diff options
866 files changed, 28696 insertions, 15388 deletions
diff --git a/core/java/android/app/LocaleManager.java b/core/java/android/app/LocaleManager.java index 794c6946f7a8..c5dedb33f954 100644 --- a/core/java/android/app/LocaleManager.java +++ b/core/java/android/app/LocaleManager.java @@ -173,7 +173,7 @@ public class LocaleManager { @TestApi public void setSystemLocales(@NonNull LocaleList locales) { try { - Configuration conf = ActivityManager.getService().getConfiguration(); + Configuration conf = new Configuration(); conf.setLocales(locales); ActivityManager.getService().updatePersistentConfiguration(conf); } catch (RemoteException e) { diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index e8379205d55f..8b41aa4a7e7d 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -8468,8 +8468,8 @@ public class Notification implements Parcelable } int maxAvatarSize = resources.getDimensionPixelSize( - isLowRam ? R.dimen.notification_person_icon_max_size - : R.dimen.notification_person_icon_max_size_low_ram); + isLowRam ? R.dimen.notification_person_icon_max_size_low_ram + : R.dimen.notification_person_icon_max_size); if (mUser != null && mUser.getIcon() != null) { mUser.getIcon().scaleDownIfNecessary(maxAvatarSize, maxAvatarSize); } diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index e022ca306674..0ea53ce52a52 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -2080,6 +2080,21 @@ public class WallpaperManager { } /** + * Set the live wallpaper for the given screen(s). + * + * This can only be called by packages with android.permission.SET_WALLPAPER_COMPONENT + * permission. The caller must hold the INTERACT_ACROSS_USERS_FULL permission to change + * another user's wallpaper. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT) + public boolean setWallpaperComponentWithFlags(@NonNull ComponentName name, + @SetWallpaperFlags int which) { + return setWallpaperComponent(name); + } + + /** * Set the display position of the current wallpaper within any larger space, when * that wallpaper is visible behind the given window. The X and Y offsets * are floating point numbers ranging from 0 to 1, representing where the diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index cc303fb1f413..2dced96d3583 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -329,6 +329,22 @@ public class AppWidgetHost { } /** + * Set the visibiity of all widgets associated with this host to hidden + * + * @hide + */ + public void setAppWidgetHidden() { + if (sService == null) { + return; + } + try { + sService.setAppWidgetHidden(mContextOpPackageName, mHostId); + } catch (RemoteException e) { + throw new RuntimeException("System server dead?", e); + } + } + + /** * Set the host's interaction handler. * * @hide @@ -418,14 +434,7 @@ public class AppWidgetHost { AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget); view.setInteractionHandler(mInteractionHandler); view.setAppWidget(appWidgetId, appWidget); - addListener(appWidgetId, view); - RemoteViews views; - try { - views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId); - } catch (RemoteException e) { - throw new RuntimeException("system server dead?", e); - } - view.updateAppWidget(views); + setListener(appWidgetId, view); return view; } @@ -513,13 +522,19 @@ public class AppWidgetHost { * 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) { + public void setListener(int appWidgetId, @NonNull AppWidgetHostListener listener) { synchronized (mListeners) { mListeners.put(appWidgetId, listener); } + RemoteViews views = null; + try { + views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId); + } catch (RemoteException e) { + throw new RuntimeException("system server dead?", e); + } + listener.updateAppWidget(views); } /** diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java index f62cc879cce3..8afd6de235a0 100644 --- a/core/java/android/os/PowerManagerInternal.java +++ b/core/java/android/os/PowerManagerInternal.java @@ -341,4 +341,10 @@ public abstract class PowerManagerInternal { * device is not awake. */ public abstract void nap(long eventTime, boolean allowWake); + + /** + * Returns true if ambient display is suppressed by any app with any token. This method will + * return false if ambient display is not available. + */ + public abstract boolean isAmbientDisplaySuppressed(); } diff --git a/core/java/android/service/dreams/DreamActivity.java b/core/java/android/service/dreams/DreamActivity.java index f6a7c8eb8c4b..a2fa1392b079 100644 --- a/core/java/android/service/dreams/DreamActivity.java +++ b/core/java/android/service/dreams/DreamActivity.java @@ -44,6 +44,8 @@ import android.text.TextUtils; public class DreamActivity extends Activity { static final String EXTRA_CALLBACK = "binder"; static final String EXTRA_DREAM_TITLE = "title"; + @Nullable + private DreamService.DreamActivityCallbacks mCallback; public DreamActivity() {} @@ -57,11 +59,19 @@ public class DreamActivity extends Activity { } final Bundle extras = getIntent().getExtras(); - final DreamService.DreamActivityCallback callback = - (DreamService.DreamActivityCallback) extras.getBinder(EXTRA_CALLBACK); + mCallback = (DreamService.DreamActivityCallbacks) extras.getBinder(EXTRA_CALLBACK); - if (callback != null) { - callback.onActivityCreated(this); + if (mCallback != null) { + mCallback.onActivityCreated(this); } } + + @Override + public void onDestroy() { + if (mCallback != null) { + mCallback.onActivityDestroyed(); + } + + super.onDestroy(); + } } diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 3c1fef02f9ba..32bdf7962273 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -1047,7 +1047,7 @@ public class DreamService extends Service implements Window.Callback { } if (mDreamToken == null) { - Slog.w(mTag, "Finish was called before the dream was attached."); + if (mDebug) Slog.v(mTag, "finish() called when not attached."); stopSelf(); return; } @@ -1295,7 +1295,7 @@ public class DreamService extends Service implements Window.Callback { Intent i = new Intent(this, DreamActivity.class); i.setPackage(getApplicationContext().getPackageName()); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallback(mDreamToken)); + i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallbacks(mDreamToken)); final ServiceInfo serviceInfo = fetchServiceInfo(this, new ComponentName(this, getClass())); i.putExtra(DreamActivity.EXTRA_DREAM_TITLE, fetchDreamLabel(this, serviceInfo)); @@ -1488,10 +1488,10 @@ public class DreamService extends Service implements Window.Callback { } /** @hide */ - final class DreamActivityCallback extends Binder { + final class DreamActivityCallbacks extends Binder { private final IBinder mActivityDreamToken; - DreamActivityCallback(IBinder token) { + DreamActivityCallbacks(IBinder token) { mActivityDreamToken = token; } @@ -1516,6 +1516,12 @@ public class DreamService extends Service implements Window.Callback { mActivity = activity; onWindowCreated(activity.getWindow()); } + + // If DreamActivity is destroyed, wake up from Dream. + void onActivityDestroyed() { + mActivity = null; + onDestroy(); + } } /** diff --git a/core/java/android/service/wallpaper/IWallpaperService.aidl b/core/java/android/service/wallpaper/IWallpaperService.aidl index 56e2486dd626..f46c60fc4f7a 100644 --- a/core/java/android/service/wallpaper/IWallpaperService.aidl +++ b/core/java/android/service/wallpaper/IWallpaperService.aidl @@ -25,6 +25,6 @@ import android.service.wallpaper.IWallpaperConnection; oneway interface IWallpaperService { void attach(IWallpaperConnection connection, IBinder windowToken, int windowType, boolean isPreview, - int reqWidth, int reqHeight, in Rect padding, int displayId); + int reqWidth, int reqHeight, in Rect padding, int displayId, int which); void detach(); } diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 01749c0bebb9..e5792a9ef4e2 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -18,6 +18,7 @@ package android.service.wallpaper; import static android.app.WallpaperManager.COMMAND_FREEZE; import static android.app.WallpaperManager.COMMAND_UNFREEZE; +import static android.app.WallpaperManager.SetWallpaperFlags; import static android.graphics.Matrix.MSCALE_X; import static android.graphics.Matrix.MSCALE_Y; import static android.graphics.Matrix.MSKEW_X; @@ -2427,7 +2428,7 @@ public abstract class WallpaperService extends Service { @Override public void attach(IWallpaperConnection conn, IBinder windowToken, int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding, - int displayId) { + int displayId, @SetWallpaperFlags int which) { mEngineWrapper = new IWallpaperEngineWrapper(mTarget, conn, windowToken, windowType, isPreview, reqWidth, reqHeight, padding, displayId); } diff --git a/core/java/android/view/RemoteAnimationTarget.java b/core/java/android/view/RemoteAnimationTarget.java index 44f419a8097d..e407707231ca 100644 --- a/core/java/android/view/RemoteAnimationTarget.java +++ b/core/java/android/view/RemoteAnimationTarget.java @@ -241,6 +241,8 @@ public class RemoteAnimationTarget implements Parcelable { */ public boolean willShowImeOnTarget; + public int rotationChange; + public RemoteAnimationTarget(int taskId, int mode, SurfaceControl leash, boolean isTranslucent, Rect clipRect, Rect contentInsets, int prefixOrderIndex, Point position, Rect localBounds, Rect screenSpaceBounds, @@ -302,6 +304,7 @@ public class RemoteAnimationTarget implements Parcelable { backgroundColor = in.readInt(); showBackdrop = in.readBoolean(); willShowImeOnTarget = in.readBoolean(); + rotationChange = in.readInt(); } public void setShowBackdrop(boolean shouldShowBackdrop) { @@ -316,6 +319,14 @@ public class RemoteAnimationTarget implements Parcelable { return willShowImeOnTarget; } + public void setRotationChange(int rotationChange) { + this.rotationChange = rotationChange; + } + + public int getRotationChange() { + return rotationChange; + } + @Override public int describeContents() { return 0; @@ -345,6 +356,7 @@ public class RemoteAnimationTarget implements Parcelable { dest.writeInt(backgroundColor); dest.writeBoolean(showBackdrop); dest.writeBoolean(willShowImeOnTarget); + dest.writeInt(rotationChange); } public void dump(PrintWriter pw, String prefix) { diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java new file mode 100644 index 000000000000..dd4385c8f50c --- /dev/null +++ b/core/java/android/window/BackProgressAnimator.java @@ -0,0 +1,136 @@ +/* + * 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.util.FloatProperty; + +import com.android.internal.dynamicanimation.animation.SpringAnimation; +import com.android.internal.dynamicanimation.animation.SpringForce; + +/** + * An animator that drives the predictive back progress with a spring. + * + * The back gesture's latest touch point and committal state determines the final position of + * the spring. The continuous movement of the spring is used to produce {@link BackEvent}s with + * smoothly transitioning progress values. + * + * @hide + */ +public class BackProgressAnimator { + /** + * A factor to scale the input progress by, so that it works better with the spring. + * We divide the output progress by this value before sending it to apps, so that apps + * always receive progress values in [0, 1]. + */ + private static final float SCALE_FACTOR = 100f; + private final SpringAnimation mSpring; + private ProgressCallback mCallback; + private float mProgress = 0; + private BackEvent mLastBackEvent; + private boolean mStarted = false; + + private void setProgress(float progress) { + mProgress = progress; + } + + private float getProgress() { + return mProgress; + } + + private static final FloatProperty<BackProgressAnimator> PROGRESS_PROP = + new FloatProperty<BackProgressAnimator>("progress") { + @Override + public void setValue(BackProgressAnimator animator, float value) { + animator.setProgress(value); + animator.updateProgressValue(value); + } + + @Override + public Float get(BackProgressAnimator object) { + return object.getProgress(); + } + }; + + + /** A callback to be invoked when there's a progress value update from the animator. */ + public interface ProgressCallback { + /** Called when there's a progress value update. */ + void onProgressUpdate(BackEvent event); + } + + public BackProgressAnimator() { + mSpring = new SpringAnimation(this, PROGRESS_PROP); + mSpring.setSpring(new SpringForce() + .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); + } + + /** + * Sets a new target position for the back progress. + * + * @param event the {@link BackEvent} containing the latest target progress. + */ + public void onBackProgressed(BackEvent event) { + if (!mStarted) { + return; + } + mLastBackEvent = event; + mSpring.animateToFinalPosition(event.getProgress() * SCALE_FACTOR); + } + + /** + * Starts the back progress animation. + * + * @param event the {@link BackEvent} that started the gesture. + * @param callback the back callback to invoke for the gesture. It will receive back progress + * dispatches as the progress animation updates. + */ + public void onBackStarted(BackEvent event, ProgressCallback callback) { + reset(); + mLastBackEvent = event; + mCallback = callback; + mStarted = true; + } + + /** + * Resets the back progress animation. This should be called when back is invoked or cancelled. + */ + public void reset() { + mSpring.animateToFinalPosition(0); + if (mSpring.canSkipToEnd()) { + mSpring.skipToEnd(); + } else { + // Should never happen. + mSpring.cancel(); + } + mStarted = false; + mLastBackEvent = null; + mCallback = null; + mProgress = 0; + } + + private void updateProgressValue(float progress) { + if (mLastBackEvent == null || mCallback == null || !mStarted) { + return; + } + mCallback.onProgressUpdate( + new BackEvent(mLastBackEvent.getTouchX(), mLastBackEvent.getTouchY(), + progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge(), + mLastBackEvent.getDepartingAnimationTarget())); + } + +} diff --git a/core/java/android/window/IOnBackInvokedCallback.aidl b/core/java/android/window/IOnBackInvokedCallback.aidl index 47796de11dd5..6af8ddda3a62 100644 --- a/core/java/android/window/IOnBackInvokedCallback.aidl +++ b/core/java/android/window/IOnBackInvokedCallback.aidl @@ -28,17 +28,18 @@ import android.window.BackEvent; oneway interface IOnBackInvokedCallback { /** * Called when a back gesture has been started, or back button has been pressed down. - * Wraps {@link OnBackInvokedCallback#onBackStarted()}. + * Wraps {@link OnBackInvokedCallback#onBackStarted(BackEvent)}. + * + * @param backEvent The {@link BackEvent} containing information about the touch or button press. */ - void onBackStarted(); + void onBackStarted(in BackEvent backEvent); /** * Called on back gesture progress. - * Wraps {@link OnBackInvokedCallback#onBackProgressed()}. + * Wraps {@link OnBackInvokedCallback#onBackProgressed(BackEvent)}. * - * @param touchX Absolute X location of the touch point. - * @param touchY Absolute Y location of the touch point. - * @param progress Value between 0 and 1 on how far along the back gesture is. + * @param backEvent The {@link BackEvent} containing information about the latest touch point + * and the progress that the back animation should seek to. */ void onBackProgressed(in BackEvent backEvent); diff --git a/core/java/android/window/OnBackAnimationCallback.java b/core/java/android/window/OnBackAnimationCallback.java index 1a37e57df403..582308436b02 100644 --- a/core/java/android/window/OnBackAnimationCallback.java +++ b/core/java/android/window/OnBackAnimationCallback.java @@ -40,10 +40,12 @@ import android.view.View; * @hide */ public interface OnBackAnimationCallback extends OnBackInvokedCallback { - /** - * Called when a back gesture has been started, or back button has been pressed down. - */ - default void onBackStarted() { } + /** + * Called when a back gesture has been started, or back button has been pressed down. + * + * @param backEvent An {@link BackEvent} object describing the progress event. + */ + default void onBackStarted(@NonNull BackEvent backEvent) {} /** * Called on back gesture progress. diff --git a/core/java/android/window/OnBackInvokedCallback.java b/core/java/android/window/OnBackInvokedCallback.java index 6e2d4f9edbc1..62c41bfb0681 100644 --- a/core/java/android/window/OnBackInvokedCallback.java +++ b/core/java/android/window/OnBackInvokedCallback.java @@ -16,6 +16,7 @@ package android.window; +import android.annotation.NonNull; import android.app.Activity; import android.app.Dialog; import android.view.Window; @@ -41,8 +42,35 @@ import android.view.Window; @SuppressWarnings("deprecation") public interface OnBackInvokedCallback { /** + * Called when a back gesture has been started, or back button has been pressed down. + * + * @param backEvent The {@link BackEvent} containing information about the touch or + * button press. + * + * @hide + */ + default void onBackStarted(@NonNull BackEvent backEvent) {} + + /** + * Called when a back gesture has been progressed. + * + * @param backEvent The {@link BackEvent} containing information about the latest touch point + * and the progress that the back animation should seek to. + * + * @hide + */ + default void onBackProgressed(@NonNull BackEvent backEvent) {} + + /** * Called when a back gesture has been completed and committed, or back button pressed * has been released and committed. */ void onBackInvoked(); + + /** + * Called when a back gesture or button press has been cancelled. + * + * @hide + */ + default void onBackCancelled() {} } diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 0730f3ddf8ac..fda39c14dac7 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -218,19 +218,24 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { public Checker getChecker() { return mChecker; } + @NonNull + private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub { private final WeakReference<OnBackInvokedCallback> mCallback; + OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) { mCallback = new WeakReference<>(callback); } @Override - public void onBackStarted() { + public void onBackStarted(BackEvent backEvent) { Handler.getMain().post(() -> { final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { - callback.onBackStarted(); + mProgressAnimator.onBackStarted(backEvent, event -> + callback.onBackProgressed(event)); + callback.onBackStarted(backEvent); } }); } @@ -240,7 +245,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { Handler.getMain().post(() -> { final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { - callback.onBackProgressed(backEvent); + mProgressAnimator.onBackProgressed(backEvent); } }); } @@ -248,6 +253,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @Override public void onBackCancelled() { Handler.getMain().post(() -> { + mProgressAnimator.reset(); final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { callback.onBackCancelled(); @@ -258,6 +264,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @Override public void onBackInvoked() throws RemoteException { Handler.getMain().post(() -> { + mProgressAnimator.reset(); final OnBackInvokedCallback callback = mCallback.get(); if (callback == null) { return; diff --git a/core/java/com/android/internal/app/AppLocaleCollector.java b/core/java/com/android/internal/app/AppLocaleCollector.java new file mode 100644 index 000000000000..65e8c646e17d --- /dev/null +++ b/core/java/com/android/internal/app/AppLocaleCollector.java @@ -0,0 +1,139 @@ +/* + * 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.internal.app; + +import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET; +import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG; + +import android.content.Context; +import android.os.Build; +import android.os.LocaleList; +import android.util.Log; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +/** The Locale data collector for per-app language. */ +class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase { + private static final String TAG = AppLocaleCollector.class.getSimpleName(); + private final Context mContext; + private final String mAppPackageName; + private final LocaleStore.LocaleInfo mAppCurrentLocale; + + AppLocaleCollector(Context context, String appPackageName) { + mContext = context; + mAppPackageName = appPackageName; + mAppCurrentLocale = LocaleStore.getAppCurrentLocaleInfo( + mContext, mAppPackageName); + } + + @Override + public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) { + HashSet<String> langTagsToIgnore = new HashSet<>(); + + LocaleList systemLangList = LocaleList.getDefault(); + for(int i = 0; i < systemLangList.size(); i++) { + langTagsToIgnore.add(systemLangList.get(i).toLanguageTag()); + } + + if (mAppCurrentLocale != null) { + langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag()); + } + return langTagsToIgnore; + } + + @Override + public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent, + boolean translatedOnly, boolean isForCountryMode) { + AppLocaleStore.AppLocaleResult result = + AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName); + Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly); + Set<LocaleStore.LocaleInfo> appLocaleList = new HashSet<>(); + Set<LocaleStore.LocaleInfo> systemLocaleList; + boolean shouldShowList = + result.mLocaleStatus == GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG + || result.mLocaleStatus == GET_SUPPORTED_LANGUAGE_FROM_ASSET; + + // Get system supported locale list + if (isForCountryMode) { + systemLocaleList = LocaleStore.getLevelLocales(mContext, + langTagsToIgnore, parent, translatedOnly); + } else { + systemLocaleList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore, + null /* no parent */, translatedOnly); + } + + // Add current app locale + if (mAppCurrentLocale != null && !isForCountryMode) { + appLocaleList.add(mAppCurrentLocale); + } + + // Add current system language into suggestion list + for(LocaleStore.LocaleInfo localeInfo: + LocaleStore.getSystemCurrentLocaleInfo()) { + boolean isNotCurrentLocale = mAppCurrentLocale == null + || !localeInfo.getLocale().equals(mAppCurrentLocale.getLocale()); + if (!isForCountryMode && isNotCurrentLocale) { + appLocaleList.add(localeInfo); + } + } + + // Add the languages that included in system supported locale + if (shouldShowList) { + appLocaleList.addAll(filterTheLanguagesNotIncludedInSystemLocale( + systemLocaleList, result.mAppSupportedLocales)); + } + + // Add "system language" option + if (!isForCountryMode && shouldShowList) { + appLocaleList.add(LocaleStore.getSystemDefaultLocaleInfo( + mAppCurrentLocale == null)); + } + + if (Build.isDebuggable()) { + Log.d(TAG, "App locale list: " + appLocaleList); + } + + return appLocaleList; + } + + @Override + public boolean hasSpecificPackageName() { + return true; + } + + private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotIncludedInSystemLocale( + Set<LocaleStore.LocaleInfo> systemLocaleList, + HashSet<Locale> appSupportedLocales) { + Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>(); + + for(LocaleStore.LocaleInfo li: systemLocaleList) { + if (appSupportedLocales.contains(li.getLocale())) { + filteredList.add(li); + } else { + for(Locale l: appSupportedLocales) { + if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) { + filteredList.add(li); + break; + } + } + } + } + return filteredList; + } +} diff --git a/core/java/com/android/internal/app/LocalePicker.java b/core/java/com/android/internal/app/LocalePicker.java index 3c53d07b6180..7dd1d2607149 100644 --- a/core/java/com/android/internal/app/LocalePicker.java +++ b/core/java/com/android/internal/app/LocalePicker.java @@ -311,8 +311,7 @@ public class LocalePicker extends ListFragment { try { final IActivityManager am = ActivityManager.getService(); - final Configuration config = am.getConfiguration(); - + final Configuration config = new Configuration(); config.setLocales(locales); config.userSetLocale = true; diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java index 965895f08d6e..3efd279c2639 100644 --- a/core/java/com/android/internal/app/LocalePickerWithRegion.java +++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java @@ -16,16 +16,12 @@ package com.android.internal.app; -import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus; - import android.app.FragmentManager; import android.app.FragmentTransaction; import android.app.ListFragment; import android.content.Context; import android.os.Bundle; -import android.os.LocaleList; import android.text.TextUtils; -import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -36,7 +32,6 @@ import android.widget.SearchView; import com.android.internal.R; -import java.util.Collections; import java.util.HashSet; import java.util.Locale; import java.util.Set; @@ -54,6 +49,7 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O private SuggestedLocaleAdapter mAdapter; private LocaleSelectedListener mListener; + private LocaleCollectorBase mLocalePickerCollector; private Set<LocaleStore.LocaleInfo> mLocaleList; private LocaleStore.LocaleInfo mParentLocale; private boolean mTranslatedOnly = false; @@ -62,7 +58,6 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O private boolean mPreviousSearchHadFocus = false; private int mFirstVisiblePosition = 0; private int mTopDistance = 0; - private String mAppPackageName; private CharSequence mTitle = null; private OnActionExpandListener mOnActionExpandListener; @@ -79,31 +74,50 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O void onLocaleSelected(LocaleStore.LocaleInfo locale); } - private static LocalePickerWithRegion createCountryPicker(Context context, + /** + * The interface which provides the locale list. + */ + interface LocaleCollectorBase { + /** Gets the ignored locale list. */ + HashSet<String> getIgnoredLocaleList(boolean translatedOnly); + + /** Gets the supported locale list. */ + Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent, + boolean translatedOnly, boolean isForCountryMode); + + /** Indicates if the class work for specific package. */ + boolean hasSpecificPackageName(); + } + + private static LocalePickerWithRegion createCountryPicker( LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, - boolean translatedOnly, String appPackageName, - OnActionExpandListener onActionExpandListener) { + boolean translatedOnly, OnActionExpandListener onActionExpandListener, + LocaleCollectorBase localePickerCollector) { LocalePickerWithRegion localePicker = new LocalePickerWithRegion(); localePicker.setOnActionExpandListener(onActionExpandListener); - boolean shouldShowTheList = localePicker.setListener(context, listener, parent, - translatedOnly, appPackageName); + boolean shouldShowTheList = localePicker.setListener(listener, parent, + translatedOnly, localePickerCollector); return shouldShowTheList ? localePicker : null; } public static LocalePickerWithRegion createLanguagePicker(Context context, LocaleSelectedListener listener, boolean translatedOnly) { - LocalePickerWithRegion localePicker = new LocalePickerWithRegion(); - localePicker.setListener(context, listener, /* parent */ null, translatedOnly, null); - return localePicker; + return createLanguagePicker(context, listener, translatedOnly, null, null); } public static LocalePickerWithRegion createLanguagePicker(Context context, LocaleSelectedListener listener, boolean translatedOnly, String appPackageName, OnActionExpandListener onActionExpandListener) { + LocaleCollectorBase localePickerController; + if (TextUtils.isEmpty(appPackageName)) { + localePickerController = new SystemLocaleCollector(context); + } else { + localePickerController = new AppLocaleCollector(context, appPackageName); + } LocalePickerWithRegion localePicker = new LocalePickerWithRegion(); localePicker.setOnActionExpandListener(onActionExpandListener); - localePicker.setListener( - context, listener, /* parent */ null, translatedOnly, appPackageName); + localePicker.setListener(listener, /* parent */ null, translatedOnly, + localePickerController); return localePicker; } @@ -120,109 +134,23 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O * In this case we don't even show the list, we call the listener with that locale, * "pretending" it was selected, and return false.</p> */ - private boolean setListener(Context context, LocaleSelectedListener listener, - LocaleStore.LocaleInfo parent, boolean translatedOnly, String appPackageName) { + private boolean setListener(LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, + boolean translatedOnly, LocaleCollectorBase localePickerController) { this.mParentLocale = parent; this.mListener = listener; this.mTranslatedOnly = translatedOnly; - this.mAppPackageName = appPackageName; + this.mLocalePickerCollector = localePickerController; setRetainInstance(true); - final HashSet<String> langTagsToIgnore = new HashSet<>(); - LocaleStore.LocaleInfo appCurrentLocale = - LocaleStore.getAppCurrentLocaleInfo(context, appPackageName); - boolean isForCountryMode = parent != null; - - if (!TextUtils.isEmpty(appPackageName) && !isForCountryMode) { - // Filter current system locale to add them into suggestion - LocaleList systemLangList = LocaleList.getDefault(); - for(int i = 0; i < systemLangList.size(); i++) { - langTagsToIgnore.add(systemLangList.get(i).toLanguageTag()); - } - - if (appCurrentLocale != null) { - Log.d(TAG, "appCurrentLocale: " + appCurrentLocale.getLocale().toLanguageTag()); - langTagsToIgnore.add(appCurrentLocale.getLocale().toLanguageTag()); - } else { - Log.d(TAG, "appCurrentLocale is null"); - } - } else if (!translatedOnly) { - final LocaleList userLocales = LocalePicker.getLocales(); - final String[] langTags = userLocales.toLanguageTags().split(","); - Collections.addAll(langTagsToIgnore, langTags); - } + mLocaleList = localePickerController.getSupportedLocaleList( + parent, translatedOnly, parent != null); - if (isForCountryMode) { - mLocaleList = LocaleStore.getLevelLocales(context, - langTagsToIgnore, parent, translatedOnly); - if (mLocaleList.size() <= 1) { - if (listener != null && (mLocaleList.size() == 1)) { - listener.onLocaleSelected(mLocaleList.iterator().next()); - } - return false; - } + if (parent != null && listener != null && mLocaleList.size() == 1) { + listener.onLocaleSelected(mLocaleList.iterator().next()); + return false; } else { - mLocaleList = LocaleStore.getLevelLocales(context, langTagsToIgnore, - null /* no parent */, translatedOnly); + return true; } - Log.d(TAG, "mLocaleList size: " + mLocaleList.size()); - - // Adding current locale and system default option into suggestion list - if(!TextUtils.isEmpty(appPackageName)) { - if (appCurrentLocale != null && !isForCountryMode) { - mLocaleList.add(appCurrentLocale); - } - - AppLocaleStore.AppLocaleResult result = - AppLocaleStore.getAppSupportedLocales(context, appPackageName); - boolean shouldShowList = - result.mLocaleStatus == LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG - || result.mLocaleStatus == LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET; - - // Add current system language into suggestion list - for(LocaleStore.LocaleInfo localeInfo: LocaleStore.getSystemCurrentLocaleInfo()) { - boolean isNotCurrentLocale = appCurrentLocale == null - || !localeInfo.getLocale().equals(appCurrentLocale.getLocale()); - if (!isForCountryMode && isNotCurrentLocale) { - mLocaleList.add(localeInfo); - } - } - - // Filter the language not support in app - mLocaleList = filterTheLanguagesNotSupportedInApp( - shouldShowList, result.mAppSupportedLocales); - - Log.d(TAG, "mLocaleList after app-supported filter: " + mLocaleList.size()); - - // Add "system language" - if (!isForCountryMode && shouldShowList) { - mLocaleList.add(LocaleStore.getSystemDefaultLocaleInfo(appCurrentLocale == null)); - } - } - return true; - } - - private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotSupportedInApp( - boolean shouldShowList, HashSet<Locale> supportedLocales) { - Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>(); - if (!shouldShowList) { - return filteredList; - } - - for(LocaleStore.LocaleInfo li: mLocaleList) { - if (supportedLocales.contains(li.getLocale())) { - filteredList.add(li); - } else { - for(Locale l: supportedLocales) { - if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) { - filteredList.add(li); - break; - } - } - } - } - - return filteredList; } private void returnToParentFrame() { @@ -246,7 +174,9 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O mTitle = getActivity().getTitle(); final boolean countryMode = mParentLocale != null; final Locale sortingLocale = countryMode ? mParentLocale.getLocale() : Locale.getDefault(); - mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode, mAppPackageName); + final boolean hasSpecificPackageName = + mLocalePickerCollector != null && mLocalePickerCollector.hasSpecificPackageName(); + mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode, hasSpecificPackageName); final LocaleHelper.LocaleInfoComparator comp = new LocaleHelper.LocaleInfoComparator(sortingLocale, countryMode); mAdapter.sort(comp); @@ -321,8 +251,8 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O returnToParentFrame(); } else { LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker( - getContext(), mListener, locale, mTranslatedOnly /* translate only */, - mAppPackageName, mOnActionExpandListener); + mListener, locale, mTranslatedOnly /* translate only */, + mOnActionExpandListener, this.mLocalePickerCollector); if (selector != null) { getFragmentManager().beginTransaction() .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) @@ -340,7 +270,8 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O inflater.inflate(R.menu.language_selection_list, menu); final MenuItem searchMenuItem = menu.findItem(R.id.locale_search_menu); - if (!TextUtils.isEmpty(mAppPackageName) && mOnActionExpandListener != null) { + if (mLocalePickerCollector.hasSpecificPackageName() + && mOnActionExpandListener != null) { searchMenuItem.setOnActionExpandListener(mOnActionExpandListener); } diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java index 4a1f7eb06c40..42b46cda6ba3 100644 --- a/core/java/com/android/internal/app/ResolverListAdapter.java +++ b/core/java/com/android/internal/app/ResolverListAdapter.java @@ -647,15 +647,16 @@ public class ResolverListAdapter extends BaseAdapter { if (info instanceof DisplayResolveInfo) { DisplayResolveInfo dri = (DisplayResolveInfo) info; - boolean hasLabel = dri.hasDisplayLabel(); - holder.bindLabel( - dri.getDisplayLabel(), - dri.getExtendedInfo(), - hasLabel && alwaysShowSubLabel()); - holder.bindIcon(info); - if (!hasLabel) { + if (dri.hasDisplayLabel()) { + holder.bindLabel( + dri.getDisplayLabel(), + dri.getExtendedInfo(), + alwaysShowSubLabel()); + } else { + holder.bindLabel("", "", false); loadLabel(dri); } + holder.bindIcon(info); if (!dri.hasDisplayIcon()) { loadIcon(dri); } diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java index 1be1247b7cc0..5ed0e8bc7883 100644 --- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java +++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java @@ -70,17 +70,17 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { private Locale mDisplayLocale = null; // used to potentially cache a modified Context that uses mDisplayLocale private Context mContextOverride = null; - private String mAppPackageName; + private boolean mHasSpecificAppPackageName; public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode) { - this(localeOptions, countryMode, null); + this(localeOptions, countryMode, false); } public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode, - String appPackageName) { + boolean hasSpecificAppPackageName) { mCountryMode = countryMode; mLocaleOptions = new ArrayList<>(localeOptions.size()); - mAppPackageName = appPackageName; + mHasSpecificAppPackageName = hasSpecificAppPackageName; for (LocaleStore.LocaleInfo li : localeOptions) { if (li.isSuggested()) { @@ -134,7 +134,7 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { @Override public int getViewTypeCount() { - if (!TextUtils.isEmpty(mAppPackageName) && showHeaders()) { + if (mHasSpecificAppPackageName && showHeaders()) { // Two headers, 1 "System language", 1 current locale return APP_LANGUAGE_PICKER_TYPE_COUNT; } else if (showHeaders()) { diff --git a/core/java/com/android/internal/app/SystemLocaleCollector.java b/core/java/com/android/internal/app/SystemLocaleCollector.java new file mode 100644 index 000000000000..9a6d4c192fdc --- /dev/null +++ b/core/java/com/android/internal/app/SystemLocaleCollector.java @@ -0,0 +1,66 @@ +/* + * 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.internal.app; + +import android.content.Context; +import android.os.LocaleList; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** The Locale data collector for System language. */ +class SystemLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase { + private final Context mContext; + + SystemLocaleCollector(Context context) { + mContext = context; + } + + @Override + public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) { + HashSet<String> ignoreList = new HashSet<>(); + if (!translatedOnly) { + final LocaleList userLocales = LocalePicker.getLocales(); + final String[] langTags = userLocales.toLanguageTags().split(","); + Collections.addAll(ignoreList, langTags); + } + return ignoreList; + } + + @Override + public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent, + boolean translatedOnly, boolean isForCountryMode) { + Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly); + Set<LocaleStore.LocaleInfo> localeList; + + if (isForCountryMode) { + localeList = LocaleStore.getLevelLocales(mContext, + langTagsToIgnore, parent, translatedOnly); + } else { + localeList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore, + null /* no parent */, translatedOnly); + } + return localeList; + } + + + @Override + public boolean hasSpecificPackageName() { + return false; + } +}
\ No newline at end of file diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl index 2d68cb472fa3..51b56dbf582b 100644 --- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl +++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl @@ -45,6 +45,7 @@ interface IAppWidgetService { @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) RemoteViews getAppWidgetViews(String callingPackage, int appWidgetId); int[] getAppWidgetIdsForHost(String callingPackage, int hostId); + void setAppWidgetHidden(in String callingPackage, int hostId); IntentSender createAppWidgetConfigIntentSender(String callingPackage, int appWidgetId, int intentFlags); diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 5de72409133d..a05062b43d60 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -45,6 +45,7 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_ENTER_TRANSITION; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_EXIT_TRANSITION; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PIP_TRANSITION; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF_SHOW_AOD; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_PAGE_SCROLL; @@ -223,6 +224,7 @@ public class InteractionJankMonitor { public static final int CUJ_SHADE_CLEAR_ALL = 62; public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = 63; public static final int CUJ_LOCKSCREEN_OCCLUSION = 64; + public static final int CUJ_RECENTS_SCROLLING = 65; private static final int NO_STATSD_LOGGING = -1; @@ -296,6 +298,7 @@ public class InteractionJankMonitor { UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_CLEAR_ALL, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING, }; private static volatile InteractionJankMonitor sInstance; @@ -380,7 +383,8 @@ public class InteractionJankMonitor { CUJ_TASKBAR_COLLAPSE, CUJ_SHADE_CLEAR_ALL, CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, - CUJ_LOCKSCREEN_OCCLUSION + CUJ_LOCKSCREEN_OCCLUSION, + CUJ_RECENTS_SCROLLING }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -893,6 +897,8 @@ public class InteractionJankMonitor { return "LAUNCHER_UNLOCK_ENTRANCE_ANIMATION"; case CUJ_LOCKSCREEN_OCCLUSION: return "LOCKSCREEN_OCCLUSION"; + case CUJ_RECENTS_SCROLLING: + return "RECENTS_SCROLLING"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 8b96597767d3..6fed26c4a81d 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -1326,6 +1326,13 @@ public class BatteryStatsImpl extends BatteryStats { LongSamplingCounter mMobileRadioActiveUnknownTime; LongSamplingCounter mMobileRadioActiveUnknownCount; + /** + * The soonest the Mobile Radio stats can be updated due to a mobile radio power state change + * after it was last updated. + */ + @VisibleForTesting + protected static final long MOBILE_RADIO_POWER_STATE_UPDATE_FREQ_MS = 1000 * 60 * 10; + int mWifiRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW; @GuardedBy("this") @@ -6260,6 +6267,15 @@ public class BatteryStatsImpl extends BatteryStats { } else { mMobileRadioActiveTimer.stopRunningLocked(realElapsedRealtimeMs); mMobileRadioActivePerAppTimer.stopRunningLocked(realElapsedRealtimeMs); + + if (mLastModemActivityInfo != null) { + if (elapsedRealtimeMs < mLastModemActivityInfo.getTimestampMillis() + + MOBILE_RADIO_POWER_STATE_UPDATE_FREQ_MS) { + // Modem Activity info has been collected recently, don't bother + // triggering another update. + return false; + } + } // Tell the caller to collect radio network/power stats. return true; } diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 44cfe1aa4a79..1d4b246de5c8 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -322,4 +322,7 @@ oneway interface IStatusBar /** Unregisters a nearby media devices provider. */ void unregisterNearbyMediaDevicesProvider(in INearbyMediaDevicesProvider provider); + + /** Dump protos from SystemUI. The proto definition is defined there */ + void dumpProto(in String[] args, in ParcelFileDescriptor pfd); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 964fe2d57b0d..f7467b5a9f86 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1147,7 +1147,28 @@ android:protectionLevel="dangerous" /> <!-- Allows an application to write to external storage. - <p class="note"><strong>Note:</strong> If <em>both</em> your <a + <p><strong>Note: </strong>If your app targets {@link android.os.Build.VERSION_CODES#R} or + higher, this permission has no effect. + + <p>If your app is on a device that runs API level 19 or higher, you don't need to declare + this permission to read and write files in your application-specific directories returned + by {@link android.content.Context#getExternalFilesDir} and + {@link android.content.Context#getExternalCacheDir}. + + <p>Learn more about how to + <a href="{@docRoot}training/data-storage/shared/media#update-other-apps-files">modify media + files</a> that your app doesn't own, and how to + <a href="{@docRoot}training/data-storage/shared/documents-files">modify non-media files</a> + that your app doesn't own. + + <p>If your app is a file manager and needs broad access to external storage files, then + the system must place your app on an allowlist so that you can successfully request the + <a href="#MANAGE_EXTERNAL_STORAGE><code>MANAGE_EXTERNAL_STORAGE</code></a> permission. + Learn more about the appropriate use cases for + <a href="{@docRoot}training/data-storage/manage-all-files>managing all files on a storage + device</a>. + + <p>If <em>both</em> your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code minSdkVersion}</a> and <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code @@ -1155,12 +1176,6 @@ grants your app this permission. If you don't need this permission, be sure your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code targetSdkVersion}</a> is 4 or higher. - <p>Starting in API level 19, this permission is <em>not</em> required to - read/write files in your application-specific directories returned by - {@link android.content.Context#getExternalFilesDir} and - {@link android.content.Context#getExternalCacheDir}. - <p>If this permission is not allowlisted for an app that targets an API level before - {@link android.os.Build.VERSION_CODES#Q} this permission cannot be granted to apps.</p> <p>Protection level: dangerous</p> --> <permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" diff --git a/core/res/res/anim/dream_activity_close_exit.xml b/core/res/res/anim/dream_activity_close_exit.xml index c4599dad31a0..8df624fdd2e5 100644 --- a/core/res/res/anim/dream_activity_close_exit.xml +++ b/core/res/res/anim/dream_activity_close_exit.xml @@ -19,5 +19,5 @@ <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:fromAlpha="1.0" android:toAlpha="0.0" - android:duration="100" /> + android:duration="@integer/config_dreamCloseAnimationDuration" /> diff --git a/core/res/res/anim/dream_activity_open_enter.xml b/core/res/res/anim/dream_activity_open_enter.xml index 9e1c6e2ee0d7..d6d9c5c990f8 100644 --- a/core/res/res/anim/dream_activity_open_enter.xml +++ b/core/res/res/anim/dream_activity_open_enter.xml @@ -22,5 +22,5 @@ those two has to be the same. --> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:fromAlpha="0.0" android:toAlpha="1.0" - android:duration="1000" /> + android:duration="@integer/config_dreamOpenAnimationDuration" /> diff --git a/core/res/res/anim/dream_activity_open_exit.xml b/core/res/res/anim/dream_activity_open_exit.xml index 740f52856b7f..2c2e501eda69 100644 --- a/core/res/res/anim/dream_activity_open_exit.xml +++ b/core/res/res/anim/dream_activity_open_exit.xml @@ -22,4 +22,4 @@ dream_activity_open_enter animation. --> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:fromAlpha="1.0" android:toAlpha="1.0" - android:duration="1000" /> + android:duration="@integer/config_dreamOpenAnimationDuration" /> diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml index a7f2aa7cba69..be1c939f0ff8 100644 --- a/core/res/res/layout/notification_template_header.xml +++ b/core/res/res/layout/notification_template_header.xml @@ -24,6 +24,7 @@ android:gravity="center_vertical" android:orientation="horizontal" android:theme="@style/Theme.DeviceDefault.Notification" + android:importantForAccessibility="no" > <ImageView diff --git a/core/res/res/values-es-rUS/strings.xml b/core/res/res/values-es-rUS/strings.xml index 2a3f916dcdec..e8e5a514d15f 100644 --- a/core/res/res/values-es-rUS/strings.xml +++ b/core/res/res/values-es-rUS/strings.xml @@ -1970,8 +1970,8 @@ <string name="usb_mtp_launch_notification_description" msgid="6942535713629852684">"Presiona para ver archivos"</string> <string name="pin_target" msgid="8036028973110156895">"Fijar"</string> <string name="pin_specific_target" msgid="7824671240625957415">"Fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string> - <string name="unpin_target" msgid="3963318576590204447">"No fijar"</string> - <string name="unpin_specific_target" msgid="3859828252160908146">"No fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string> + <string name="unpin_target" msgid="3963318576590204447">"Dejar de fijar"</string> + <string name="unpin_specific_target" msgid="3859828252160908146">"Dejar de fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string> <string name="app_info" msgid="6113278084877079851">"Información de apps"</string> <string name="negative_duration" msgid="1938335096972945232">"−<xliff:g id="TIME">%1$s</xliff:g>"</string> <string name="demo_starting_message" msgid="6577581216125805905">"Iniciando demostración…"</string> diff --git a/core/res/res/values-hi/strings.xml b/core/res/res/values-hi/strings.xml index 79faaac94919..0daa3249cc0c 100644 --- a/core/res/res/values-hi/strings.xml +++ b/core/res/res/values-hi/strings.xml @@ -1139,7 +1139,7 @@ <string name="copy" msgid="5472512047143665218">"कॉपी करें"</string> <string name="failed_to_copy_to_clipboard" msgid="725919885138539875">"क्लिपबोर्ड पर कॉपी नहीं हो सका"</string> <string name="paste" msgid="461843306215520225">"चिपकाएं"</string> - <string name="paste_as_plain_text" msgid="7664800665823182587">"सादे पाठ के रूप में चिपकाएं"</string> + <string name="paste_as_plain_text" msgid="7664800665823182587">"सादे टेक्स्ट के रूप में चिपकाएं"</string> <string name="replace" msgid="7842675434546657444">"बदलें•"</string> <string name="delete" msgid="1514113991712129054">"मिटाएं"</string> <string name="copyUrl" msgid="6229645005987260230">"यूआरएल को कॉपी करें"</string> diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml index 9ab64e6f26df..d11eca61752f 100644 --- a/core/res/res/values-ko/strings.xml +++ b/core/res/res/values-ko/strings.xml @@ -252,7 +252,7 @@ <string name="bugreport_message" msgid="5212529146119624326">"현재 기기 상태에 대한 정보를 수집하여 이메일 메시지로 전송합니다. 버그 신고를 시작하여 전송할 준비가 되려면 약간 시간이 걸립니다."</string> <string name="bugreport_option_interactive_title" msgid="7968287837902871289">"대화형 보고서"</string> <string name="bugreport_option_interactive_summary" msgid="8493795476325339542">"대부분의 경우 이 옵션을 사용합니다. 신고 진행 상황을 추적하고 문제에 대한 세부정보를 입력하고 스크린샷을 찍을 수 있습니다. 신고하기에 시간이 너무 오래 걸리고 사용 빈도가 낮은 일부 섹션을 생략할 수 있습니다."</string> - <string name="bugreport_option_full_title" msgid="7681035745950045690">"전체 보고서"</string> + <string name="bugreport_option_full_title" msgid="7681035745950045690">"전체 신고"</string> <string name="bugreport_option_full_summary" msgid="1975130009258435885">"기기가 응답하지 않거나 너무 느리거나 모든 보고서 섹션이 필요한 경우 이 옵션을 사용하여 시스템 방해를 최소화합니다. 세부정보를 추가하거나 스크린샷을 추가로 찍을 수 없습니다."</string> <string name="bugreport_countdown" msgid="6418620521782120755">"{count,plural, =1{버그 신고 스크린샷을 #초 후에 찍습니다.}other{버그 신고 스크린샷을 #초 후에 찍습니다.}}"</string> <string name="bugreport_screenshot_success_toast" msgid="7986095104151473745">"버그 신고용 스크린샷 촬영 완료"</string> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 077b6181743a..881d49991720 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2482,6 +2482,11 @@ <!-- Whether dreams are disabled when ambient mode is suppressed. --> <bool name="config_dreamsDisabledByAmbientModeSuppressionConfig">false</bool> + <!-- The duration in milliseconds of the dream opening animation. --> + <integer name="config_dreamOpenAnimationDuration">250</integer> + <!-- The duration in milliseconds of the dream closing animation. --> + <integer name="config_dreamCloseAnimationDuration">100</integer> + <!-- Whether to dismiss the active dream when an activity is started. Doesn't apply to assistant activities (ACTIVITY_TYPE_ASSISTANT) --> <bool name="config_dismissDreamOnActivityStart">false</bool> @@ -3577,9 +3582,9 @@ config_sidefpsSkipWaitForPowerVendorAcquireMessage --> <integer name="config_sidefpsSkipWaitForPowerAcquireMessage">6</integer> - <!-- This vendor acquired message that will cause the sidefpsKgPowerPress window to be skipped. - config_sidefpsSkipWaitForPowerOnFingerUp must be true and - config_sidefpsSkipWaitForPowerAcquireMessage must be BIOMETRIC_ACQUIRED_VENDOR == 6. --> + <!-- This vendor acquired message will cause the sidefpsKgPowerPress window to be skipped + when config_sidefpsSkipWaitForPowerAcquireMessage == 6 (VENDOR) and the vendor acquire + message equals this constant --> <integer name="config_sidefpsSkipWaitForPowerVendorAcquireMessage">2</integer> <!-- This config is used to force VoiceInteractionService to start on certain low ram devices. @@ -5958,4 +5963,8 @@ TODO(b/236022708) Move rear display state to device state config file --> <integer name="config_deviceStateRearDisplay">-1</integer> + + <!-- Whether the lock screen is allowed to run its own live wallpaper, + different from the home screen wallpaper. --> + <bool name="config_independentLockscreenLiveWallpaper">false</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 6ba46a8fdb4d..7a7b43a3632a 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2237,6 +2237,8 @@ <java-symbol type="string" name="config_dreamsDefaultComponent" /> <java-symbol type="bool" name="config_dreamsDisabledByAmbientModeSuppressionConfig" /> <java-symbol type="bool" name="config_dreamsOnlyEnabledForSystemUser" /> + <java-symbol type="integer" name="config_dreamOpenAnimationDuration" /> + <java-symbol type="integer" name="config_dreamCloseAnimationDuration" /> <java-symbol type="array" name="config_supportedDreamComplications" /> <java-symbol type="array" name="config_disabledDreamComponents" /> <java-symbol type="bool" name="config_dismissDreamOnActivityStart" /> @@ -4847,6 +4849,7 @@ <java-symbol type="array" name="config_deviceStatesAvailableForAppRequests" /> <java-symbol type="array" name="config_serviceStateLocationAllowedPackages" /> <java-symbol type="integer" name="config_deviceStateRearDisplay"/> + <java-symbol type="bool" name="config_independentLockscreenLiveWallpaper"/> <!-- For app language picker --> <java-symbol type="string" name="system_locale_title" /> diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index 0b8b29b9dda9..bcb13d2108b8 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -48,6 +48,7 @@ import static com.android.internal.util.ContrastColorUtilTest.assertContrastIsWi import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.fail; import static org.junit.Assert.assertEquals; @@ -56,7 +57,9 @@ import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import android.annotation.Nullable; import android.app.Notification.CallStyle; @@ -68,6 +71,7 @@ import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; +import android.graphics.Typeface; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Build; @@ -79,7 +83,9 @@ import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; import android.text.style.TextAppearanceSpan; +import android.util.Pair; import android.widget.RemoteViews; import androidx.test.InstrumentationRegistry; @@ -89,6 +95,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; import com.android.internal.util.ContrastColorUtil; +import junit.framework.Assert; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -218,8 +226,10 @@ public class NotificationTest { @Test public void allPendingIntents_recollectedAfterReusingBuilder() { - PendingIntent intent1 = PendingIntent.getActivity(mContext, 0, new Intent("test1"), PendingIntent.FLAG_MUTABLE_UNAUDITED); - PendingIntent intent2 = PendingIntent.getActivity(mContext, 0, new Intent("test2"), PendingIntent.FLAG_MUTABLE_UNAUDITED); + PendingIntent intent1 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + PendingIntent intent2 = PendingIntent.getActivity( + mContext, 0, new Intent("test2"), PendingIntent.FLAG_IMMUTABLE); Notification.Builder builder = new Notification.Builder(mContext, "channel"); builder.setContentIntent(intent1); @@ -669,30 +679,23 @@ public class NotificationTest { Notification notification = new Notification.Builder(mContext, "Channel").setStyle( style).build(); + int targetSize = mContext.getResources().getDimensionPixelSize( + ActivityManager.isLowRamDeviceStatic() + ? R.dimen.notification_person_icon_max_size_low_ram + : R.dimen.notification_person_icon_max_size); + Bitmap personIcon = style.getUser().getIcon().getBitmap(); - assertThat(personIcon.getWidth()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); - assertThat(personIcon.getHeight()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); + assertThat(personIcon.getWidth()).isEqualTo(targetSize); + assertThat(personIcon.getHeight()).isEqualTo(targetSize); Bitmap avatarIcon = style.getMessages().get(0).getSenderPerson().getIcon().getBitmap(); - assertThat(avatarIcon.getWidth()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); - assertThat(avatarIcon.getHeight()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); + assertThat(avatarIcon.getWidth()).isEqualTo(targetSize); + assertThat(avatarIcon.getHeight()).isEqualTo(targetSize); Bitmap historicAvatarIcon = style.getHistoricMessages().get( 0).getSenderPerson().getIcon().getBitmap(); - assertThat(historicAvatarIcon.getWidth()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); - assertThat(historicAvatarIcon.getHeight()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); + assertThat(historicAvatarIcon.getWidth()).isEqualTo(targetSize); + assertThat(historicAvatarIcon.getHeight()).isEqualTo(targetSize); } @Test @@ -780,7 +783,6 @@ public class NotificationTest { assertFalse(notification.isMediaNotification()); } - @Test public void validateColorizedPaletteForColor(int rawColor) { Notification.Colors cDay = new Notification.Colors(); Notification.Colors cNight = new Notification.Colors(); @@ -861,19 +863,22 @@ public class NotificationTest { Bundle fakeTypes = new Bundle(); fakeTypes.putParcelable(EXTRA_LARGE_ICON_BIG, new Bundle()); - style.restoreFromExtras(fakeTypes); // no crash, good } @Test public void testRestoreFromExtras_Messaging_invalidExtra_noCrash() { - Notification.Style style = new Notification.MessagingStyle(); + Notification.Style style = new Notification.MessagingStyle("test"); Bundle fakeTypes = new Bundle(); fakeTypes.putParcelable(EXTRA_MESSAGING_PERSON, new Bundle()); fakeTypes.putParcelable(EXTRA_CONVERSATION_ICON, new Bundle()); - style.restoreFromExtras(fakeTypes); + Notification n = new Notification.Builder(mContext, "test") + .setStyle(style) + .setExtras(fakeTypes) + .build(); + Notification.Builder.recoverBuilder(mContext, n); // no crash, good } @@ -885,22 +890,33 @@ public class NotificationTest { fakeTypes.putParcelable(EXTRA_MEDIA_SESSION, new Bundle()); fakeTypes.putParcelable(EXTRA_MEDIA_REMOTE_INTENT, new Bundle()); - style.restoreFromExtras(fakeTypes); + Notification n = new Notification.Builder(mContext, "test") + .setStyle(style) + .setExtras(fakeTypes) + .build(); + Notification.Builder.recoverBuilder(mContext, n); // no crash, good } @Test public void testRestoreFromExtras_Call_invalidExtra_noCrash() { - Notification.Style style = new CallStyle(); + PendingIntent intent1 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Notification.Style style = Notification.CallStyle.forIncomingCall( + new Person.Builder().setName("hi").build(), intent1, intent1); + Bundle fakeTypes = new Bundle(); fakeTypes.putParcelable(EXTRA_CALL_PERSON, new Bundle()); fakeTypes.putParcelable(EXTRA_ANSWER_INTENT, new Bundle()); fakeTypes.putParcelable(EXTRA_DECLINE_INTENT, new Bundle()); fakeTypes.putParcelable(EXTRA_HANG_UP_INTENT, new Bundle()); - style.restoreFromExtras(fakeTypes); - + Notification n = new Notification.Builder(mContext, "test") + .setStyle(style) + .setExtras(fakeTypes) + .build(); + Notification.Builder.recoverBuilder(mContext, n); // no crash, good } @@ -962,7 +978,11 @@ public class NotificationTest { fakeTypes.putParcelable(KEY_ON_READ, new Bundle()); fakeTypes.putParcelable(KEY_ON_REPLY, new Bundle()); fakeTypes.putParcelable(KEY_REMOTE_INPUT, new Bundle()); - Notification.CarExtender.UnreadConversation.getUnreadConversationFromBundle(fakeTypes); + + Notification n = new Notification.Builder(mContext, "test") + .setExtras(fakeTypes) + .build(); + Notification.CarExtender extender = new Notification.CarExtender(n); // no crash, good } @@ -980,6 +1000,493 @@ public class NotificationTest { // no crash, good } + + @Test + public void testDoesNotStripsExtenders() { + Notification.Builder nb = new Notification.Builder(mContext, "channel"); + nb.extend(new Notification.CarExtender().setColor(Color.RED)); + nb.extend(new Notification.TvExtender().setChannelId("different channel")); + nb.extend(new Notification.WearableExtender().setDismissalId("dismiss")); + Notification before = nb.build(); + Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before); + + assertTrue(before == after); + + Assert.assertEquals("different channel", + new Notification.TvExtender(before).getChannelId()); + Assert.assertEquals(Color.RED, new Notification.CarExtender(before).getColor()); + Assert.assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId()); + } + + @Test + public void testStyleChangeVisiblyDifferent_noStyles() { + Notification.Builder n1 = new Notification.Builder(mContext, "test"); + Notification.Builder n2 = new Notification.Builder(mContext, "test"); + + assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testStyleChangeVisiblyDifferent_noStyleToStyle() { + Notification.Builder n1 = new Notification.Builder(mContext, "test"); + Notification.Builder n2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle()); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testStyleChangeVisiblyDifferent_styleToNoStyle() { + Notification.Builder n2 = new Notification.Builder(mContext, "test"); + Notification.Builder n1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle()); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testStyleChangeVisiblyDifferent_changeStyle() { + Notification.Builder n1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.InboxStyle()); + Notification.Builder n2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle()); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testInboxTextChange() { + Notification.Builder nInbox1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.InboxStyle().addLine("a").addLine("b")); + Notification.Builder nInbox2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.InboxStyle().addLine("b").addLine("c")); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2)); + } + + @Test + public void testBigTextTextChange() { + Notification.Builder nBigText1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle().bigText("something")); + Notification.Builder nBigText2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle().bigText("else")); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2)); + } + + @Test + public void testBigPictureChange() { + Bitmap bitA = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + Bitmap bitB = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888); + + Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigPictureStyle().bigPicture(bitA)); + Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigPictureStyle().bigPicture(bitB)); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2)); + } + + @Test + public void testMessagingChange_text() { + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build())) + .addMessage(new Notification.MessagingStyle.Message( + "b", 100, new Person.Builder().setName("hi").build())) + ); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_data() { + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()) + .setData("text", mock(Uri.class)))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()))); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_sender() { + Person a = new Person.Builder().setName("A").build(); + Person b = new Person.Builder().setName("b").build(); + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_key() { + Person a = new Person.Builder().setName("hi").setKey("A").build(); + Person b = new Person.Builder().setName("hi").setKey("b").build(); + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_ignoreTimeChange() { + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 1000, new Person.Builder().setName("hi").build())) + ); + + assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testRemoteViews_nullChange() { + Notification.Builder n1 = new Notification.Builder(mContext, "test") + .setContent(mock(RemoteViews.class)); + Notification.Builder n2 = new Notification.Builder(mContext, "test"); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test"); + n2 = new Notification.Builder(mContext, "test") + .setContent(mock(RemoteViews.class)); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test") + .setCustomBigContentView(mock(RemoteViews.class)); + n2 = new Notification.Builder(mContext, "test"); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test"); + n2 = new Notification.Builder(mContext, "test") + .setCustomBigContentView(mock(RemoteViews.class)); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test"); + n2 = new Notification.Builder(mContext, "test"); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_layoutChange() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(189); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_layoutSame() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(234); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_sequenceChange() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + when(a.getSequenceNumber()).thenReturn(1); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(234); + when(b.getSequenceNumber()).thenReturn(2); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_sequenceSame() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + when(a.getSequenceNumber()).thenReturn(1); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(234); + when(b.getSequenceNumber()).thenReturn(1); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testActionsDifferent_null() { + Notification n1 = new Notification.Builder(mContext, "test") + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentSame() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentText() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) + .build(); + + assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentSpannables() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, + new SpannableStringBuilder().append("test1", + new StyleSpan(Typeface.BOLD), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE), + intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "test1", intent).build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentNumber() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) + .build(); + + assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentIntent() { + PendingIntent intent1 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + PendingIntent intent2 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsIgnoresRemoteInputs() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(new RemoteInput.Builder("a") + .setChoices(new CharSequence[] {"i", "m"}) + .build()) + .build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(new RemoteInput.Builder("a") + .setChoices(new CharSequence[] {"t", "m"}) + .build()) + .build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testFreeformRemoteInputActionPair_noRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + Notification notification = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .build()) + .build(); + Assert.assertNull(notification.findRemoteInputActionPair(false)); + } + + @Test + public void testFreeformRemoteInputActionPair_hasRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + RemoteInput remoteInput = new RemoteInput.Builder("a").build(); + + Notification.Action actionWithRemoteInput = + new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(remoteInput) + .addRemoteInput(remoteInput) + .build(); + + Notification.Action actionWithoutRemoteInput = + new Notification.Action.Builder(icon, "TEXT 2", intent) + .build(); + + Notification notification = new Notification.Builder(mContext, "test") + .addAction(actionWithoutRemoteInput) + .addAction(actionWithRemoteInput) + .build(); + + Pair<RemoteInput, Notification.Action> remoteInputActionPair = + notification.findRemoteInputActionPair(false); + + assertNotNull(remoteInputActionPair); + Assert.assertEquals(remoteInput, remoteInputActionPair.first); + Assert.assertEquals(actionWithRemoteInput, remoteInputActionPair.second); + } + + @Test + public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + Notification notification = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput( + new RemoteInput.Builder("a") + .setAllowFreeFormInput(false).build()) + .build()) + .build(); + Assert.assertNull(notification.findRemoteInputActionPair(true)); + } + + @Test + public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + RemoteInput remoteInput = + new RemoteInput.Builder("a").setAllowFreeFormInput(false).build(); + RemoteInput freeformRemoteInput = + new RemoteInput.Builder("b").setAllowFreeFormInput(true).build(); + + Notification.Action actionWithFreeformRemoteInput = + new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(remoteInput) + .addRemoteInput(freeformRemoteInput) + .build(); + + Notification.Action actionWithoutFreeformRemoteInput = + new Notification.Action.Builder(icon, "TEXT 2", intent) + .addRemoteInput(remoteInput) + .build(); + + Notification notification = new Notification.Builder(mContext, "test") + .addAction(actionWithoutFreeformRemoteInput) + .addAction(actionWithFreeformRemoteInput) + .build(); + + Pair<RemoteInput, Notification.Action> remoteInputActionPair = + notification.findRemoteInputActionPair(true); + + assertNotNull(remoteInputActionPair); + Assert.assertEquals(freeformRemoteInput, remoteInputActionPair.first); + Assert.assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second); + } + private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index f448cb3091e7..f370ebd94545 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -60,6 +60,8 @@ public class WindowOnBackInvokedDispatcherTest { private OnBackAnimationCallback mCallback1; @Mock private OnBackAnimationCallback mCallback2; + @Mock + private BackEvent mBackEvent; @Before public void setUp() throws Exception { @@ -85,14 +87,14 @@ public class WindowOnBackInvokedDispatcherTest { verify(mWindowSession, times(2)).setOnBackInvokedCallbackInfo( Mockito.eq(mWindow), captor.capture()); - captor.getAllValues().get(0).getCallback().onBackStarted(); + captor.getAllValues().get(0).getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback1).onBackStarted(); + verify(mCallback1).onBackStarted(mBackEvent); verifyZeroInteractions(mCallback2); - captor.getAllValues().get(1).getCallback().onBackStarted(); + captor.getAllValues().get(1).getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback2).onBackStarted(); + verify(mCallback2).onBackStarted(mBackEvent); verifyNoMoreInteractions(mCallback1); } @@ -110,9 +112,9 @@ public class WindowOnBackInvokedDispatcherTest { Mockito.eq(mWindow), captor.capture()); verifyNoMoreInteractions(mWindowSession); assertEquals(captor.getValue().getPriority(), OnBackInvokedDispatcher.PRIORITY_OVERLAY); - captor.getValue().getCallback().onBackStarted(); + captor.getValue().getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback1).onBackStarted(); + verify(mCallback1).onBackStarted(mBackEvent); } @Test @@ -148,8 +150,8 @@ public class WindowOnBackInvokedDispatcherTest { mDispatcher.registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_OVERLAY, mCallback2); verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), captor.capture()); - captor.getValue().getCallback().onBackStarted(); + captor.getValue().getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback2).onBackStarted(); + verify(mCallback2).onBackStarted(mBackEvent); } } diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java index d19f9f5ea58f..52feac5a585a 100644 --- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java +++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java @@ -47,6 +47,7 @@ import android.telephony.DataConnectionRealTimeInfo; import android.telephony.ModemActivityInfo; import android.telephony.ServiceState; import android.telephony.TelephonyManager; +import android.util.Log; import android.util.MutableInt; import android.util.SparseIntArray; import android.util.SparseLongArray; @@ -82,6 +83,7 @@ import java.util.function.IntConsumer; * com.android.frameworks.coretests/androidx.test.runner.AndroidJUnitRunner */ public class BatteryStatsNoteTest extends TestCase { + private static final String TAG = BatteryStatsNoteTest.class.getSimpleName(); private static final int UID = 10500; private static final int ISOLATED_APP_ID = Process.FIRST_ISOLATED_UID + 23; @@ -2031,6 +2033,115 @@ public class BatteryStatsNoteTest extends TestCase { noRadioProcFlags, lastProcStateChangeFlags.value); } + + + @SmallTest + public void testNoteMobileRadioPowerStateLocked() { + long curr; + boolean update; + final MockClock clocks = new MockClock(); // holds realtime and uptime in ms + final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); + bi.setOnBatteryInternal(true); + + // Note mobile radio is on. + curr = 1000L * (clocks.realtime = clocks.uptime = 1001); + bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr, + UID); + + // Note mobile radio is still on. + curr = 1000L * (clocks.realtime = clocks.uptime = 2001); + update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, + curr, UID); + assertFalse( + "noteMobileRadioPowerStateLocked should not request an update when the power " + + "state does not change from HIGH.", + update); + + // Note mobile radio is off. + curr = 1000L * (clocks.realtime = clocks.uptime = 3001); + update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, + curr, UID); + assertTrue( + "noteMobileRadioPowerStateLocked should request an update when the power state " + + "changes from HIGH to LOW.", + update); + + // Note mobile radio is still off. + curr = 1000L * (clocks.realtime = clocks.uptime = 4001); + update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, + curr, UID); + assertFalse( + "noteMobileRadioPowerStateLocked should not request an update when the power " + + "state does not change from LOW.", + update); + + // Note mobile radio is on. + curr = 1000L * (clocks.realtime = clocks.uptime = 5001); + update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, + curr, UID); + assertFalse( + "noteMobileRadioPowerStateLocked should not request an update when the power " + + "state changes from LOW to HIGH.", + update); + } + + @SmallTest + public void testNoteMobileRadioPowerStateLocked_rateLimited() { + long curr; + boolean update; + final MockClock clocks = new MockClock(); // holds realtime and uptime in ms + final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); + bi.setPowerProfile(mock(PowerProfile.class)); + + final int txLevelCount = CellSignalStrength.getNumSignalStrengthLevels(); + final ModemActivityInfo mai = new ModemActivityInfo(0L, 0L, 0L, new int[txLevelCount], 0L); + + final long rateLimit = bi.getMobileRadioPowerStateUpdateRateLimit(); + if (rateLimit < 0) { + Log.w(TAG, "Skipping testNoteMobileRadioPowerStateLocked_rateLimited, rateLimit = " + + rateLimit); + return; + } + bi.setOnBatteryInternal(true); + + // Note mobile radio is on. + curr = 1000L * (clocks.realtime = clocks.uptime = 1001); + bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr, + UID); + + clocks.realtime = clocks.uptime = 2001; + mai.setTimestamp(clocks.realtime); + bi.noteModemControllerActivity(mai, POWER_DATA_UNAVAILABLE, + clocks.realtime, clocks.uptime, mNetworkStatsManager); + + // Note mobile radio is off within the rate limit duration. + clocks.realtime = clocks.uptime = clocks.realtime + (long) (rateLimit * 0.7); + curr = 1000L * clocks.realtime; + update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, + curr, UID); + assertFalse( + "noteMobileRadioPowerStateLocked should not request an update when the power " + + "state so soon after a noteModemControllerActivity", + update); + + // Note mobile radio is on. + clocks.realtime = clocks.uptime = clocks.realtime + (long) (rateLimit * 0.7); + curr = 1000L * clocks.realtime; + bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr, + UID); + + // Note mobile radio is off much later + clocks.realtime = clocks.uptime = clocks.realtime + rateLimit; + curr = 1000L * clocks.realtime; + update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, + curr, UID); + assertTrue( + "noteMobileRadioPowerStateLocked should request an update when the power state " + + "changes from HIGH to LOW much later after a " + + "noteModemControllerActivity.", + update); + } + private void setFgState(int uid, boolean fgOn, MockBatteryStatsImpl bi) { // Note that noteUidProcessStateLocked uses ActivityManager process states. if (fgOn) { diff --git a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java index edeb5e9f4834..5ea4f069f026 100644 --- a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java +++ b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java @@ -114,6 +114,10 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { return getUidStatsLocked(uid).mOnBatteryScreenOffBackgroundTimeBase; } + public long getMobileRadioPowerStateUpdateRateLimit() { + return MOBILE_RADIO_POWER_STATE_UPDATE_FREQ_MS; + } + public MockBatteryStatsImpl setNetworkStats(NetworkStats networkStats) { mNetworkStats = networkStats; return this; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java index 00be5a6e3416..77284c4166bd 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -109,6 +109,12 @@ class SplitContainer { return (mSplitRule instanceof SplitPlaceholderRule); } + @NonNull + SplitInfo toSplitInfo() { + return new SplitInfo(mPrimaryContainer.toActivityStack(), + mSecondaryContainer.toActivityStack(), mSplitAttributes); + } + static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) { final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule; final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule) diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index bf7326a5b30e..1d513e444050 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -1422,6 +1422,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") void updateContainer(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { + if (!container.getTaskContainer().isVisible()) { + // Wait until the Task is visible to avoid unnecessary update when the Task is still in + // background. + return; + } if (launchPlaceholderIfNecessary(wct, container)) { // Placeholder was launched, the positions will be updated when the activity is added // to the secondary container. @@ -1643,16 +1648,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Notifies listeners about changes to split states if necessary. */ + @VisibleForTesting @GuardedBy("mLock") - private void updateCallbackIfNecessary() { - if (mEmbeddingCallback == null) { + void updateCallbackIfNecessary() { + if (mEmbeddingCallback == null || !readyToReportToClient()) { return; } - if (!allActivitiesCreated()) { - return; - } - List<SplitInfo> currentSplitStates = getActiveSplitStates(); - if (currentSplitStates == null || mLastReportedSplitStates.equals(currentSplitStates)) { + final List<SplitInfo> currentSplitStates = getActiveSplitStates(); + if (mLastReportedSplitStates.equals(currentSplitStates)) { return; } mLastReportedSplitStates.clear(); @@ -1661,48 +1664,27 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** - * @return a list of descriptors for currently active split states. If the value returned is - * null, that indicates that the active split states are in an intermediate state and should - * not be reported. + * Returns a list of descriptors for currently active split states. */ @GuardedBy("mLock") - @Nullable + @NonNull private List<SplitInfo> getActiveSplitStates() { - List<SplitInfo> splitStates = new ArrayList<>(); + final List<SplitInfo> splitStates = new ArrayList<>(); for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - final List<SplitContainer> splitContainers = mTaskContainers.valueAt(i) - .mSplitContainers; - for (SplitContainer container : splitContainers) { - if (container.getPrimaryContainer().isEmpty() - || container.getSecondaryContainer().isEmpty()) { - // We are in an intermediate state because either the split container is about - // to be removed or the primary or secondary container are about to receive an - // activity. - return null; - } - final ActivityStack primaryContainer = container.getPrimaryContainer() - .toActivityStack(); - final ActivityStack secondaryContainer = container.getSecondaryContainer() - .toActivityStack(); - final SplitInfo splitState = new SplitInfo(primaryContainer, secondaryContainer, - container.getSplitAttributes()); - splitStates.add(splitState); - } + mTaskContainers.valueAt(i).getSplitStates(splitStates); } return splitStates; } /** - * Checks if all activities that are registered with the containers have already appeared in - * the client. + * Whether we can now report the split states to the client. */ - private boolean allActivitiesCreated() { + @GuardedBy("mLock") + private boolean readyToReportToClient() { for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; - for (TaskFragmentContainer container : containers) { - if (!container.taskInfoActivityCountMatchesCreated()) { - return false; - } + if (mTaskContainers.valueAt(i).isInIntermediateState()) { + // If any Task is in an intermediate state, wait for the server update. + return false; } } return true; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 00943f2d53e1..231da0542e95 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -221,6 +221,24 @@ class TaskContainer { return mContainers.indexOf(child); } + /** Whether the Task is in an intermediate state waiting for the server update.*/ + boolean isInIntermediateState() { + for (TaskFragmentContainer container : mContainers) { + if (container.isInIntermediateState()) { + // We are in an intermediate state to wait for server update on this TaskFragment. + return true; + } + } + return false; + } + + /** Adds the descriptors of split states in this Task to {@code outSplitStates}. */ + void getSplitStates(@NonNull List<SplitInfo> outSplitStates) { + for (SplitContainer container : mSplitContainers) { + outSplitStates.add(container.toSplitInfo()); + } + } + /** * A wrapper class which contains the display ID and {@link Configuration} of a * {@link TaskContainer} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 18712aed1be6..71b884018bdb 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -166,16 +166,34 @@ class TaskFragmentContainer { return allActivities; } - /** - * Checks if the count of activities from the same process in task fragment info corresponds to - * the ones created and available on the client side. - */ - boolean taskInfoActivityCountMatchesCreated() { + /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/ + boolean isInIntermediateState() { if (mInfo == null) { - return false; + // Haven't received onTaskFragmentAppeared event. + return true; + } + if (mInfo.isEmpty()) { + // Empty TaskFragment will be removed or will have activity launched into it soon. + return true; + } + if (!mPendingAppearedActivities.isEmpty()) { + // Reparented activity hasn't appeared. + return true; } - return mPendingAppearedActivities.isEmpty() - && mInfo.getActivities().size() == collectNonFinishingActivities().size(); + // Check if there is any reported activity that is no longer alive. + for (IBinder token : mInfo.getActivities()) { + final Activity activity = mController.getActivity(token); + if (activity == null && !mTaskContainer.isVisible()) { + // Activity can be null if the activity is not attached to process yet. That can + // happen when the activity is started in background. + continue; + } + if (activity == null || activity.isFinishing()) { + // One of the reported activity is no longer alive, wait for the server update. + return true; + } + } + return false; } @NonNull diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index a40303150079..87d027899eb4 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -102,6 +102,7 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; /** * Test class for {@link SplitController}. @@ -132,6 +133,8 @@ public class SplitControllerTest { private SplitController mSplitController; private SplitPresenter mSplitPresenter; + private Consumer<List<SplitInfo>> mEmbeddingCallback; + private List<SplitInfo> mSplitInfos; private TransactionManager mTransactionManager; @Before @@ -141,9 +144,16 @@ public class SplitControllerTest { .getCurrentWindowLayoutInfo(anyInt(), any()); mSplitController = new SplitController(mWindowLayoutComponent); mSplitPresenter = mSplitController.mPresenter; + mSplitInfos = new ArrayList<>(); + mEmbeddingCallback = splitInfos -> { + mSplitInfos.clear(); + mSplitInfos.addAll(splitInfos); + }; + mSplitController.setSplitInfoCallback(mEmbeddingCallback); mTransactionManager = mSplitController.mTransactionManager; spyOn(mSplitController); spyOn(mSplitPresenter); + spyOn(mEmbeddingCallback); spyOn(mTransactionManager); doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean()); final Configuration activityConfig = new Configuration(); @@ -329,6 +339,30 @@ public class SplitControllerTest { } @Test + public void testUpdateContainer_skipIfTaskIsInvisible() { + final Activity r0 = createMockActivity(); + final Activity r1 = createMockActivity(); + addSplitTaskFragments(r0, r1); + final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); + final TaskFragmentContainer taskFragmentContainer = taskContainer.mContainers.get(0); + spyOn(taskContainer); + + // No update when the Task is invisible. + clearInvocations(mSplitPresenter); + doReturn(false).when(taskContainer).isVisible(); + mSplitController.updateContainer(mTransaction, taskFragmentContainer); + + verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any()); + + // Update the split when the Task is visible. + doReturn(true).when(taskContainer).isVisible(); + mSplitController.updateContainer(mTransaction, taskFragmentContainer); + + verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0), + taskFragmentContainer, mTransaction); + } + + @Test public void testOnStartActivityResultError() { final Intent intent = new Intent(); final TaskContainer taskContainer = createTestTaskContainer(); @@ -1162,14 +1196,69 @@ public class SplitControllerTest { new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); } + @Test + public void testSplitInfoCallback_reportSplit() { + final Activity r0 = createMockActivity(); + final Activity r1 = createMockActivity(); + addSplitTaskFragments(r0, r1); + + mSplitController.updateCallbackIfNecessary(); + assertEquals(1, mSplitInfos.size()); + final SplitInfo splitInfo = mSplitInfos.get(0); + assertEquals(1, splitInfo.getPrimaryActivityStack().getActivities().size()); + assertEquals(1, splitInfo.getSecondaryActivityStack().getActivities().size()); + assertEquals(r0, splitInfo.getPrimaryActivityStack().getActivities().get(0)); + assertEquals(r1, splitInfo.getSecondaryActivityStack().getActivities().get(0)); + } + + @Test + public void testSplitInfoCallback_reportSplitInMultipleTasks() { + final int taskId0 = 1; + final int taskId1 = 2; + final Activity r0 = createMockActivity(taskId0); + final Activity r1 = createMockActivity(taskId0); + final Activity r2 = createMockActivity(taskId1); + final Activity r3 = createMockActivity(taskId1); + addSplitTaskFragments(r0, r1); + addSplitTaskFragments(r2, r3); + + mSplitController.updateCallbackIfNecessary(); + assertEquals(2, mSplitInfos.size()); + } + + @Test + public void testSplitInfoCallback_doNotReportIfInIntermediateState() { + final Activity r0 = createMockActivity(); + final Activity r1 = createMockActivity(); + addSplitTaskFragments(r0, r1); + final TaskFragmentContainer tf0 = mSplitController.getContainerWithActivity(r0); + final TaskFragmentContainer tf1 = mSplitController.getContainerWithActivity(r1); + spyOn(tf0); + spyOn(tf1); + + // Do not report if activity has not appeared in the TaskFragmentContainer in split. + doReturn(true).when(tf0).isInIntermediateState(); + mSplitController.updateCallbackIfNecessary(); + verify(mEmbeddingCallback, never()).accept(any()); + + doReturn(false).when(tf0).isInIntermediateState(); + mSplitController.updateCallbackIfNecessary(); + verify(mEmbeddingCallback).accept(any()); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { + return createMockActivity(TASK_ID); + } + + /** Creates a mock activity in the organizer process. */ + private Activity createMockActivity(int taskId) { final Activity activity = mock(Activity.class); doReturn(mActivityResources).when(activity).getResources(); final IBinder activityToken = new Binder(); doReturn(activityToken).when(activity).getActivityToken(); doReturn(activity).when(mSplitController).getActivity(activityToken); - doReturn(TASK_ID).when(activity).getTaskId(); + doReturn(taskId).when(activity).getTaskId(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); return activity; @@ -1177,7 +1266,8 @@ public class SplitControllerTest { /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { - final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID); + final TaskFragmentContainer container = mSplitController.newContainer(activity, + activity.getTaskId()); setupTaskFragmentInfo(container, activity); return container; } @@ -1268,7 +1358,7 @@ public class SplitControllerTest { // We need to set those in case we are not respecting clear top. // TODO(b/231845476) we should always respect clearTop. - final int windowingMode = mSplitController.getTaskContainer(TASK_ID) + final int windowingMode = mSplitController.getTaskContainer(primaryContainer.getTaskId()) .getWindowingModeForSplitTaskFragment(TASK_BOUNDS); primaryContainer.setLastRequestedWindowingMode(windowingMode); secondaryContainer.setLastRequestedWindowingMode(windowingMode); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 35415d816d8b..d43c471fb8ae 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -334,6 +334,70 @@ public class TaskFragmentContainerTest { assertFalse(container.hasActivity(mActivity.getActivityToken())); } + @Test + public void testIsInIntermediateState() { + // True if no info set. + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + spyOn(taskContainer); + doReturn(true).when(taskContainer).isVisible(); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // True if empty info set. + final List<IBinder> activities = new ArrayList<>(); + doReturn(activities).when(mInfo).getActivities(); + doReturn(true).when(mInfo).isEmpty(); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // False if info is not empty. + doReturn(false).when(mInfo).isEmpty(); + container.setInfo(mTransaction, mInfo); + + assertFalse(container.isInIntermediateState()); + assertFalse(taskContainer.isInIntermediateState()); + + // True if there is pending appeared activity. + container.addPendingAppearedActivity(mActivity); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // True if the activity is finishing. + activities.add(mActivity.getActivityToken()); + doReturn(true).when(mActivity).isFinishing(); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // False if the activity is not finishing. + doReturn(false).when(mActivity).isFinishing(); + container.setInfo(mTransaction, mInfo); + + assertFalse(container.isInIntermediateState()); + assertFalse(taskContainer.isInIntermediateState()); + + // True if there is a token that can't find associated activity. + activities.clear(); + activities.add(new Binder()); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // False if there is a token that can't find associated activity when the Task is invisible. + doReturn(false).when(taskContainer).isVisible(); + + assertFalse(container.isInIntermediateState()); + assertFalse(taskContainer.isInIntermediateState()); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { final Activity activity = mock(Activity.class); diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index 0fdfed86b29e..17b1d1bdbff1 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"አስፋ"</string> <string name="minimize_button_text" msgid="271592547935841753">"አሳንስ"</string> <string name="close_button_text" msgid="2913281996024033299">"ዝጋ"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"ተመለስ"</string> + <string name="handle_text" msgid="1766582106752184456">"መያዣ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index 351e1928a260..2babc3ea5fcd 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"تكبير"</string> <string name="minimize_button_text" msgid="271592547935841753">"تصغير"</string> <string name="close_button_text" msgid="2913281996024033299">"إغلاق"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"رجوع"</string> + <string name="handle_text" msgid="1766582106752184456">"مقبض"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 765aaba4d23c..fe43cd69110f 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"সৰ্বাধিক মাত্ৰালৈ বঢ়াওক"</string> <string name="minimize_button_text" msgid="271592547935841753">"মিনিমাইজ কৰক"</string> <string name="close_button_text" msgid="2913281996024033299">"বন্ধ কৰক"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"উভতি যাওক"</string> + <string name="handle_text" msgid="1766582106752184456">"হেণ্ডেল"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index 0ec5db185e34..c22f5425048e 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Böyüdün"</string> <string name="minimize_button_text" msgid="271592547935841753">"Kiçildin"</string> <string name="close_button_text" msgid="2913281996024033299">"Bağlayın"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Geriyə"</string> + <string name="handle_text" msgid="1766582106752184456">"Hər kəsə açıq istifadəçi adı"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index bd4a079f54c6..cf5394b0ebd2 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Разгарнуць"</string> <string name="minimize_button_text" msgid="271592547935841753">"Згарнуць"</string> <string name="close_button_text" msgid="2913281996024033299">"Закрыць"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index a04a50da7089..c525f521c0e3 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Увеличаване"</string> <string name="minimize_button_text" msgid="271592547935841753">"Намаляване"</string> <string name="close_button_text" msgid="2913281996024033299">"Затваряне"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Манипулатор"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 2cc0ab321b4a..bec284d4dff8 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -84,6 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimiziranje"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimiziranje"</string> <string name="close_button_text" msgid="2913281996024033299">"Zatvaranje"</string> - <string name="back_button_text" msgid="1469718707134137085">"Natrag"</string> - <string name="handle_text" msgid="1766582106752184456">"Pokazivač"</string> + <string name="back_button_text" msgid="1469718707134137085">"Nazad"</string> + <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index a2badaf06989..c84bb4884068 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maximitza"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimitza"</string> <string name="close_button_text" msgid="2913281996024033299">"Tanca"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Enrere"</string> + <string name="handle_text" msgid="1766582106752184456">"Ansa"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index 084ea865f769..60c1f8588445 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimér"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimer"</string> <string name="close_button_text" msgid="2913281996024033299">"Luk"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Tilbage"</string> + <string name="handle_text" msgid="1766582106752184456">"Håndtag"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index 195c3355df2d..b57f0c857d26 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maximieren"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimieren"</string> <string name="close_button_text" msgid="2913281996024033299">"Schließen"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Zurück"</string> + <string name="handle_text" msgid="1766582106752184456">"Ziehpunkt"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index 0ad387bbd39c..aed03acf026c 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> <string name="close_button_text" msgid="2913281996024033299">"Cerrar"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string> + <string name="handle_text" msgid="1766582106752184456">"Controlador"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index 4b85a22a12aa..bddc2c3e61bc 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> <string name="close_button_text" msgid="2913281996024033299">"Cerrar"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string> + <string name="handle_text" msgid="1766582106752184456">"Controlador"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index cc9057815ca2..696a520d0b86 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimeeri"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimeeri"</string> <string name="close_button_text" msgid="2913281996024033299">"Sule"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Tagasi"</string> + <string name="handle_text" msgid="1766582106752184456">"Käepide"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index a1d0b5851f84..7550a091bd79 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"بزرگ کردن"</string> <string name="minimize_button_text" msgid="271592547935841753">"کوچک کردن"</string> <string name="close_button_text" msgid="2913281996024033299">"بستن"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"برگشتن"</string> + <string name="handle_text" msgid="1766582106752184456">"دستگیره"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index 5df1e04abb2a..a79adf8e686c 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Suurenna"</string> <string name="minimize_button_text" msgid="271592547935841753">"Pienennä"</string> <string name="close_button_text" msgid="2913281996024033299">"Sulje"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Takaisin"</string> + <string name="handle_text" msgid="1766582106752184456">"Kahva"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index c4b386aad3d1..865e2dcf4775 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Agrandir"</string> <string name="minimize_button_text" msgid="271592547935841753">"Réduire"</string> <string name="close_button_text" msgid="2913281996024033299">"Fermer"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Retour"</string> + <string name="handle_text" msgid="1766582106752184456">"Poignée"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index f5a106d3956d..47e6696dfe4c 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> <string name="close_button_text" msgid="2913281996024033299">"Pechar"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string> + <string name="handle_text" msgid="1766582106752184456">"Controlador"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index 95484d5fa1d5..2c643ded9dee 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"बड़ा करें"</string> <string name="minimize_button_text" msgid="271592547935841753">"विंडो छोटी करें"</string> <string name="close_button_text" msgid="2913281996024033299">"बंद करें"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"वापस जाएं"</string> + <string name="handle_text" msgid="1766582106752184456">"हैंडल"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index 5ae72eb27df2..d3bca3374e9d 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Ծավալել"</string> <string name="minimize_button_text" msgid="271592547935841753">"Ծալել"</string> <string name="close_button_text" msgid="2913281996024033299">"Փակել"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Հետ"</string> + <string name="handle_text" msgid="1766582106752184456">"Նշիչ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index d36a83f115ec..f157fcf6dbec 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimalkan"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimalkan"</string> <string name="close_button_text" msgid="2913281996024033299">"Tutup"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Kembali"</string> + <string name="handle_text" msgid="1766582106752184456">"Tuas"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index 31d483ed7c1a..ead1757ed4bb 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Stækka"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minnka"</string> <string name="close_button_text" msgid="2913281996024033299">"Loka"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Til baka"</string> + <string name="handle_text" msgid="1766582106752184456">"Handfang"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index 5c9425e92929..12c962d09ce1 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"הגדלה"</string> <string name="minimize_button_text" msgid="271592547935841753">"מזעור"</string> <string name="close_button_text" msgid="2913281996024033299">"סגירה"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"חזרה"</string> + <string name="handle_text" msgid="1766582106752184456">"נקודת אחיזה"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index 787ac3e1af00..b28436131fe9 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"მაქსიმალურად გაშლა"</string> <string name="minimize_button_text" msgid="271592547935841753">"ჩაკეცვა"</string> <string name="close_button_text" msgid="2913281996024033299">"დახურვა"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"უკან"</string> + <string name="handle_text" msgid="1766582106752184456">"იდენტიფიკატორი"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index 927f0d7a0dde..2d842b88996a 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Жаю"</string> <string name="minimize_button_text" msgid="271592547935841753">"Кішірейту"</string> <string name="close_button_text" msgid="2913281996024033299">"Жабу"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Артқа"</string> + <string name="handle_text" msgid="1766582106752184456">"Идентификатор"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index 955b2f88f314..3243a648ac0e 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"ពង្រីក"</string> <string name="minimize_button_text" msgid="271592547935841753">"បង្រួម"</string> <string name="close_button_text" msgid="2913281996024033299">"បិទ"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"ថយក្រោយ"</string> + <string name="handle_text" msgid="1766582106752184456">"ឈ្មោះអ្នកប្រើប្រាស់"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index a69e105876be..3a655b870df3 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"최대화"</string> <string name="minimize_button_text" msgid="271592547935841753">"최소화"</string> <string name="close_button_text" msgid="2913281996024033299">"닫기"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"뒤로"</string> + <string name="handle_text" msgid="1766582106752184456">"핸들"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index 15bde88c7afb..b800e3eb174f 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"ຂະຫຍາຍໃຫຍ່ສຸດ"</string> <string name="minimize_button_text" msgid="271592547935841753">"ຫຍໍ້ລົງ"</string> <string name="close_button_text" msgid="2913281996024033299">"ປິດ"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"ກັບຄືນ"</string> + <string name="handle_text" msgid="1766582106752184456">"ມືບັງຄັບ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index 275efa203a74..94339a46df6f 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Padidinti"</string> <string name="minimize_button_text" msgid="271592547935841753">"Sumažinti"</string> <string name="close_button_text" msgid="2913281996024033299">"Uždaryti"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Atgal"</string> + <string name="handle_text" msgid="1766582106752184456">"Rankenėlė"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index 3048630e2344..d28245360922 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimizēt"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimizēt"</string> <string name="close_button_text" msgid="2913281996024033299">"Aizvērt"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Atpakaļ"</string> + <string name="handle_text" msgid="1766582106752184456">"Turis"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index 9013828fb487..5db8d6d071f1 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"വലുതാക്കുക"</string> <string name="minimize_button_text" msgid="271592547935841753">"ചെറുതാക്കുക"</string> <string name="close_button_text" msgid="2913281996024033299">"അടയ്ക്കുക"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"മടങ്ങുക"</string> + <string name="handle_text" msgid="1766582106752184456">"ഹാൻഡിൽ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index dc884e9bf3dd..779cf5c870bc 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"मोठे करा"</string> <string name="minimize_button_text" msgid="271592547935841753">"लहान करा"</string> <string name="close_button_text" msgid="2913281996024033299">"बंद करा"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"मागे जा"</string> + <string name="handle_text" msgid="1766582106752184456">"हँडल"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index a2f24f2c948b..0ceee2d1c928 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimer"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimer"</string> <string name="close_button_text" msgid="2913281996024033299">"Lukk"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Tilbake"</string> + <string name="handle_text" msgid="1766582106752184456">"Håndtak"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index 6a70d8da05ed..7ba49e57111e 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"ठुलो बनाउनुहोस्"</string> <string name="minimize_button_text" msgid="271592547935841753">"मिनिमाइज गर्नुहोस्"</string> <string name="close_button_text" msgid="2913281996024033299">"बन्द गर्नुहोस्"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"पछाडि"</string> + <string name="handle_text" msgid="1766582106752184456">"ह्यान्डल"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index d1d7db80caf5..dd3ebe415218 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maximaliseren"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimaliseren"</string> <string name="close_button_text" msgid="2913281996024033299">"Sluiten"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Terug"</string> + <string name="handle_text" msgid="1766582106752184456">"Gebruikersnaam"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index d2a96d95db74..c3056ca0a511 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"ਵੱਡਾ ਕਰੋ"</string> <string name="minimize_button_text" msgid="271592547935841753">"ਛੋਟਾ ਕਰੋ"</string> <string name="close_button_text" msgid="2913281996024033299">"ਬੰਦ ਕਰੋ"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"ਪਿੱਛੇ"</string> + <string name="handle_text" msgid="1766582106752184456">"ਹੈਂਡਲ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index cc2b2c314a85..56a6fb1664c1 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksymalizuj"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimalizuj"</string> <string name="close_button_text" msgid="2913281996024033299">"Zamknij"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Wstecz"</string> + <string name="handle_text" msgid="1766582106752184456">"Uchwyt"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index 98a775e1e6b3..b96caf23a0a7 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Развернуть"</string> <string name="minimize_button_text" msgid="271592547935841753">"Свернуть"</string> <string name="close_button_text" msgid="2913281996024033299">"Закрыть"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index 57240618ef19..a1ec3b5d5bb2 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"විහිදන්න"</string> <string name="minimize_button_text" msgid="271592547935841753">"කුඩා කරන්න"</string> <string name="close_button_text" msgid="2913281996024033299">"වසන්න"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"ආපසු"</string> + <string name="handle_text" msgid="1766582106752184456">"හැඬලය"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index 12f022e7a1f6..2f995e5076f3 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimiraj"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimiraj"</string> <string name="close_button_text" msgid="2913281996024033299">"Zapri"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Nazaj"</string> + <string name="handle_text" msgid="1766582106752184456">"Ročica"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index cdd870689886..3d9bde4f8066 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimizo"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimizo"</string> <string name="close_button_text" msgid="2913281996024033299">"Mbyll"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Pas"</string> + <string name="handle_text" msgid="1766582106752184456">"Emërtimi"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index fef3792b35a2..b39fd04ebf3d 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Utöka"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimera"</string> <string name="close_button_text" msgid="2913281996024033299">"Stäng"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Tillbaka"</string> + <string name="handle_text" msgid="1766582106752184456">"Handtag"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 2d10e2039a11..f4d4ceec5a1f 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Panua"</string> <string name="minimize_button_text" msgid="271592547935841753">"Punguza"</string> <string name="close_button_text" msgid="2913281996024033299">"Funga"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Rudi nyuma"</string> + <string name="handle_text" msgid="1766582106752184456">"Ncha"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index 0eeeca78cb86..6d050c2ba26e 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"பெரிதாக்கும்"</string> <string name="minimize_button_text" msgid="271592547935841753">"சிறிதாக்கும்"</string> <string name="close_button_text" msgid="2913281996024033299">"மூடும்"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"பின்செல்லும்"</string> + <string name="handle_text" msgid="1766582106752184456">"ஹேண்டில்"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index 2b105bdb7963..025e2e6215e7 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Ekranı Kapla"</string> <string name="minimize_button_text" msgid="271592547935841753">"Küçült"</string> <string name="close_button_text" msgid="2913281996024033299">"Kapat"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Geri"</string> + <string name="handle_text" msgid="1766582106752184456">"Herkese açık kullanıcı adı"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index a0925318ac26..97bb68080117 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Збільшити"</string> <string name="minimize_button_text" msgid="271592547935841753">"Згорнути"</string> <string name="close_button_text" msgid="2913281996024033299">"Закрити"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index 0330125c3e6f..ce73bd586c71 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Yoyish"</string> <string name="minimize_button_text" msgid="271592547935841753">"Kichraytirish"</string> <string name="close_button_text" msgid="2913281996024033299">"Yopish"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Orqaga"</string> + <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index 6e4a7682854d..511db6fbc116 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Phóng to"</string> <string name="minimize_button_text" msgid="271592547935841753">"Thu nhỏ"</string> <string name="close_button_text" msgid="2913281996024033299">"Đóng"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Quay lại"</string> + <string name="handle_text" msgid="1766582106752184456">"Xử lý"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index df911ed55ab3..16cbf126eff8 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"最大化"</string> <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> <string name="close_button_text" msgid="2913281996024033299">"关闭"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"返回"</string> + <string name="handle_text" msgid="1766582106752184456">"处理"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index f48402264a73..59d87a0a9371 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -19,7 +19,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="pip_phone_close" msgid="5783752637260411309">"Vala"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Nweba"</string> - <string name="pip_phone_settings" msgid="5468987116750491918">"Izilungiselelo"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Amasethingi"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Faka ukuhlukanisa isikrini"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Imenyu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"U-<xliff:g id="NAME">%s</xliff:g> ungaphakathi kwesithombe esiphakathi kwesithombe"</string> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 0bc70857a113..3ee20ea95ee5 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -321,4 +321,21 @@ <!-- The smaller size of the dismiss target (shrinks when something is in the target). --> <dimen name="floating_dismiss_circle_small">120dp</dimen> + + <!-- The thickness of shadows of a window that has focus in DIP. --> + <dimen name="freeform_decor_shadow_focused_thickness">20dp</dimen> + + <!-- The thickness of shadows of a window that doesn't have focus in DIP. --> + <dimen name="freeform_decor_shadow_unfocused_thickness">5dp</dimen> + + <!-- Height of button (32dp) + 2 * margin (5dp each). --> + <dimen name="freeform_decor_caption_height">42dp</dimen> + + <!-- Width of buttons (64dp) + handle (128dp) + padding (24dp total). --> + <dimen name="freeform_decor_caption_width">216dp</dimen> + + <dimen name="freeform_resize_handle">30dp</dimen> + + <dimen name="freeform_resize_corner">44dp</dimen> + </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index 490975cce956..921861ae0913 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -303,6 +303,7 @@ class ActivityEmbeddingAnimationRunner { // 3. Animate the TaskFragment using Activity Change info (start/end bounds). // This is because the TaskFragment surface/change won't contain the Activity's before its // reparent. + Animation changeAnimation = null; for (TransitionInfo.Change change : info.getChanges()) { if (change.getMode() != TRANSIT_CHANGE || change.getStartAbsBounds().equals(change.getEndAbsBounds())) { @@ -325,8 +326,14 @@ class ActivityEmbeddingAnimationRunner { } } + // There are two animations in the array. The first one is for the start leash + // (snapshot), and the second one is for the end leash (TaskFragment). final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(change, boundsAnimationChange.getEndAbsBounds()); + // Keep track as we might need to add background color for the animation. + // Although there may be multiple change animation, record one of them is sufficient + // because the background color will be added to the root leash for the whole animation. + changeAnimation = animations[1]; // Create a screenshot based on change, but attach it to the top of the // boundsAnimationChange. @@ -345,6 +352,9 @@ class ActivityEmbeddingAnimationRunner { animations[1], boundsAnimationChange)); } + // If there is no corresponding open/close window with the change, we should show background + // color to cover the empty part of the screen. + boolean shouldShouldBackgroundColor = true; // Handle the other windows that don't have bounds change in the same transition. for (TransitionInfo.Change change : info.getChanges()) { if (handledChanges.contains(change)) { @@ -359,11 +369,20 @@ class ActivityEmbeddingAnimationRunner { animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change); } else if (Transitions.isClosingType(change.getMode())) { animation = mAnimationSpec.createChangeBoundsCloseAnimation(change); + shouldShouldBackgroundColor = false; } else { animation = mAnimationSpec.createChangeBoundsOpenAnimation(change); + shouldShouldBackgroundColor = false; } adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change)); } + + if (shouldShouldBackgroundColor && changeAnimation != null) { + // Change animation may leave part of the screen empty. Show background color to cover + // that. + changeAnimation.setShowBackdrop(true); + } + return adapters; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java index 58b23667dc18..2bb73692b457 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java @@ -158,7 +158,7 @@ class ActivityEmbeddingAnimationSpec { // The position should be 0-based as we will post translate in // ActivityEmbeddingAnimationAdapter#onAnimationUpdate final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0, - 0, 0); + startBounds.top - endBounds.top, 0); endTranslate.setDuration(CHANGE_ANIMATION_DURATION); endSet.addAnimation(endTranslate); // The end leash is resizing, we should update the window crop based on the clip rect. 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 938189fc8a88..cbcd9498fe55 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 @@ -75,13 +75,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private static final String TAG = "BackAnimationController"; private static final int SETTING_VALUE_OFF = 0; private static final int SETTING_VALUE_ON = 1; - private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP = - "persist.wm.debug.predictive_back_progress_threshold"; public static final boolean IS_ENABLED = SystemProperties.getInt("persist.wm.debug.predictive_back", - SETTING_VALUE_ON) != SETTING_VALUE_OFF; - private static final int PROGRESS_THRESHOLD = SystemProperties - .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); + SETTING_VALUE_ON) == SETTING_VALUE_ON; + /** Flag for U animation features */ + public static boolean IS_U_ANIMATION_ENABLED = + SystemProperties.getInt("persist.wm.debug.predictive_back_anim", + SETTING_VALUE_OFF) == SETTING_VALUE_ON; + /** Predictive back animation developer option */ private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); // TODO (b/241808055) Find a appropriate time to remove during refactor private static final boolean USE_TRANSITION = @@ -114,7 +115,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Nullable private IOnBackInvokedCallback mBackToLauncherCallback; private float mTriggerThreshold; - private float mProgressThreshold; private final Runnable mResetTransitionRunnable = () -> { finishAnimation(); mTransitionInProgress = false; @@ -125,7 +125,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private IBackNaviAnimationController mBackAnimationController; private BackAnimationAdaptor mBackAnimationAdaptor; - private boolean mWaitingAnimationStart; private final TouchTracker mTouchTracker = new TouchTracker(); private final CachingBackDispatcher mCachingBackDispatcher = new CachingBackDispatcher(); @@ -148,44 +147,6 @@ 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. */ @@ -212,15 +173,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont boolean consumed = false; if (mWaitingAnimation && mOnBackCallback != null) { if (mTriggerBack) { - final BackEvent backFinish = new BackEvent( - mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 1, - mTouchTracker.mSwipeEdge, mAnimationTarget); + final BackEvent backFinish = mTouchTracker.createProgressEvent(1); dispatchOnBackProgressed(mBackToLauncherCallback, backFinish); dispatchOnBackInvoked(mOnBackCallback); } else { - final BackEvent backFinish = new BackEvent( - mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 0, - mTouchTracker.mSwipeEdge, mAnimationTarget); + final BackEvent backFinish = mTouchTracker.createProgressEvent(0); dispatchOnBackProgressed(mBackToLauncherCallback, backFinish); dispatchOnBackCancelled(mOnBackCallback); } @@ -263,6 +220,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont shellInit.addInitCallback(this::onInit, this); } + @VisibleForTesting + void setEnableUAnimation(boolean enable) { + IS_U_ANIMATION_ENABLED = enable; + } + private void onInit() { setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler); mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION, @@ -404,7 +366,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } - mTouchTracker.update(touchX, touchY, swipeEdge); + mTouchTracker.update(touchX, touchY); if (keyAction == MotionEvent.ACTION_DOWN) { if (!mBackGestureStarted) { mShouldStartOnNextMoveEvent = true; @@ -414,7 +376,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // Let the animation initialized here to make sure the onPointerDownOutsideFocus // could be happened when ACTION_DOWN, it may change the current focus that we // would access it when startBackNavigation. - onGestureStarted(touchX, touchY); + onGestureStarted(touchX, touchY, swipeEdge); mShouldStartOnNextMoveEvent = false; } onMove(touchX, touchY, swipeEdge); @@ -428,14 +390,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - private void onGestureStarted(float touchX, float touchY) { + private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted); if (mBackGestureStarted || mBackNavigationInfo != null) { Log.e(TAG, "Animation is being initialized but is already started."); finishAnimation(); } - mTouchTracker.setGestureStartLocation(touchX, touchY); + mTouchTracker.setGestureStartLocation(touchX, touchY, swipeEdge); mBackGestureStarted = true; try { @@ -464,6 +426,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont displayTargetScreenshot(hardwareBuffer, backNavigationInfo.getTaskWindowConfiguration()); } + targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); mTransaction.apply(); } else if (dispatchToLauncher) { targetCallback = mBackToLauncherCallback; @@ -474,7 +437,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); } if (!USE_TRANSITION || !dispatchToLauncher) { - dispatchOnBackStarted(targetCallback); + dispatchOnBackStarted( + targetCallback, + mTouchTracker.createStartEvent( + mBackNavigationInfo.getDepartingAnimationTarget())); } } @@ -514,29 +480,15 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (!mBackGestureStarted || mBackNavigationInfo == null) { return; } - 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); - if (USE_TRANSITION) { - if (mBackAnimationController != null && mAnimationTarget != null) { - final BackEvent backEvent = new BackEvent( - touchX, touchY, progress, swipeEdge, mAnimationTarget); + final BackEvent backEvent = mTouchTracker.createProgressEvent(); + if (USE_TRANSITION && mBackAnimationController != null && mAnimationTarget != null) { dispatchOnBackProgressed(mBackToLauncherCallback, backEvent); - } - } else { + } else if (mEnableAnimations.get()) { int backType = mBackNavigationInfo.getType(); - RemoteAnimationTarget animationTarget = - mBackNavigationInfo.getDepartingAnimationTarget(); - - BackEvent backEvent = new BackEvent( - touchX, touchY, progress, swipeEdge, animationTarget); - IOnBackInvokedCallback targetCallback = null; + IOnBackInvokedCallback targetCallback; 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) { + } else { targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); } dispatchOnBackProgressed(targetCallback, backEvent); @@ -620,18 +572,21 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont || mBackNavigationInfo.getDepartingAnimationTarget() != null); } - private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) { + private void dispatchOnBackStarted(IOnBackInvokedCallback callback, + BackEvent backEvent) { if (callback == null) { return; } try { - callback.onBackStarted(); + if (shouldDispatchAnimation(callback)) { + callback.onBackStarted(backEvent); + } } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackStarted error: ", e); } } - private static void dispatchOnBackInvoked(IOnBackInvokedCallback callback) { + private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) { if (callback == null) { return; } @@ -642,29 +597,38 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - private static void dispatchOnBackCancelled(IOnBackInvokedCallback callback) { + private void dispatchOnBackCancelled(IOnBackInvokedCallback callback) { if (callback == null) { return; } try { - callback.onBackCancelled(); + if (shouldDispatchAnimation(callback)) { + callback.onBackCancelled(); + } } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackCancelled error: ", e); } } - private static void dispatchOnBackProgressed(IOnBackInvokedCallback callback, + private void dispatchOnBackProgressed(IOnBackInvokedCallback callback, BackEvent backEvent) { if (callback == null) { return; } try { - callback.onBackProgressed(backEvent); + if (shouldDispatchAnimation(callback)) { + callback.onBackProgressed(backEvent); + } } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackProgressed error: ", e); } } + private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) { + return (IS_U_ANIMATION_ENABLED || callback == mBackToLauncherCallback) + && mEnableAnimations.get(); + } + /** * Sets to true when the back gesture has passed the triggering threshold, false otherwise. */ @@ -673,10 +637,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } mTriggerBack = triggerBack; + mTouchTracker.setTriggerBack(triggerBack); } private void setSwipeThresholds(float triggerThreshold, float progressThreshold) { - mProgressThreshold = progressThreshold; + mTouchTracker.setProgressThreshold(progressThreshold); mTriggerThreshold = triggerThreshold; } @@ -686,6 +651,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont BackNavigationInfo backNavigationInfo = mBackNavigationInfo; boolean triggerBack = mTriggerBack; mBackNavigationInfo = null; + mAnimationTarget = null; mTriggerBack = false; mShouldStartOnNextMoveEvent = false; if (backNavigationInfo == null) { @@ -762,17 +728,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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); + dispatchOnBackStarted(mBackToLauncherCallback, + mTouchTracker.createStartEvent(mAnimationTarget)); + final BackEvent backInit = mTouchTracker.createProgressEvent(); if (!mCachingBackDispatcher.consume()) { dispatchOnBackProgressed(mBackToLauncherCallback, backInit); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java new file mode 100644 index 000000000000..ccfac65d6342 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java @@ -0,0 +1,119 @@ +/* + * 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.wm.shell.back; + +import android.os.SystemProperties; +import android.view.RemoteAnimationTarget; +import android.window.BackEvent; + +/** + * Helper class to record the touch location for gesture and generate back events. + */ +class TouchTracker { + private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP = + "persist.wm.debug.predictive_back_progress_threshold"; + private static final int PROGRESS_THRESHOLD = SystemProperties + .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); + private float mProgressThreshold; + /** + * Location of the latest touch event + */ + private float mLatestTouchX; + private float mLatestTouchY; + private boolean mTriggerBack; + + /** + * Location of the initial touch event of the back gesture. + */ + private float mInitTouchX; + private float mInitTouchY; + private float mStartThresholdX; + private int mSwipeEdge; + private boolean mCancelled; + + void update(float touchX, float touchY) { + /** + * If back was previously cancelled but the user has started swiping in the forward + * direction again, restart back. + */ + if (mCancelled && ((touchX > mLatestTouchX && mSwipeEdge == BackEvent.EDGE_LEFT) + || touchX < mLatestTouchX && mSwipeEdge == BackEvent.EDGE_RIGHT)) { + mCancelled = false; + mStartThresholdX = touchX; + } + mLatestTouchX = touchX; + mLatestTouchY = touchY; + } + + void setTriggerBack(boolean triggerBack) { + if (mTriggerBack != triggerBack && !triggerBack) { + mCancelled = true; + } + mTriggerBack = triggerBack; + } + + void setGestureStartLocation(float touchX, float touchY, int swipeEdge) { + mInitTouchX = touchX; + mInitTouchY = touchY; + mSwipeEdge = swipeEdge; + mStartThresholdX = mInitTouchX; + } + + void reset() { + mInitTouchX = 0; + mInitTouchY = 0; + mStartThresholdX = 0; + mCancelled = false; + mTriggerBack = false; + mSwipeEdge = BackEvent.EDGE_LEFT; + } + + BackEvent createStartEvent(RemoteAnimationTarget target) { + return new BackEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target); + } + + BackEvent createProgressEvent() { + float progressThreshold = PROGRESS_THRESHOLD >= 0 + ? PROGRESS_THRESHOLD : mProgressThreshold; + progressThreshold = progressThreshold == 0 ? 1 : progressThreshold; + float progress = 0; + // Progress is always 0 when back is cancelled and not restarted. + if (!mCancelled) { + // If back is committed, progress is the distance between the last and first touch + // point, divided by the max drag distance. Otherwise, it's the distance between + // the last touch point and the starting threshold, divided by max drag distance. + // The starting threshold is initially the first touch location, and updated to + // the location everytime back is restarted after being cancelled. + float startX = mTriggerBack ? mInitTouchX : mStartThresholdX; + float deltaX = Math.max( + mSwipeEdge == BackEvent.EDGE_LEFT + ? mLatestTouchX - startX + : startX - mLatestTouchX, + 0); + progress = Math.min(Math.max(deltaX / progressThreshold, 0), 1); + } + return createProgressEvent(progress); + } + + BackEvent createProgressEvent(float progress) { + return new BackEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null); + } + + public void setProgressThreshold(float progressThreshold) { + mProgressThreshold = progressThreshold; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java index d6803e8052c6..d3a9a672ec76 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java @@ -52,7 +52,7 @@ public class BubbleBadgeIconFactory extends BaseIconFactory { userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon); } Bitmap userBadgedBitmap = createIconBitmap( - userBadgedAppIcon, 1, BITMAP_GENERATION_MODE_WITH_SHADOW); + userBadgedAppIcon, 1, MODE_WITH_SHADOW); return createIconBitmap(userBadgedBitmap); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java index 5dab8a071f76..4ded3ea951e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java @@ -79,6 +79,6 @@ public class BubbleIconFactory extends BaseIconFactory { true /* shrinkNonAdaptiveIcons */, null /* outscale */, outScale); - return createIconBitmap(icon, outScale[0], BITMAP_GENERATION_MODE_WITH_SHADOW); + return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java index 44a467ffcf3d..cbd544cc4b86 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java @@ -18,9 +18,21 @@ package com.android.wm.shell.desktopmode; import com.android.wm.shell.common.annotations.ExternalThread; +import java.util.concurrent.Executor; + /** * Interface to interact with desktop mode feature in shell. */ @ExternalThread public interface DesktopMode { + + /** + * Adds a listener to find out about changes in the visibility of freeform tasks. + * + * @param listener the listener to add. + * @param callbackExecutor the executor to call the listener on. + */ + void addListener(DesktopModeTaskRepository.VisibleTasksListener listener, + Executor callbackExecutor); + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index b96facf4c46e..34ff6d814c8d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; @@ -60,6 +61,7 @@ import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; import java.util.Comparator; +import java.util.concurrent.Executor; /** * Handles windowing changes when desktop mode system setting changes @@ -132,6 +134,17 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll return new IDesktopModeImpl(this); } + /** + * Adds a listener to find out about changes in the visibility of freeform tasks. + * + * @param listener the listener to add. + * @param callbackExecutor the executor to call the listener on. + */ + public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener, + Executor callbackExecutor) { + mDesktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor); + } + @VisibleForTesting void updateDesktopModeActive(boolean active) { ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active); @@ -181,7 +194,18 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll /** * Show apps on desktop */ - WindowContainerTransaction showDesktopApps() { + void showDesktopApps() { + WindowContainerTransaction wct = bringDesktopAppsToFront(); + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */); + } else { + mShellTaskOrganizer.applyTransaction(wct); + } + } + + @NonNull + private WindowContainerTransaction bringDesktopAppsToFront() { ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks(); ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size()); ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>(); @@ -197,11 +221,6 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll for (RunningTaskInfo task : taskInfos) { wct.reorder(task.token, true); } - - if (!Transitions.ENABLE_SHELL_TRANSITIONS) { - mShellTaskOrganizer.applyTransaction(wct); - } - return wct; } @@ -237,17 +256,29 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { - - // Only do anything if we are in desktop mode and opening a task/app - if (!DesktopModeStatus.isActive(mContext) || request.getType() != TRANSIT_OPEN) { + // Only do anything if we are in desktop mode and opening a task/app in freeform + if (!DesktopModeStatus.isActive(mContext)) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "skip shell transition request: desktop mode not active"); + return null; + } + if (request.getType() != TRANSIT_OPEN) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "skip shell transition request: only supports TRANSIT_OPEN"); return null; } + if (request.getTriggerTask() == null + || request.getTriggerTask().getWindowingMode() != WINDOWING_MODE_FREEFORM) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "skip shell transition request: not freeform task"); + return null; + } + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "handle shell transition request: %s", request); WindowContainerTransaction wct = mTransitions.dispatchRequest(transition, request, this); if (wct == null) { wct = new WindowContainerTransaction(); } - wct.merge(showDesktopApps(), true /* transfer */); + wct.merge(bringDesktopAppsToFront(), true /* transfer */); wct.reorder(request.getTriggerTask().token, true /* onTop */); return wct; @@ -293,7 +324,14 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll */ @ExternalThread private final class DesktopModeImpl implements DesktopMode { - // Do nothing + + @Override + public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener, + Executor callbackExecutor) { + mMainExecutor.execute(() -> { + DesktopModeController.this.addListener(listener, callbackExecutor); + }); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 988601c0e8a8..c91d54a62ae6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -16,7 +16,9 @@ package com.android.wm.shell.desktopmode +import android.util.ArrayMap import android.util.ArraySet +import java.util.concurrent.Executor /** * Keeps track of task data related to desktop mode. @@ -30,20 +32,39 @@ class DesktopModeTaskRepository { * Task gets removed from this list when it vanishes. Or when desktop mode is turned off. */ private val activeTasks = ArraySet<Int>() - private val listeners = ArraySet<Listener>() + private val visibleTasks = ArraySet<Int>() + private val activeTasksListeners = ArraySet<ActiveTasksListener>() + // Track visible tasks separately because a task may be part of the desktop but not visible. + private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>() /** - * Add a [Listener] to be notified of updates to the repository. + * Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository. */ - fun addListener(listener: Listener) { - listeners.add(listener) + fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) { + activeTasksListeners.add(activeTasksListener) } /** - * Remove a previously registered [Listener] + * Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not. */ - fun removeListener(listener: Listener) { - listeners.remove(listener) + fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) { + visibleTasksListeners.put(visibleTasksListener, executor) + executor.execute( + Runnable { visibleTasksListener.onVisibilityChanged(visibleTasks.size > 0) }) + } + + /** + * Remove a previously registered [ActiveTasksListener] + */ + fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) { + activeTasksListeners.remove(activeTasksListener) + } + + /** + * Remove a previously registered [VisibleTasksListener] + */ + fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) { + visibleTasksListeners.remove(visibleTasksListener) } /** @@ -52,7 +73,7 @@ class DesktopModeTaskRepository { fun addActiveTask(taskId: Int) { val added = activeTasks.add(taskId) if (added) { - listeners.onEach { it.onActiveTasksChanged() } + activeTasksListeners.onEach { it.onActiveTasksChanged() } } } @@ -62,7 +83,7 @@ class DesktopModeTaskRepository { fun removeActiveTask(taskId: Int) { val removed = activeTasks.remove(taskId) if (removed) { - listeners.onEach { it.onActiveTasksChanged() } + activeTasksListeners.onEach { it.onActiveTasksChanged() } } } @@ -81,9 +102,43 @@ class DesktopModeTaskRepository { } /** - * Defines interface for classes that can listen to changes in repository state. + * Updates whether a freeform task with this id is visible or not and notifies listeners. + */ + fun updateVisibleFreeformTasks(taskId: Int, visible: Boolean) { + val prevCount: Int = visibleTasks.size + if (visible) { + visibleTasks.add(taskId) + } else { + visibleTasks.remove(taskId) + } + if (prevCount == 0 && visibleTasks.size == 1 || + prevCount > 0 && visibleTasks.size == 0) { + for ((listener, executor) in visibleTasksListeners) { + executor.execute( + Runnable { listener.onVisibilityChanged(visibleTasks.size > 0) }) + } + } + } + + /** + * Defines interface for classes that can listen to changes for active tasks in desktop mode. + */ + interface ActiveTasksListener { + /** + * Called when the active tasks change in desktop mode. + */ + @JvmDefault + fun onActiveTasksChanged() {} + } + + /** + * Defines interface for classes that can listen to changes for visible tasks in desktop mode. */ - interface Listener { - fun onActiveTasksChanged() + interface VisibleTasksListener { + /** + * Called when the desktop starts or stops showing freeform tasks. + */ + @JvmDefault + fun onVisibilityChanged(hasVisibleFreeformTasks: Boolean) {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index f82a34621262..eaa7158abbe5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -90,6 +90,8 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "Adding active freeform task: #%d", taskInfo.taskId); mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId)); + mDesktopModeTaskRepository.ifPresent( + it -> it.updateVisibleFreeformTasks(taskInfo.taskId, true)); } } @@ -103,6 +105,8 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "Removing active freeform task: #%d", taskInfo.taskId); mDesktopModeTaskRepository.ifPresent(it -> it.removeActiveTask(taskInfo.taskId)); + mDesktopModeTaskRepository.ifPresent( + it -> it.updateVisibleFreeformTasks(taskInfo.taskId, false)); } if (!Transitions.ENABLE_SHELL_TRANSITIONS) { @@ -124,6 +128,8 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { "Adding active freeform task: #%d", taskInfo.taskId); mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId)); } + mDesktopModeTaskRepository.ifPresent( + it -> it.updateVisibleFreeformTasks(taskInfo.taskId, taskInfo.isVisible)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index afb64c9eec41..43d3f36f1fe5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -60,7 +60,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, FloatingContentCoordinator.FloatingContent { public static final boolean ENABLE_FLING_TO_DISMISS_PIP = - SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", true); + SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", false); private static final String TAG = "PipMotionHelper"; private static final boolean DEBUG = false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl index b71cc32a0347..1a6c1d65db03 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl @@ -16,7 +16,7 @@ package com.android.wm.shell.recents; -import android.app.ActivityManager; +import android.app.ActivityManager.RunningTaskInfo; import com.android.wm.shell.recents.IRecentTasksListener; import com.android.wm.shell.util.GroupedRecentTaskInfo; @@ -44,5 +44,5 @@ interface IRecentTasks { /** * Gets the set of running tasks. */ - ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) = 4; + RunningTaskInfo[] getRunningTasks(int maxNum) = 4; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl index 59f72335678e..e8f58fe2bfad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl @@ -16,7 +16,7 @@ package com.android.wm.shell.recents; -import android.app.ActivityManager; +import android.app.ActivityManager.RunningTaskInfo; /** * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks. @@ -31,10 +31,10 @@ oneway interface IRecentTasksListener { /** * Called when a running task appears. */ - void onRunningTaskAppeared(in ActivityManager.RunningTaskInfo taskInfo); + void onRunningTaskAppeared(in RunningTaskInfo taskInfo); /** * Called when a running task vanishes. */ - void onRunningTaskVanished(in ActivityManager.RunningTaskInfo taskInfo); -}
\ No newline at end of file + void onRunningTaskVanished(in RunningTaskInfo taskInfo); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 08f3db65e62f..f9172ba183de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -68,7 +68,7 @@ import java.util.function.Consumer; * Manages the recent task list from the system, caching it as necessary. */ public class RecentTasksController implements TaskStackListenerCallback, - RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.Listener { + RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener { private static final String TAG = RecentTasksController.class.getSimpleName(); private final Context mContext; @@ -147,7 +147,7 @@ public class RecentTasksController implements TaskStackListenerCallback, this::createExternalInterface, this); mShellCommandHandler.addDumpCallback(this::dump, this); mTaskStackListener.addListener(this); - mDesktopModeTaskRepository.ifPresent(it -> it.addListener(this)); + mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this)); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index 8cee4f1dc8fb..6ce981e25f5e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -432,7 +432,8 @@ public class SplashscreenContentDrawer { final ShapeIconFactory factory = new ShapeIconFactory( SplashscreenContentDrawer.this.mContext, scaledIconDpi, mFinalIconSize); - final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(iconDrawable); + final Bitmap bitmap = factory.createScaledBitmap(iconDrawable, + BaseIconFactory.MODE_DEFAULT); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); createIconDrawable(new BitmapDrawable(bitmap), true, mHighResIconProvider.mLoadInDetail); 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 9c2c2fa8598a..af79386caf9c 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 @@ -619,12 +619,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // Animation length is already expected to be scaled. va.overrideDurationScale(1.0f); va.setDuration(anim.computeDurationHint()); - va.addUpdateListener(animation -> { + final ValueAnimator.AnimatorUpdateListener updateListener = animation -> { final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix, position, cornerRadius, clipRect); - }); + }; + va.addUpdateListener(updateListener); final Runnable finisher = () -> { applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix, @@ -637,20 +638,30 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { }); }; va.addListener(new AnimatorListenerAdapter() { + // It is possible for the end/cancel to be called more than once, which may cause + // issues if the animating surface has already been released. Track the finished + // state here to skip duplicate callbacks. See b/252872225. private boolean mFinished = false; @Override public void onAnimationEnd(Animator animation) { - if (mFinished) return; - mFinished = true; - finisher.run(); + onFinish(); } @Override public void onAnimationCancel(Animator animation) { + onFinish(); + } + + private void onFinish() { if (mFinished) return; mFinished = true; finisher.run(); + // The update listener can continue to be called after the animation has ended if + // end() is called manually again before the finisher removes the animation. + // Remove it manually here to prevent animating a released surface. + // See b/252872225. + va.removeUpdateListener(updateListener); } }); animations.add(va); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java index e90389764af3..f209521b1da4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java @@ -33,6 +33,8 @@ public class SplitBounds implements Parcelable { // This class is orientation-agnostic, so we compute both for later use public final float topTaskPercent; public final float leftTaskPercent; + public final float dividerWidthPercent; + public final float dividerHeightPercent; /** * If {@code true}, that means at the time of creation of this object, the * split-screened apps were vertically stacked. This is useful in scenarios like @@ -62,8 +64,12 @@ public class SplitBounds implements Parcelable { appsStackedVertically = false; } - leftTaskPercent = this.leftTopBounds.width() / (float) rightBottomBounds.right; - topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom; + float totalWidth = rightBottomBounds.right - leftTopBounds.left; + float totalHeight = rightBottomBounds.bottom - leftTopBounds.top; + leftTaskPercent = leftTopBounds.width() / totalWidth; + topTaskPercent = leftTopBounds.height() / totalHeight; + dividerWidthPercent = visualDividerBounds.width() / totalWidth; + dividerHeightPercent = visualDividerBounds.height() / totalHeight; } public SplitBounds(Parcel parcel) { @@ -75,6 +81,8 @@ public class SplitBounds implements Parcelable { appsStackedVertically = parcel.readBoolean(); leftTopTaskId = parcel.readInt(); rightBottomTaskId = parcel.readInt(); + dividerWidthPercent = parcel.readInt(); + dividerHeightPercent = parcel.readInt(); } @Override @@ -87,6 +95,8 @@ public class SplitBounds implements Parcelable { parcel.writeBoolean(appsStackedVertically); parcel.writeInt(leftTopTaskId); parcel.writeInt(rightBottomTaskId); + parcel.writeFloat(dividerWidthPercent); + parcel.writeFloat(dividerHeightPercent); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 87700ee4fb50..9d61c14e1435 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -21,7 +21,6 @@ import android.app.WindowConfiguration; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; -import android.graphics.Rect; import android.graphics.drawable.VectorDrawable; import android.os.Handler; import android.view.Choreographer; @@ -43,22 +42,6 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus; * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't. */ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { - // The thickness of shadows of a window that has focus in DIP. - private static final int DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP = 20; - // The thickness of shadows of a window that doesn't have focus in DIP. - private static final int DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP = 5; - - // Height of button (32dp) + 2 * margin (5dp each) - private static final int DECOR_CAPTION_HEIGHT_IN_DIP = 42; - // Width of buttons (64dp) + handle (128dp) + padding (24dp total) - private static final int DECOR_CAPTION_WIDTH_IN_DIP = 216; - private static final int RESIZE_HANDLE_IN_DIP = 30; - private static final int RESIZE_CORNER_IN_DIP = 44; - - private static final Rect EMPTY_OUTSET = new Rect(); - private static final Rect RESIZE_HANDLE_OUTSET = new Rect( - RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP); - private final Handler mHandler; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; @@ -69,6 +52,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL private DragResizeInputListener mDragResizeListener; + private RelayoutParams mRelayoutParams = new RelayoutParams(); private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult = new WindowDecoration.RelayoutResult<>(); @@ -114,19 +98,32 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL void relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - final int shadowRadiusDp = taskInfo.isFocused - ? DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP : DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP; - final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode() - == WindowConfiguration.WINDOWING_MODE_FREEFORM; - final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable; - final Rect outset = isDragResizeable ? RESIZE_HANDLE_OUTSET : EMPTY_OUTSET; + final int shadowRadiusID = taskInfo.isFocused + ? R.dimen.freeform_decor_shadow_focused_thickness + : R.dimen.freeform_decor_shadow_unfocused_thickness; + final boolean isFreeform = + taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; + final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); - relayout(taskInfo, R.layout.caption_window_decoration, oldRootView, - DECOR_CAPTION_HEIGHT_IN_DIP, DECOR_CAPTION_WIDTH_IN_DIP, outset, shadowRadiusDp, - startT, finishT, wct, mResult); + + int outsetLeftId = R.dimen.freeform_resize_handle; + int outsetTopId = R.dimen.freeform_resize_handle; + int outsetRightId = R.dimen.freeform_resize_handle; + int outsetBottomId = R.dimen.freeform_resize_handle; + + mRelayoutParams.reset(); + mRelayoutParams.mRunningTaskInfo = taskInfo; + mRelayoutParams.mLayoutResId = R.layout.caption_window_decoration; + mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height; + mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width; + mRelayoutParams.mShadowRadiusId = shadowRadiusID; + if (isDragResizeable) { + mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId); + } + relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); mTaskOrganizer.applyTransaction(wct); @@ -167,10 +164,12 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL } int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()).getScaledTouchSlop(); - + int resize_handle = mResult.mRootView.getResources() + .getDimensionPixelSize(R.dimen.freeform_resize_handle); + int resize_corner = mResult.mRootView.getResources() + .getDimensionPixelSize(R.dimen.freeform_resize_corner); mDragResizeListener.setGeometry( - mResult.mWidth, mResult.mHeight, (int) (mResult.mDensity * RESIZE_HANDLE_IN_DIP), - (int) (mResult.mDensity * RESIZE_CORNER_IN_DIP), touchSlop); + mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index bf863ea2c7ab..b314163802ca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -19,11 +19,11 @@ package com.android.wm.shell.windowdecor; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; -import android.util.DisplayMetrics; import android.view.Display; import android.view.InsetsState; import android.view.LayoutInflater; @@ -91,7 +91,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> SurfaceControl mTaskBackgroundSurface; SurfaceControl mCaptionContainerSurface; - private CaptionWindowManager mCaptionWindowManager; + private WindowlessWindowManager mCaptionWindowManager; private SurfaceControlViewHost mViewHost; private final Rect mCaptionInsetsRect = new Rect(); @@ -142,15 +142,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> */ abstract void relayout(RunningTaskInfo taskInfo); - void relayout(RunningTaskInfo taskInfo, int layoutResId, T rootView, float captionHeightDp, - float captionWidthDp, Rect outsetsDp, float shadowRadiusDp, - SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - WindowContainerTransaction wct, RelayoutResult<T> outResult) { + void relayout(RelayoutParams params, SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView, + RelayoutResult<T> outResult) { outResult.reset(); final Configuration oldTaskConfig = mTaskInfo.getConfiguration(); - if (taskInfo != null) { - mTaskInfo = taskInfo; + if (params.mRunningTaskInfo != null) { + mTaskInfo = params.mRunningTaskInfo; } if (!mTaskInfo.isVisible) { @@ -159,7 +158,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return; } - if (rootView == null && layoutResId == 0) { + if (rootView == null && params.mLayoutResId == 0) { throw new IllegalArgumentException("layoutResId and rootView can't both be invalid."); } @@ -176,15 +175,15 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return; } mDecorWindowContext = mContext.createConfigurationContext(taskConfig); - if (layoutResId != 0) { - outResult.mRootView = - (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null); + if (params.mLayoutResId != 0) { + outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) + .inflate(params.mLayoutResId, null); } } if (outResult.mRootView == null) { - outResult.mRootView = - (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null); + outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) + .inflate(params.mLayoutResId , null); } // DecorationContainerSurface @@ -200,18 +199,19 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); - outResult.mDensity = taskConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; - final int decorContainerOffsetX = -(int) (outsetsDp.left * outResult.mDensity); - final int decorContainerOffsetY = -(int) (outsetsDp.top * outResult.mDensity); + final Resources resources = mDecorWindowContext.getResources(); + final int decorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId); + final int decorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId); outResult.mWidth = taskBounds.width() - + (int) (outsetsDp.right * outResult.mDensity) + + loadDimensionPixelSize(resources, params.mOutsetRightId) - decorContainerOffsetX; outResult.mHeight = taskBounds.height() - + (int) (outsetsDp.bottom * outResult.mDensity) + + loadDimensionPixelSize(resources, params.mOutsetBottomId) - decorContainerOffsetY; startT.setPosition( mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY) - .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight) + .setWindowCrop(mDecorationContainerSurface, + outResult.mWidth, outResult.mHeight) // TODO(b/244455401): Change the z-order when it's better organized .setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1) .show(mDecorationContainerSurface); @@ -226,12 +226,13 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .build(); } - float shadowRadius = outResult.mDensity * shadowRadiusDp; + float shadowRadius = loadDimension(resources, params.mShadowRadiusId); int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor(); mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f; mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f; mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f; - startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height()) + startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), + taskBounds.height()) .setShadowRadius(mTaskBackgroundSurface, shadowRadius) .setColor(mTaskBackgroundSurface, mTmpColor) // TODO(b/244455401): Change the z-order when it's better organized @@ -248,8 +249,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .build(); } - final int captionHeight = (int) Math.ceil(captionHeightDp * outResult.mDensity); - final int captionWidth = (int) Math.ceil(captionWidthDp * outResult.mDensity); + final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); + final int captionWidth = loadDimensionPixelSize(resources, params.mCaptionWidthId); //Prevent caption from going offscreen if task is too high up final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2; @@ -264,8 +265,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> if (mCaptionWindowManager == null) { // Put caption under a container surface because ViewRootImpl sets the destination frame // of windowless window layers and BLASTBufferQueue#update() doesn't support offset. - mCaptionWindowManager = new CaptionWindowManager( - mTaskInfo.getConfiguration(), mCaptionContainerSurface); + mCaptionWindowManager = new WindowlessWindowManager( + mTaskInfo.getConfiguration(), mCaptionContainerSurface, + null /* hostInputToken */); } // Caption view @@ -289,8 +291,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> // Caption insets mCaptionInsetsRect.set(taskBounds); - mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight - captionYPos; - wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, CAPTION_INSETS_TYPES); + mCaptionInsetsRect.bottom = + mCaptionInsetsRect.top + captionHeight - captionYPos; + wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, + CAPTION_INSETS_TYPES); } else { startT.hide(mCaptionContainerSurface); } @@ -365,34 +369,67 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> releaseViews(); } + private static int loadDimensionPixelSize(Resources resources, int resourceId) { + if (resourceId == Resources.ID_NULL) { + return 0; + } + return resources.getDimensionPixelSize(resourceId); + } + + private static float loadDimension(Resources resources, int resourceId) { + if (resourceId == Resources.ID_NULL) { + return 0; + } + return resources.getDimension(resourceId); + } + + static class RelayoutParams{ + RunningTaskInfo mRunningTaskInfo; + int mLayoutResId; + int mCaptionHeightId; + int mCaptionWidthId; + int mShadowRadiusId; + + int mOutsetTopId; + int mOutsetBottomId; + int mOutsetLeftId; + int mOutsetRightId; + + void setOutsets(int leftId, int topId, int rightId, int bottomId) { + mOutsetLeftId = leftId; + mOutsetTopId = topId; + mOutsetRightId = rightId; + mOutsetBottomId = bottomId; + } + + void reset() { + mLayoutResId = Resources.ID_NULL; + mCaptionHeightId = Resources.ID_NULL; + mCaptionWidthId = Resources.ID_NULL; + mShadowRadiusId = Resources.ID_NULL; + + mOutsetTopId = Resources.ID_NULL; + mOutsetBottomId = Resources.ID_NULL; + mOutsetLeftId = Resources.ID_NULL; + mOutsetRightId = Resources.ID_NULL; + } + } + static class RelayoutResult<T extends View & TaskFocusStateConsumer> { int mWidth; int mHeight; - float mDensity; T mRootView; void reset() { mWidth = 0; mHeight = 0; - mDensity = 0; mRootView = null; } } - private static class CaptionWindowManager extends WindowlessWindowManager { - CaptionWindowManager(Configuration config, SurfaceControl rootSurface) { - super(config, rootSurface, null /* hostInputToken */); - } - - @Override - public void setConfiguration(Configuration configuration) { - super.setConfiguration(configuration); - } - } - interface SurfaceControlViewHostFactory { default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { return new SurfaceControlViewHost(c, d, wmm); } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml new file mode 100644 index 000000000000..8949a75d1a15 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml @@ -0,0 +1,27 @@ +<?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. +--> +<resources> + <!-- Resources used in WindowDecorationTests --> + <dimen name="test_freeform_decor_caption_height">32dp</dimen> + <dimen name="test_freeform_decor_caption_width">216dp</dimen> + <dimen name="test_window_decor_left_outset">10dp</dimen> + <dimen name="test_window_decor_top_outset">20dp</dimen> + <dimen name="test_window_decor_right_outset">30dp</dimen> + <dimen name="test_window_decor_bottom_outset">40dp</dimen> + <dimen name="test_window_decor_shadow_radius">5dp</dimen> + <dimen name="test_window_decor_resize_handle">10dp</dimen> +</resources>
\ No newline at end of file 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 077e9ca2e88c..2e328b0736dd 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 @@ -127,6 +127,7 @@ public class BackAnimationControllerTest extends ShellTestCase { mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction, mActivityTaskManager, mContext, mContentResolver); + mController.setEnableUAnimation(true); mShellInit.init(); mEventTime = 0; mShellExecutor.flushAll(); @@ -245,10 +246,10 @@ public class BackAnimationControllerTest extends ShellTestCase { // 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, atLeastOnce()).onBackProgressed(backEventCaptor.capture()); + verify(mIOnBackInvokedCallback).onBackStarted(backEventCaptor.capture()); assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget()); + verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackEvent.class)); // Check that back invocation is dispatched. mController.setTriggerBack(true); // Fake trigger back @@ -276,11 +277,11 @@ public class BackAnimationControllerTest extends ShellTestCase { triggerBackGesture(); - verify(appCallback, never()).onBackStarted(); + verify(appCallback, never()).onBackStarted(any(BackEvent.class)); verify(appCallback, never()).onBackProgressed(backEventCaptor.capture()); verify(appCallback, times(1)).onBackInvoked(); - verify(mIOnBackInvokedCallback, never()).onBackStarted(); + verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackEvent.class)); verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture()); verify(mIOnBackInvokedCallback, never()).onBackInvoked(); } @@ -313,7 +314,7 @@ public class BackAnimationControllerTest extends ShellTestCase { doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); - verify(mIOnBackInvokedCallback).onBackStarted(); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); } @Test @@ -332,7 +333,7 @@ public class BackAnimationControllerTest extends ShellTestCase { doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); - verify(mIOnBackInvokedCallback).onBackStarted(); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); } @@ -348,7 +349,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // 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(); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); // Check that back invocation is dispatched. mController.setTriggerBack(true); // Fake trigger back diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java new file mode 100644 index 000000000000..3aefc3f03a8a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java @@ -0,0 +1,136 @@ +/* + * 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.wm.shell.back; + +import static org.junit.Assert.assertEquals; + +import android.window.BackEvent; + +import org.junit.Before; +import org.junit.Test; + +public class TouchTrackerTest { + private static final float FAKE_THRESHOLD = 400; + private static final float INITIAL_X_LEFT_EDGE = 5; + private static final float INITIAL_X_RIGHT_EDGE = FAKE_THRESHOLD - INITIAL_X_LEFT_EDGE; + private TouchTracker mTouchTracker; + + @Before + public void setUp() throws Exception { + mTouchTracker = new TouchTracker(); + mTouchTracker.setProgressThreshold(FAKE_THRESHOLD); + } + + @Test + public void generatesProgress_onStart() { + mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT); + BackEvent event = mTouchTracker.createStartEvent(null); + assertEquals(event.getProgress(), 0f, 0f); + } + + @Test + public void generatesProgress_leftEdge() { + mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT); + float touchX = 10; + + // Pre-commit + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); + + // Post-commit + touchX += 100; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); + + // Cancel + touchX -= 10; + mTouchTracker.setTriggerBack(false); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Cancel more + touchX -= 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restart + touchX += 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restarted, but pre-commit + float restartX = touchX; + touchX += 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - restartX) / FAKE_THRESHOLD, 0f); + + // Restarted, post-commit + touchX += 10; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); + } + + @Test + public void generatesProgress_rightEdge() { + mTouchTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0, BackEvent.EDGE_RIGHT); + float touchX = INITIAL_X_RIGHT_EDGE - 10; // Fake right edge + + // Pre-commit + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); + + // Post-commit + touchX -= 100; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); + + // Cancel + touchX += 10; + mTouchTracker.setTriggerBack(false); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Cancel more + touchX += 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restart + touchX -= 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restarted, but pre-commit + float restartX = touchX; + touchX -= 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (restartX - touchX) / FAKE_THRESHOLD, 0f); + + // Restarted, post-commit + touchX -= 10; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); + } + + private float getProgress() { + return mTouchTracker.createProgressEvent().getProgress(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java index c850a3b3b780..79b520c734c8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java @@ -20,6 +20,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; @@ -35,10 +37,12 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.testing.AndroidTestingRunner; import android.window.DisplayAreaInfo; +import android.window.TransitionRequestInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import android.window.WindowContainerTransaction.Change; @@ -243,6 +247,44 @@ public class DesktopModeControllerTest extends ShellTestCase { assertThat(op2.getContainer()).isEqualTo(token2.binder()); } + @Test + public void testHandleTransitionRequest_desktopModeNotActive_returnsNull() { + when(DesktopModeStatus.isActive(any())).thenReturn(false); + WindowContainerTransaction wct = mController.handleRequest( + new Binder(), + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + assertThat(wct).isNull(); + } + + @Test + public void testHandleTransitionRequest_notTransitOpen_returnsNull() { + WindowContainerTransaction wct = mController.handleRequest( + new Binder(), + new TransitionRequestInfo(TRANSIT_TO_FRONT, null /* trigger */, null /* remote */)); + assertThat(wct).isNull(); + } + + @Test + public void testHandleTransitionRequest_notFreeform_returnsNull() { + ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo(); + trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + WindowContainerTransaction wct = mController.handleRequest( + new Binder(), + new TransitionRequestInfo(TRANSIT_TO_FRONT, trigger, null /* remote */)); + assertThat(wct).isNull(); + } + + @Test + public void testHandleTransitionRequest_returnsWct() { + ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo(); + trigger.token = new MockToken().mToken; + trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + WindowContainerTransaction wct = mController.handleRequest( + mock(IBinder.class), + new TransitionRequestInfo(TRANSIT_OPEN, trigger, null /* remote */)); + assertThat(wct).isNotNull(); + } + private static class MockToken { private final WindowContainerToken mToken; private final IBinder mBinder; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 9b28d11f6a9d..aaa5c8a35acb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.desktopmode import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -38,7 +39,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun addActiveTask_listenerNotifiedAndTaskIsActive() { val listener = TestListener() - repo.addListener(listener) + repo.addActiveTaskListener(listener) repo.addActiveTask(1) assertThat(listener.activeTaskChangedCalls).isEqualTo(1) @@ -48,7 +49,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun addActiveTask_sameTaskDoesNotNotify() { val listener = TestListener() - repo.addListener(listener) + repo.addActiveTaskListener(listener) repo.addActiveTask(1) repo.addActiveTask(1) @@ -58,7 +59,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun addActiveTask_multipleTasksAddedNotifiesForEach() { val listener = TestListener() - repo.addListener(listener) + repo.addActiveTaskListener(listener) repo.addActiveTask(1) repo.addActiveTask(2) @@ -68,7 +69,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun removeActiveTask_listenerNotifiedAndTaskNotActive() { val listener = TestListener() - repo.addListener(listener) + repo.addActiveTaskListener(listener) repo.addActiveTask(1) repo.removeActiveTask(1) @@ -80,7 +81,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun removeActiveTask_removeNotExistingTaskDoesNotNotify() { val listener = TestListener() - repo.addListener(listener) + repo.addActiveTaskListener(listener) repo.removeActiveTask(99) assertThat(listener.activeTaskChangedCalls).isEqualTo(0) } @@ -90,10 +91,69 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { assertThat(repo.isActiveTask(99)).isFalse() } - class TestListener : DesktopModeTaskRepository.Listener { + @Test + fun addListener_notifiesVisibleFreeformTask() { + repo.updateVisibleFreeformTasks(1, true) + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + executor.flushAll() + + assertThat(listener.hasVisibleFreeformTasks).isTrue() + assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(1) + } + + @Test + fun updateVisibleFreeformTasks_addVisibleTasksNotifiesListener() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + repo.updateVisibleFreeformTasks(1, true) + repo.updateVisibleFreeformTasks(2, true) + executor.flushAll() + + assertThat(listener.hasVisibleFreeformTasks).isTrue() + // Equal to 2 because adding the listener notifies the current state + assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2) + } + + @Test + fun updateVisibleFreeformTasks_removeVisibleTasksNotifiesListener() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + repo.updateVisibleFreeformTasks(1, true) + repo.updateVisibleFreeformTasks(2, true) + executor.flushAll() + + assertThat(listener.hasVisibleFreeformTasks).isTrue() + repo.updateVisibleFreeformTasks(1, false) + executor.flushAll() + + // Equal to 2 because adding the listener notifies the current state + assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2) + + repo.updateVisibleFreeformTasks(2, false) + executor.flushAll() + + assertThat(listener.hasVisibleFreeformTasks).isFalse() + assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(3) + } + + class TestListener : DesktopModeTaskRepository.ActiveTasksListener { var activeTaskChangedCalls = 0 override fun onActiveTasksChanged() { activeTaskChangedCalls++ } } + + class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener { + var hasVisibleFreeformTasks = false + var visibleFreeformTaskChangedCalls = 0 + + override fun onVisibilityChanged(hasVisibleTasks: Boolean) { + hasVisibleFreeformTasks = hasVisibleTasks + visibleFreeformTaskChangedCalls++ + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index fa62b9c00fc7..4d37e5dbc4dc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -50,11 +50,13 @@ import android.view.WindowManager.LayoutParams; import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.tests.R; import org.junit.Before; import org.junit.Test; @@ -76,13 +78,9 @@ import java.util.function.Supplier; @SmallTest @RunWith(AndroidTestingRunner.class) public class WindowDecorationTests extends ShellTestCase { - private static final int CAPTION_HEIGHT_DP = 32; - private static final int CAPTION_WIDTH_DP = 216; - private static final int SHADOW_RADIUS_DP = 5; private static final Rect TASK_BOUNDS = new Rect(100, 300, 400, 400); private static final Point TASK_POSITION_IN_PARENT = new Point(40, 60); - private final Rect mOutsetsDp = new Rect(); private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult = new WindowDecoration.RelayoutResult<>(); @@ -104,6 +102,7 @@ public class WindowDecorationTests extends ShellTestCase { private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>(); private SurfaceControl.Transaction mMockSurfaceControlStartT; private SurfaceControl.Transaction mMockSurfaceControlFinishT; + private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams(); @Before public void setUp() { @@ -147,7 +146,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mOutsetsDp.set(10, 20, 30, 40); + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -197,8 +200,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mOutsetsDp.set(10, 20, 30, 40); - + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -226,16 +232,17 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockSurfaceControlStartT).show(captionContainerSurface); verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any()); + verify(mMockSurfaceControlViewHost) .setView(same(mMockView), argThat(lp -> lp.height == 64 - && lp.width == 300 + && lp.width == 432 && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0)); if (ViewRootImpl.CAPTION_ON_SHELL) { verify(mMockView).setTaskFocusState(true); verify(mMockWindowContainerTransaction) .addRectInsetsProvider(taskInfo.token, - new Rect(100, 300, 400, 364), + new Rect(100, 300, 400, 332), new int[] { InsetsState.ITYPE_CAPTION_BAR }); } @@ -248,7 +255,6 @@ public class WindowDecorationTests extends ShellTestCase { assertEquals(380, mRelayoutResult.mWidth); assertEquals(220, mRelayoutResult.mHeight); - assertEquals(2, mRelayoutResult.mDensity, 0.f); } @Test @@ -287,7 +293,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mOutsetsDp.set(10, 20, 30, 40); + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -358,7 +368,8 @@ public class WindowDecorationTests extends ShellTestCase { private TestWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) { - return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, + return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(), + mMockDisplayController, mMockShellTaskOrganizer, taskInfo, testSurface, new MockObjectSupplier<>(mMockSurfaceControlBuilders, () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))), @@ -410,9 +421,13 @@ public class WindowDecorationTests extends ShellTestCase { @Override void relayout(ActivityManager.RunningTaskInfo taskInfo) { - relayout(null /* taskInfo */, 0 /* layoutResId */, mMockView, CAPTION_HEIGHT_DP, - CAPTION_WIDTH_DP, mOutsetsDp, SHADOW_RADIUS_DP, mMockSurfaceControlStartT, - mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mRelayoutResult); + mRelayoutParams.mLayoutResId = 0; + mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height; + mRelayoutParams.mCaptionWidthId = R.dimen.test_freeform_decor_caption_width; + mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius; + + relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, + mMockWindowContainerTransaction, mMockView, mRelayoutResult); } } } diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java index 2fe7b1668d08..262f5f198c22 100644 --- a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java @@ -100,16 +100,12 @@ public final class BluetoothMidiDevice { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + Log.d(TAG, "onConnectionStateChange() status: " + status + ", newState: " + newState); String intentAction; if (newState == BluetoothProfile.STATE_CONNECTED) { Log.d(TAG, "Connected to GATT server."); Log.d(TAG, "Attempting to start service discovery:" + mBluetoothGatt.discoverServices()); - if (!mBluetoothGatt.requestMtu(MAX_PACKET_SIZE)) { - Log.e(TAG, "request mtu failed"); - mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE); - mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE); - } } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { Log.i(TAG, "Disconnected from GATT server."); close(); @@ -118,6 +114,7 @@ public final class BluetoothMidiDevice { @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { + Log.d(TAG, "onServicesDiscovered() status: " + status); if (status == BluetoothGatt.GATT_SUCCESS) { BluetoothGattService service = gatt.getService(MIDI_SERVICE); if (service != null) { @@ -137,9 +134,14 @@ public final class BluetoothMidiDevice { // Specification says to read the characteristic first and then // switch to receiving notifications mBluetoothGatt.readCharacteristic(characteristic); - } - openBluetoothDevice(mBluetoothDevice); + // Request higher MTU size + if (!gatt.requestMtu(MAX_PACKET_SIZE)) { + Log.e(TAG, "request mtu failed"); + mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE); + mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE); + } + } } } else { Log.e(TAG, "onServicesDiscovered received: " + status); @@ -235,13 +237,13 @@ public final class BluetoothMidiDevice { System.arraycopy(buffer, 0, mCachedBuffer, 0, count); if (DEBUG) { - logByteArray("Sent ", mCharacteristic.getValue(), 0, - mCharacteristic.getValue().length); + logByteArray("Sent ", mCachedBuffer, 0, mCachedBuffer.length); } - if (mBluetoothGatt.writeCharacteristic(mCharacteristic, mCachedBuffer, - mCharacteristic.getWriteType()) != BluetoothGatt.GATT_SUCCESS) { - Log.w(TAG, "could not write characteristic to Bluetooth GATT"); + int result = mBluetoothGatt.writeCharacteristic(mCharacteristic, mCachedBuffer, + mCharacteristic.getWriteType()); + if (result != BluetoothGatt.GATT_SUCCESS) { + Log.w(TAG, "could not write characteristic to Bluetooth GATT. result: " + result); return false; } @@ -254,6 +256,10 @@ public final class BluetoothMidiDevice { mBluetoothDevice = device; mService = service; + // Set a small default packet size in case there is an issue with configuring MTUs. + mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE); + mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE); + mBluetoothGatt = mBluetoothDevice.connectGatt(context, false, mGattCallback); mContext = context; diff --git a/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml b/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml index 18b8ca9b4dd6..77dfbeee9bbf 100644 --- a/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml +++ b/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml @@ -16,7 +16,7 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="app_label" msgid="4470785958457506021">"隨附裝置管理員"</string> + <string name="app_label" msgid="4470785958457506021">"隨附裝置管理工具"</string> <string name="confirmation_title" msgid="3785000297483688997">"允許「<xliff:g id="APP_NAME">%1$s</xliff:g>」<strong></strong>存取「<xliff:g id="DEVICE_NAME">%2$s</xliff:g>」<strong></strong>"</string> <string name="profile_name_watch" msgid="576290739483672360">"手錶"</string> <string name="chooser_title" msgid="2262294130493605839">"選擇要讓「<xliff:g id="APP_NAME">%2$s</xliff:g>」<strong></strong>管理的<xliff:g id="PROFILE_NAME">%1$s</xliff:g>"</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 5662ce6bd808..6bc1160a8d0a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -356,7 +356,7 @@ public class CachedBluetoothDeviceManager { * @return {@code true}, if the device should pair automatically; Otherwise, return * {@code false}. */ - public synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) { + private synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) { boolean isOngoingSetMemberPair = mOngoingSetMemberPair != null; int bondState = device.getBondState(); if (isOngoingSetMemberPair || bondState != BluetoothDevice.BOND_NONE @@ -365,10 +365,44 @@ public class CachedBluetoothDeviceManager { + " , device.getBondState: " + bondState); return false; } + return true; + } - Log.d(TAG, "Bond " + device.getName() + " by CSIP"); + /** + * Called when we found a set member of a group. The function will check the {@code groupId} if + * it exists and the bond state of the device is BOND_NONE, and if there isn't any ongoing pair + * , and then pair the device automatically. + * + * @param device The found device + * @param groupId The group id of the found device + */ + public synchronized void pairDeviceByCsip(BluetoothDevice device, int groupId) { + if (!shouldPairByCsip(device, groupId)) { + return; + } + Log.d(TAG, "Bond " + device.getAnonymizedAddress() + " by CSIP"); mOngoingSetMemberPair = device; - return true; + syncConfigFromMainDevice(device, groupId); + device.createBond(BluetoothDevice.TRANSPORT_LE); + } + + private void syncConfigFromMainDevice(BluetoothDevice device, int groupId) { + if (!isOngoingPairByCsip(device)) { + return; + } + CachedBluetoothDevice memberDevice = findDevice(device); + CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(memberDevice); + if (mainDevice == null) { + mainDevice = mCsipDeviceManager.getCachedDevice(groupId); + } + + if (mainDevice == null || mainDevice.equals(memberDevice)) { + Log.d(TAG, "no mainDevice"); + return; + } + + // The memberDevice set PhonebookAccessPermission + device.setPhonebookAccessPermission(mainDevice.getDevice().getPhonebookAccessPermission()); } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index d5de3f0525a0..20a6cd8e09ce 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -101,7 +101,14 @@ public class CsipDeviceManager { return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID; } - private CachedBluetoothDevice getCachedDevice(int groupId) { + /** + * To find the device with {@code groupId}. + * + * @param groupId The group id + * @return if we could find a device with this {@code groupId} return this device. Otherwise, + * return null. + */ + public CachedBluetoothDevice getCachedDevice(int groupId) { log("getCachedDevice: groupId: " + groupId); for (int i = mCachedDevices.size() - 1; i >= 0; i--) { CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java index 39977dfa5c80..f969a63dc663 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java @@ -41,19 +41,19 @@ public class MobileNetworkTypeIconsTest { MobileNetworkTypeIcon icon = MobileNetworkTypeIcons.getNetworkTypeIcon(TelephonyIcons.FOUR_G); - assertThat(icon.getName()).isEqualTo(TelephonyIcons.H_PLUS.name); + assertThat(icon.getName()).isEqualTo(TelephonyIcons.FOUR_G.name); assertThat(icon.getIconResId()).isEqualTo(TelephonyIcons.ICON_4G); } @Test public void getNetworkTypeIcon_unknown_returnsUnknown() { - SignalIcon.MobileIconGroup unknownGroup = - new SignalIcon.MobileIconGroup("testUnknownNameHere", 45, 6); + SignalIcon.MobileIconGroup unknownGroup = new SignalIcon.MobileIconGroup( + "testUnknownNameHere", /* dataContentDesc= */ 45, /* dataType= */ 6); MobileNetworkTypeIcon icon = MobileNetworkTypeIcons.getNetworkTypeIcon(unknownGroup); assertThat(icon.getName()).isEqualTo("testUnknownNameHere"); - assertThat(icon.getIconResId()).isEqualTo(45); - assertThat(icon.getContentDescriptionResId()).isEqualTo(6); + assertThat(icon.getIconResId()).isEqualTo(6); + assertThat(icon.getContentDescriptionResId()).isEqualTo(45); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java index 62552f914459..61802a87361c 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java @@ -582,4 +582,24 @@ public class CachedBluetoothDeviceManagerTest { assertThat(mCachedDeviceManager.isSubDevice(mDevice2)).isTrue(); assertThat(mCachedDeviceManager.isSubDevice(mDevice3)).isFalse(); } + + @Test + public void pairDeviceByCsip_device2AndCapGroup1_device2StartsPairing() { + doReturn(CAP_GROUP1).when(mCsipSetCoordinatorProfile).getGroupUuidMapByDevice(mDevice1); + when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + when(mDevice1.getPhonebookAccessPermission()).thenReturn(BluetoothDevice.ACCESS_ALLOWED); + CachedBluetoothDevice cachedDevice1 = mCachedDeviceManager.addDevice(mDevice1); + assertThat(cachedDevice1).isNotNull(); + when(mDevice2.getBondState()).thenReturn(BluetoothDevice.BOND_NONE); + CachedBluetoothDevice cachedDevice2 = mCachedDeviceManager.addDevice(mDevice2); + assertThat(cachedDevice2).isNotNull(); + + int groupId = CAP_GROUP1.keySet().stream().findFirst().orElse( + BluetoothCsipSetCoordinator.GROUP_ID_INVALID); + assertThat(groupId).isNotEqualTo(BluetoothCsipSetCoordinator.GROUP_ID_INVALID); + mCachedDeviceManager.pairDeviceByCsip(mDevice2, groupId); + + verify(mDevice2).setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); + verify(mDevice2).createBond(BluetoothDevice.TRANSPORT_LE); + } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java index 808ea9ede9dc..6d375ac215a4 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -549,7 +549,7 @@ public class SettingsHelper { try { IActivityManager am = ActivityManager.getService(); - Configuration config = am.getConfiguration(); + final Configuration config = new Configuration(); config.setLocales(merged); // indicate this isn't some passing default - the user wants this remembered config.userSetLocale = true; diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index fd7554f11873..528af2ec2528 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -376,9 +376,11 @@ final class SettingsState { Setting newSetting = new Setting(name, oldSetting.getValue(), null, oldSetting.getPackageName(), oldSetting.getTag(), false, oldSetting.getId()); - mSettings.put(name, newSetting); - updateMemoryUsagePerPackageLocked(newSetting.getPackageName(), oldValue, + int newSize = getNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), oldValue, newSetting.getValue(), oldDefaultValue, newSetting.getDefaultValue()); + checkNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), newSize); + mSettings.put(name, newSetting); + updateMemoryUsagePerPackageLocked(newSetting.getPackageName(), newSize); scheduleWriteIfNeededLocked(); } } @@ -410,6 +412,12 @@ final class SettingsState { Setting oldState = mSettings.get(name); String oldValue = (oldState != null) ? oldState.value : null; String oldDefaultValue = (oldState != null) ? oldState.defaultValue : null; + String newDefaultValue = makeDefault ? value : oldDefaultValue; + + int newSize = getNewMemoryUsagePerPackageLocked(packageName, oldValue, value, + oldDefaultValue, newDefaultValue); + checkNewMemoryUsagePerPackageLocked(packageName, newSize); + Setting newState; if (oldState != null) { @@ -430,8 +438,7 @@ final class SettingsState { addHistoricalOperationLocked(HISTORICAL_OPERATION_UPDATE, newState); - updateMemoryUsagePerPackageLocked(packageName, oldValue, value, - oldDefaultValue, newState.getDefaultValue()); + updateMemoryUsagePerPackageLocked(packageName, newSize); scheduleWriteIfNeededLocked(); @@ -552,13 +559,14 @@ final class SettingsState { } Setting oldState = mSettings.remove(name); + int newSize = getNewMemoryUsagePerPackageLocked(oldState.packageName, oldState.value, + null, oldState.defaultValue, null); FrameworkStatsLog.write(FrameworkStatsLog.SETTING_CHANGED, name, /* value= */ "", /* newValue= */ "", oldState.value, /* tag */ "", false, getUserIdFromKey(mKey), FrameworkStatsLog.SETTING_CHANGED__REASON__DELETED); - updateMemoryUsagePerPackageLocked(oldState.packageName, oldState.value, - null, oldState.defaultValue, null); + updateMemoryUsagePerPackageLocked(oldState.packageName, newSize); addHistoricalOperationLocked(HISTORICAL_OPERATION_DELETE, oldState); @@ -579,16 +587,18 @@ final class SettingsState { Setting oldSetting = new Setting(setting); String oldValue = setting.getValue(); String oldDefaultValue = setting.getDefaultValue(); + String newValue = oldDefaultValue; + String newDefaultValue = oldDefaultValue; + + int newSize = getNewMemoryUsagePerPackageLocked(setting.packageName, oldValue, + newValue, oldDefaultValue, newDefaultValue); + checkNewMemoryUsagePerPackageLocked(setting.packageName, newSize); if (!setting.reset()) { return false; } - String newValue = setting.getValue(); - String newDefaultValue = setting.getDefaultValue(); - - updateMemoryUsagePerPackageLocked(setting.packageName, oldValue, - newValue, oldDefaultValue, newDefaultValue); + updateMemoryUsagePerPackageLocked(setting.packageName, newSize); addHistoricalOperationLocked(HISTORICAL_OPERATION_RESET, oldSetting); @@ -696,38 +706,49 @@ final class SettingsState { } @GuardedBy("mLock") - private void updateMemoryUsagePerPackageLocked(String packageName, String oldValue, - String newValue, String oldDefaultValue, String newDefaultValue) { - if (mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED) { - return; - } + private boolean isExemptFromMemoryUsageCap(String packageName) { + return mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED + || SYSTEM_PACKAGE_NAME.equals(packageName); + } - if (SYSTEM_PACKAGE_NAME.equals(packageName)) { + @GuardedBy("mLock") + private void checkNewMemoryUsagePerPackageLocked(String packageName, int newSize) + throws IllegalStateException { + if (isExemptFromMemoryUsageCap(packageName)) { return; } + if (newSize > mMaxBytesPerAppPackage) { + throw new IllegalStateException("You are adding too many system settings. " + + "You should stop using system settings for app specific data" + + " package: " + packageName); + } + } + @GuardedBy("mLock") + private int getNewMemoryUsagePerPackageLocked(String packageName, String oldValue, + String newValue, String oldDefaultValue, String newDefaultValue) { + if (isExemptFromMemoryUsageCap(packageName)) { + return 0; + } + final Integer currentSize = mPackageToMemoryUsage.get(packageName); final int oldValueSize = (oldValue != null) ? oldValue.length() : 0; final int newValueSize = (newValue != null) ? newValue.length() : 0; final int oldDefaultValueSize = (oldDefaultValue != null) ? oldDefaultValue.length() : 0; final int newDefaultValueSize = (newDefaultValue != null) ? newDefaultValue.length() : 0; final int deltaSize = newValueSize + newDefaultValueSize - oldValueSize - oldDefaultValueSize; + return Math.max((currentSize != null) ? currentSize + deltaSize : deltaSize, 0); + } - Integer currentSize = mPackageToMemoryUsage.get(packageName); - final int newSize = Math.max((currentSize != null) - ? currentSize + deltaSize : deltaSize, 0); - - if (newSize > mMaxBytesPerAppPackage) { - throw new IllegalStateException("You are adding too many system settings. " - + "You should stop using system settings for app specific data" - + " package: " + packageName); + @GuardedBy("mLock") + private void updateMemoryUsagePerPackageLocked(String packageName, int newSize) { + if (isExemptFromMemoryUsageCap(packageName)) { + return; } - if (DEBUG) { Slog.i(LOG_TAG, "Settings for package: " + packageName + " size: " + newSize + " bytes."); } - mPackageToMemoryUsage.put(packageName, newSize); } diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java index 69eb7133f46f..66b809aeae30 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java @@ -20,6 +20,8 @@ import android.test.AndroidTestCase; import android.util.TypedXmlSerializer; import android.util.Xml; +import com.google.common.base.Strings; + import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -276,4 +278,40 @@ public class SettingsStateTest extends AndroidTestCase { settingsState.setVersionLocked(SettingsState.SETTINGS_VERSION_NEW_ENCODING); return settingsState; } + + public void testInsertSetting_memoryUsage() { + SettingsState settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + // No exception should be thrown when there is no cap + settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001), + null, false, "p1"); + settingsState.deleteSettingLocked(SETTING_NAME); + + settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper()); + // System package doesn't have memory usage limit + settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001), + null, false, SYSTEM_PACKAGE); + settingsState.deleteSettingLocked(SETTING_NAME); + + // Should not throw if usage is under the cap + settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 19999), + null, false, "p1"); + settingsState.deleteSettingLocked(SETTING_NAME); + try { + settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001), + null, false, "p1"); + fail("Should throw because it exceeded per package memory usage"); + } catch (IllegalStateException ex) { + assertTrue(ex.getMessage().contains("p1")); + } + try { + settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001), + null, false, "p1"); + fail("Should throw because it exceeded per package memory usage"); + } catch (IllegalStateException ex) { + assertTrue(ex.getMessage().contains("p1")); + } + assertTrue(settingsState.getSettingLocked(SETTING_NAME).isNull()); + } } 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 23cee4d0972d..fdfad2bc2fa1 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -25,7 +25,6 @@ import android.graphics.Rect import android.os.Looper import android.util.Log import android.util.MathUtils -import android.view.GhostView import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT @@ -86,6 +85,9 @@ constructor( */ val sourceIdentity: Any + /** The CUJ associated to this controller. */ + val cuj: DialogCuj? + /** * Move the drawing of the source in the overlay of [viewGroup]. * @@ -142,7 +144,31 @@ constructor( * controlled by this controller. */ // TODO(b/252723237): Make this non-nullable - fun jankConfigurationBuilder(cuj: Int): InteractionJankMonitor.Configuration.Builder? + fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? + + companion object { + /** + * Create a [Controller] that can animate [source] to and from a dialog. + * + * Important: The view must be attached to a [ViewGroup] when calling this function and + * during the animation. For safety, this method will return null when it is not. + * + * Note: The background of [view] should be a (rounded) rectangle so that it can be + * properly animated. + */ + fun fromView(source: View, cuj: DialogCuj? = null): Controller? { + if (source.parent !is ViewGroup) { + Log.e( + TAG, + "Skipping animation as view $source is not attached to a ViewGroup", + Exception(), + ) + return null + } + + return ViewDialogLaunchAnimatorController(source, cuj) + } + } } /** @@ -172,7 +198,12 @@ constructor( cuj: DialogCuj? = null, animateBackgroundBoundsChange: Boolean = false ) { - show(dialog, createController(view), cuj, animateBackgroundBoundsChange) + val controller = Controller.fromView(view, cuj) + if (controller == null) { + dialog.show() + } else { + show(dialog, controller, animateBackgroundBoundsChange) + } } /** @@ -187,10 +218,10 @@ constructor( * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be * made fullscreen and 2 views will be inserted between the dialog DecorView and its children. */ + @JvmOverloads fun show( dialog: Dialog, controller: Controller, - cuj: DialogCuj? = null, animateBackgroundBoundsChange: Boolean = false ) { if (Looper.myLooper() != Looper.getMainLooper()) { @@ -207,7 +238,10 @@ constructor( it.dialog.window.decorView.viewRootImpl == controller.viewRoot } val animateFrom = - animatedParent?.dialogContentWithBackground?.let { createController(it) } ?: controller + animatedParent?.dialogContentWithBackground?.let { + Controller.fromView(it, controller.cuj) + } + ?: controller if (animatedParent == null && animateFrom !is LaunchableView) { // Make sure the View we launch from implements LaunchableView to avoid visibility @@ -244,96 +278,12 @@ constructor( animateBackgroundBoundsChange, animatedParent, isForTesting, - cuj, ) openedDialogs.add(animatedDialog) animatedDialog.start() } - /** Create a [Controller] that can animate [source] to & from a dialog. */ - private fun createController(source: View): Controller { - return object : Controller { - override val viewRoot: ViewRootImpl - get() = source.viewRootImpl - - override val sourceIdentity: Any = source - - override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { - // Create a temporary ghost of the source (which will make it invisible) and add it - // to the host dialog. - GhostView.addGhost(source, viewGroup) - - // The ghost of the source was just created, so the source is currently invisible. - // We need to make sure that it stays invisible as long as the dialog is shown or - // animating. - (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true) - } - - override fun stopDrawingInOverlay() { - // Note: here we should remove the ghost from the overlay, but in practice this is - // already done by the launch controllers created below. - - // Make sure we allow the source to change its visibility again. - (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) - source.visibility = View.VISIBLE - } - - override fun createLaunchController(): LaunchAnimator.Controller { - val delegate = GhostedViewLaunchAnimatorController(source) - return object : LaunchAnimator.Controller by delegate { - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { - // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another - // ghost (that ghosts only the source content, and not its background) will - // be added right after this by the delegate and will be animated. - GhostView.removeGhost(source) - delegate.onLaunchAnimationStart(isExpandingFullyAbove) - } - - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { - delegate.onLaunchAnimationEnd(isExpandingFullyAbove) - - // We hide the source when the dialog is showing. We will make this view - // visible again when dismissing the dialog. This does nothing if the source - // implements [LaunchableView], as it's already INVISIBLE in that case. - source.visibility = View.INVISIBLE - } - } - } - - override fun createExitController(): LaunchAnimator.Controller { - return GhostedViewLaunchAnimatorController(source) - } - - override fun shouldAnimateExit(): Boolean { - // The source should be invisible by now, if it's not then something else changed - // its visibility and we probably don't want to run the animation. - if (source.visibility != View.INVISIBLE) { - return false - } - - return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true) - } - - override fun onExitAnimationCancelled() { - // Make sure we allow the source to change its visibility again. - (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) - - // If the view is invisible it's probably because of us, so we make it visible - // again. - if (source.visibility == View.INVISIBLE) { - source.visibility = View.VISIBLE - } - } - - override fun jankConfigurationBuilder( - cuj: Int - ): InteractionJankMonitor.Configuration.Builder? { - return InteractionJankMonitor.Configuration.Builder.withView(cuj, source) - } - } - } - /** * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will * allow for dismissing the whole stack. @@ -563,9 +513,6 @@ private class AnimatedDialog( * Whether synchronization should be disabled, which can be useful if we are running in a test. */ private val forceDisableSynchronization: Boolean, - - /** Interaction to which the dialog animation is associated. */ - private val cuj: DialogCuj? = null ) { /** * The DecorView of this dialog window. @@ -618,8 +565,9 @@ private class AnimatedDialog( private var hasInstrumentedJank = false fun start() { + val cuj = controller.cuj if (cuj != null) { - val config = controller.jankConfigurationBuilder(cuj.cujType) + val config = controller.jankConfigurationBuilder() if (config != null) { if (cuj.tag != null) { config.setTag(cuj.tag) @@ -865,7 +813,7 @@ private class AnimatedDialog( return } - ViewRootSync.synchronizeNextDraw(decorView, controller.viewRoot.view, then) + ViewRootSync.synchronizeNextDraw(controller.viewRoot.view, decorView, then) decorView.invalidate() controller.viewRoot.view.invalidate() } @@ -917,7 +865,7 @@ private class AnimatedDialog( } if (hasInstrumentedJank) { - interactionJankMonitor.end(cuj!!.cujType) + interactionJankMonitor.end(controller.cuj!!.cujType) } } ) diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt index 8ce372dbb278..40a5e9794d37 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt @@ -30,7 +30,12 @@ interface Expandable { */ fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller? - // TODO(b/230830644): Introduce DialogLaunchAnimator and a function to expose it here. + /** + * Create a [DialogLaunchAnimator.Controller] that can be used to expand this [Expandable] into + * a Dialog, or return `null` if this [Expandable] should not be animated (e.g. if it is + * currently not attached or visible). + */ + fun dialogLaunchController(cuj: DialogCuj? = null): DialogLaunchAnimator.Controller? companion object { /** @@ -39,6 +44,7 @@ interface Expandable { * Note: The background of [view] should be a (rounded) rectangle so that it can be properly * animated. */ + @JvmStatic fun fromView(view: View): Expandable { return object : Expandable { override fun activityLaunchController( @@ -46,6 +52,12 @@ interface Expandable { ): ActivityLaunchAnimator.Controller? { return ActivityLaunchAnimator.Controller.fromView(view, cujType) } + + override fun dialogLaunchController( + cuj: DialogCuj? + ): DialogLaunchAnimator.Controller? { + return DialogLaunchAnimator.Controller.fromView(view, cuj) + } } } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt index f79b328190dd..5f1bb83715c2 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -89,6 +89,11 @@ class TextAnimator( var y: Float = 0f /** + * The current line of text being drawn, in a multi-line TextView. + */ + var lineNo: Int = 0 + + /** * Mutable text size of the glyph in pixels. */ var textSize: Float = 0f diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt index d427a57f3b87..0448c818f765 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt @@ -244,7 +244,7 @@ class TextInterpolator( canvas.translate(origin, layout.getLineBaseline(lineNo).toFloat()) run.fontRuns.forEach { fontRun -> - drawFontRun(canvas, run, fontRun, tmpPaint) + drawFontRun(canvas, run, fontRun, lineNo, tmpPaint) } } finally { canvas.restore() @@ -349,7 +349,7 @@ class TextInterpolator( var glyphFilter: GlyphCallback? = null // Draws single font run. - private fun drawFontRun(c: Canvas, line: Run, run: FontRun, paint: Paint) { + private fun drawFontRun(c: Canvas, line: Run, run: FontRun, lineNo: Int, paint: Paint) { var arrayIndex = 0 val font = fontInterpolator.lerp(run.baseFont, run.targetFont, progress) @@ -368,11 +368,13 @@ class TextInterpolator( tmpGlyph.font = font tmpGlyph.runStart = run.start tmpGlyph.runLength = run.end - run.start + tmpGlyph.lineNo = lineNo tmpPaintForGlyph.set(paint) var prevStart = run.start for (i in run.start until run.end) { + tmpGlyph.glyphIndex = i tmpGlyph.glyphId = line.glyphIds[i] tmpGlyph.x = MathUtils.lerp(line.baseX[i], line.targetX[i], progress) tmpGlyph.y = MathUtils.lerp(line.baseY[i], line.targetY[i], progress) diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt new file mode 100644 index 000000000000..ecee598afe4e --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt @@ -0,0 +1,107 @@ +/* + * 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.GhostView +import android.view.View +import android.view.ViewGroup +import android.view.ViewRootImpl +import com.android.internal.jank.InteractionJankMonitor + +/** A [DialogLaunchAnimator.Controller] that can animate a [View] from/to a dialog. */ +class ViewDialogLaunchAnimatorController +internal constructor( + private val source: View, + override val cuj: DialogCuj?, +) : DialogLaunchAnimator.Controller { + override val viewRoot: ViewRootImpl + get() = source.viewRootImpl + + override val sourceIdentity: Any = source + + override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { + // Create a temporary ghost of the source (which will make it invisible) and add it + // to the host dialog. + GhostView.addGhost(source, viewGroup) + + // The ghost of the source was just created, so the source is currently invisible. + // We need to make sure that it stays invisible as long as the dialog is shown or + // animating. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true) + } + + override fun stopDrawingInOverlay() { + // Note: here we should remove the ghost from the overlay, but in practice this is + // already done by the launch controllers created below. + + // Make sure we allow the source to change its visibility again. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) + source.visibility = View.VISIBLE + } + + override fun createLaunchController(): LaunchAnimator.Controller { + val delegate = GhostedViewLaunchAnimatorController(source) + return object : LaunchAnimator.Controller by delegate { + override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another + // ghost (that ghosts only the source content, and not its background) will + // be added right after this by the delegate and will be animated. + GhostView.removeGhost(source) + delegate.onLaunchAnimationStart(isExpandingFullyAbove) + } + + override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + + // We hide the source when the dialog is showing. We will make this view + // visible again when dismissing the dialog. This does nothing if the source + // implements [LaunchableView], as it's already INVISIBLE in that case. + source.visibility = View.INVISIBLE + } + } + } + + override fun createExitController(): LaunchAnimator.Controller { + return GhostedViewLaunchAnimatorController(source) + } + + override fun shouldAnimateExit(): Boolean { + // The source should be invisible by now, if it's not then something else changed + // its visibility and we probably don't want to run the animation. + if (source.visibility != View.INVISIBLE) { + return false + } + + return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true) + } + + override fun onExitAnimationCancelled() { + // Make sure we allow the source to change its visibility again. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) + + // If the view is invisible it's probably because of us, so we make it visible + // again. + if (source.visibility == View.INVISIBLE) { + source.visibility = View.VISIBLE + } + } + + override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? { + val type = cuj?.cujType ?: return null + return InteractionJankMonitor.Configuration.Builder.withView(type, source) + } +} diff --git a/packages/SystemUI/checks/Android.bp b/packages/SystemUI/checks/Android.bp index 8457312dc403..cf66ff60f9ab 100644 --- a/packages/SystemUI/checks/Android.bp +++ b/packages/SystemUI/checks/Android.bp @@ -40,6 +40,10 @@ java_test_host { "tests/**/*.kt", "tests/**/*.java", ], + data: [ + ":framework", + ":androidx.annotation_annotation", + ], static_libs: [ "SystemUILintChecker", "junit", diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt index 4eeeb850292a..4b9aa13c0240 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt @@ -32,7 +32,8 @@ import org.jetbrains.uast.UReferenceExpression class SoftwareBitmapDetector : Detector(), SourceCodeScanner { override fun getApplicableReferenceNames(): List<String> { - return mutableListOf("ALPHA_8", "RGB_565", "ARGB_8888", "RGBA_F16", "RGBA_1010102") + return mutableListOf( + "ALPHA_8", "RGB_565", "ARGB_4444", "ARGB_8888", "RGBA_F16", "RGBA_1010102") } override fun visitReference( @@ -40,13 +41,12 @@ class SoftwareBitmapDetector : Detector(), SourceCodeScanner { reference: UReferenceExpression, referenced: PsiElement ) { - val evaluator = context.evaluator if (evaluator.isMemberInClass(referenced as? PsiField, "android.graphics.Bitmap.Config")) { context.report( ISSUE, referenced, - context.getNameLocation(referenced), + context.getNameLocation(reference), "Replace software bitmap with `Config.HARDWARE`" ) } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt new file mode 100644 index 000000000000..1db072548a76 --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt @@ -0,0 +1,100 @@ +/* + * 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.internal.systemui.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +private const val CLASS_SETTINGS = "android.provider.Settings" + +/** + * Detects usage of static methods in android.provider.Settings and suggests to use an injected + * settings provider instance instead. + */ +@Suppress("UnstableApiUsage") +class StaticSettingsProviderDetector : Detector(), SourceCodeScanner { + override fun getApplicableMethodNames(): List<String> { + return listOf( + "getFloat", + "getInt", + "getLong", + "getString", + "getUriFor", + "putFloat", + "putInt", + "putLong", + "putString" + ) + } + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + val evaluator = context.evaluator + val className = method.containingClass?.qualifiedName + if ( + className != "$CLASS_SETTINGS.Global" && + className != "$CLASS_SETTINGS.Secure" && + className != "$CLASS_SETTINGS.System" + ) { + return + } + if (!evaluator.isStatic(method)) { + return + } + + val subclassName = className.substring(CLASS_SETTINGS.length + 1) + + context.report( + ISSUE, + method, + context.getNameLocation(node), + "`@Inject` a ${subclassName}Settings instead" + ) + } + + companion object { + @JvmField + val ISSUE: Issue = + Issue.create( + id = "StaticSettingsProvider", + briefDescription = "Static settings provider usage", + explanation = + """ + Static settings provider methods, such as `Settings.Global.putInt()`, should \ + not be used because they make testing difficult. Instead, use an injected \ + settings provider. For example, instead of calling `Settings.Secure.getInt()`, \ + annotate the class constructor with `@Inject` and add `SecureSettings` to the \ + parameters. + """, + category = Category.CORRECTNESS, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation( + StaticSettingsProviderDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt index cf7c1b5e44a2..3f334c1cdb9c 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt @@ -36,6 +36,7 @@ class SystemUIIssueRegistry : IssueRegistry() { RegisterReceiverViaContextDetector.ISSUE, SoftwareBitmapDetector.ISSUE, NonInjectedServiceDetector.ISSUE, + StaticSettingsProviderDetector.ISSUE ) override val api: Int diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt index 486af9dd5d98..141dd0535986 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt @@ -18,6 +18,8 @@ package com.android.internal.systemui.lint import com.android.annotations.NonNull import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java +import com.android.tools.lint.checks.infrastructure.TestFiles.LibraryReferenceTestFile +import java.io.File import org.intellij.lang.annotations.Language @Suppress("UnstableApiUsage") @@ -30,132 +32,8 @@ private fun indentedJava(@NonNull @Language("JAVA") source: String) = java(sourc */ internal val androidStubs = arrayOf( - indentedJava( - """ -package android.app; - -public class ActivityManager { - public static int getCurrentUser() {} -} -""" - ), - indentedJava( - """ -package android.accounts; - -public class AccountManager { - public static AccountManager get(Context context) { return null; } -} -""" - ), - indentedJava( - """ -package android.os; -import android.content.pm.UserInfo; -import android.annotation.UserIdInt; - -public class UserManager { - public UserInfo getUserInfo(@UserIdInt int userId) {} -} -""" - ), - indentedJava(""" -package android.annotation; - -public @interface UserIdInt {} -"""), - indentedJava(""" -package android.content.pm; - -public class UserInfo {} -"""), - indentedJava(""" -package android.os; - -public class Looper {} -"""), - indentedJava(""" -package android.os; - -public class Handler {} -"""), - indentedJava(""" -package android.content; - -public class ServiceConnection {} -"""), - indentedJava(""" -package android.os; - -public enum UserHandle { - ALL -} -"""), - indentedJava( - """ -package android.content; -import android.os.UserHandle; -import android.os.Handler; -import android.os.Looper; -import java.util.concurrent.Executor; - -public class Context { - public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {} - public void registerReceiverAsUser( - BroadcastReceiver receiver, UserHandle user, IntentFilter filter, - String broadcastPermission, Handler scheduler) {} - public void registerReceiverForAllUsers( - BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, - Handler scheduler) {} - public void sendBroadcast(Intent intent) {} - public void sendBroadcast(Intent intent, String receiverPermission) {} - public void sendBroadcastAsUser(Intent intent, UserHandle userHandle, String permission) {} - public void bindService(Intent intent) {} - public void bindServiceAsUser( - Intent intent, ServiceConnection connection, int flags, UserHandle userHandle) {} - public void unbindService(ServiceConnection connection) {} - public Looper getMainLooper() { return null; } - public Executor getMainExecutor() { return null; } - public Handler getMainThreadHandler() { return null; } - public final @Nullable <T> T getSystemService(@NonNull Class<T> serviceClass) { return null; } - public abstract @Nullable Object getSystemService(@ServiceName @NonNull String name); -} -""" - ), - indentedJava( - """ -package android.app; -import android.content.Context; - -public class Activity extends Context {} -""" - ), - indentedJava( - """ -package android.graphics; - -public class Bitmap { - public enum Config { - ARGB_8888, - RGB_565, - HARDWARE - } - public static Bitmap createBitmap(int width, int height, Config config) { - return null; - } -} -""" - ), - indentedJava(""" -package android.content; - -public class BroadcastReceiver {} -"""), - indentedJava(""" -package android.content; - -public class IntentFilter {} -"""), + LibraryReferenceTestFile(File("framework.jar").canonicalFile), + LibraryReferenceTestFile(File("androidx.annotation_annotation.jar").canonicalFile), indentedJava( """ package com.android.systemui.settings; @@ -167,23 +45,4 @@ public interface UserTracker { } """ ), - indentedJava( - """ -package androidx.annotation; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.CONSTRUCTOR; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -@Retention(SOURCE) -@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) -public @interface WorkerThread { -} -""" - ), ) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt index 6ae8fd3f25a1..c35ac61a6543 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class BindServiceOnMainThreadDetectorTest : LintDetectorTest() { +class BindServiceOnMainThreadDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = BindServiceOnMainThreadDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(BindServiceOnMainThreadDetector.ISSUE) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt index 7d422807ae08..376acb56fac9 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class BroadcastSentViaContextDetectorTest : LintDetectorTest() { +class BroadcastSentViaContextDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = BroadcastSentViaContextDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(BroadcastSentViaContextDetector.ISSUE) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt index c468af8d09e0..301c338f9b42 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class NonInjectedMainThreadDetectorTest : LintDetectorTest() { +class NonInjectedMainThreadDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = NonInjectedMainThreadDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(NonInjectedMainThreadDetector.ISSUE) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt index c83a35b46ca6..0a74bfcfee57 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class NonInjectedServiceDetectorTest : LintDetectorTest() { +class NonInjectedServiceDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = NonInjectedServiceDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(NonInjectedServiceDetector.ISSUE) @Test diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt index ebcddebfbc28..9ed7aa029b1d 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { +class RegisterReceiverViaContextDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = RegisterReceiverViaContextDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt index b03a11c4f02f..54cac7b35598 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class SlowUserQueryDetectorTest : LintDetectorTest() { +class SlowUserQueryDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = SlowUserQueryDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf( diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt index fb6537e92d15..c632636eb9c8 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class SoftwareBitmapDetectorTest : LintDetectorTest() { +class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = SoftwareBitmapDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(SoftwareBitmapDetector.ISSUE) @@ -54,12 +51,12 @@ class SoftwareBitmapDetectorTest : LintDetectorTest() { .run() .expect( """ - src/android/graphics/Bitmap.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] - ARGB_8888, - ~~~~~~~~~ - src/android/graphics/Bitmap.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] - RGB_565, - ~~~~~~~ + src/TestClass.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] + Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565); + ~~~~~~~ + src/TestClass.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + ~~~~~~~~~ 0 errors, 2 warnings """ ) @@ -70,7 +67,7 @@ class SoftwareBitmapDetectorTest : LintDetectorTest() { lint() .files( TestFiles.java( - """ + """ import android.graphics.Bitmap; public class TestClass { @@ -79,8 +76,7 @@ class SoftwareBitmapDetectorTest : LintDetectorTest() { } } """ - ) - .indented(), + ), *stubs ) .issues(SoftwareBitmapDetector.ISSUE) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt new file mode 100644 index 000000000000..b83ed7067bc3 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt @@ -0,0 +1,208 @@ +/* + * 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.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +@Suppress("UnstableApiUsage") +class StaticSettingsProviderDetectorTest : SystemUILintDetectorTest() { + + override fun getDetector(): Detector = StaticSettingsProviderDetector() + override fun getIssues(): List<Issue> = listOf(StaticSettingsProviderDetector.ISSUE) + + @Test + fun testGetServiceWithString() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + + import android.provider.Settings; + import android.provider.Settings.Global; + import android.provider.Settings.Secure; + + public class TestClass { + public void getSystemServiceWithoutDagger(Context context) { + final ContentResolver cr = mContext.getContentResolver(); + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND); + Global.getInt(cr, Settings.Global.UNLOCK_SOUND); + Global.getLong(cr, Settings.Global.UNLOCK_SOUND); + Global.getString(cr, Settings.Global.UNLOCK_SOUND); + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f); + Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1); + Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L); + Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1"); + Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f); + Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1); + Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L); + Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1"); + + Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f); + Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1); + Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L); + Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1"); + Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f); + Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1); + Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L); + Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1"); + + Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT); + Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT); + Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT); + Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT); + Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f); + Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1); + Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L); + Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1"); + Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f); + Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1); + Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L); + Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1"); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(StaticSettingsProviderDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:10: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND); + ~~~~~~~~ + src/test/pkg/TestClass.java:11: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getInt(cr, Settings.Global.UNLOCK_SOUND); + ~~~~~~ + src/test/pkg/TestClass.java:12: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getLong(cr, Settings.Global.UNLOCK_SOUND); + ~~~~~~~ + src/test/pkg/TestClass.java:13: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getString(cr, Settings.Global.UNLOCK_SOUND); + ~~~~~~~~~ + src/test/pkg/TestClass.java:14: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:15: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1); + ~~~~~~ + src/test/pkg/TestClass.java:16: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:17: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:18: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:19: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1); + ~~~~~~ + src/test/pkg/TestClass.java:20: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:21: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:23: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + ~~~~~~~~ + src/test/pkg/TestClass.java:24: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + ~~~~~~ + src/test/pkg/TestClass.java:25: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + ~~~~~~~ + src/test/pkg/TestClass.java:26: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + ~~~~~~~~~ + src/test/pkg/TestClass.java:27: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:28: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1); + ~~~~~~ + src/test/pkg/TestClass.java:29: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:30: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:31: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:32: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1); + ~~~~~~ + src/test/pkg/TestClass.java:33: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:34: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:36: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT); + ~~~~~~~~ + src/test/pkg/TestClass.java:37: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT); + ~~~~~~ + src/test/pkg/TestClass.java:38: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT); + ~~~~~~~ + src/test/pkg/TestClass.java:39: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT); + ~~~~~~~~~ + src/test/pkg/TestClass.java:40: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:41: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1); + ~~~~~~ + src/test/pkg/TestClass.java:42: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:43: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:44: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:45: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1); + ~~~~~~ + src/test/pkg/TestClass.java:46: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:47: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1"); + ~~~~~~~~~ + 0 errors, 36 warnings + """ + ) + } + + private val stubs = androidStubs +} diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt new file mode 100644 index 000000000000..3f93f075fe8b --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt @@ -0,0 +1,48 @@ +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestLintTask +import java.io.File +import org.junit.ClassRule +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.model.Statement + +@Suppress("UnstableApiUsage") +@RunWith(JUnit4::class) +abstract class SystemUILintDetectorTest : LintDetectorTest() { + + companion object { + @ClassRule + @JvmField + val libraryChecker: LibraryExists = + LibraryExists("framework.jar", "androidx.annotation_annotation.jar") + } + + class LibraryExists(vararg val libraryNames: String) : TestRule { + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + for (libName in libraryNames) { + val libFile = File(libName) + if (!libFile.canonicalFile.exists()) { + throw Exception( + "Could not find $libName in the test's working directory. " + + "File ${libFile.absolutePath} does not exist." + ) + } + } + base.evaluate() + } + } + } + } + /** + * Customize the lint task to disable SDK usage completely. This ensures that running the tests + * in Android Studio has the same result as running the tests in atest + */ + override fun lint(): TestLintTask = + super.lint().allowMissingSdk(true).sdkHome(File("/dev/null")) +} diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt index 065c3149c2f5..50c3d7e1e76b 100644 --- a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt +++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt @@ -40,17 +40,16 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.animation.LaunchAnimator import kotlin.math.roundToInt -/** A controller that can control animated launches. */ +/** A controller that can control animated launches from an [Expandable]. */ interface ExpandableController { - /** Create an [ActivityLaunchAnimator.Controller] to animate into an Activity. */ - fun forActivity(): ActivityLaunchAnimator.Controller - - /** Create a [DialogLaunchAnimator.Controller] to animate into a Dialog. */ - fun forDialog(): DialogLaunchAnimator.Controller + /** The [Expandable] controlled by this controller. */ + val expandable: Expandable } /** @@ -120,13 +119,26 @@ internal class ExpandableControllerImpl( private val layoutDirection: LayoutDirection, private val isComposed: State<Boolean>, ) : ExpandableController { - override fun forActivity(): ActivityLaunchAnimator.Controller { - return activityController() - } + override val expandable: Expandable = + object : Expandable { + override fun activityLaunchController( + cujType: Int?, + ): ActivityLaunchAnimator.Controller? { + if (!isComposed.value) { + return null + } - override fun forDialog(): DialogLaunchAnimator.Controller { - return dialogController() - } + return activityController(cujType) + } + + override fun dialogLaunchController(cuj: DialogCuj?): DialogLaunchAnimator.Controller? { + if (!isComposed.value) { + return null + } + + return dialogController(cuj) + } + } /** * Create a [LaunchAnimator.Controller] that is going to be used to drive an activity or dialog @@ -233,7 +245,7 @@ internal class ExpandableControllerImpl( } /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */ - private fun activityController(): ActivityLaunchAnimator.Controller { + private fun activityController(cujType: Int?): ActivityLaunchAnimator.Controller { val delegate = launchController() return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate { override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { @@ -248,10 +260,11 @@ internal class ExpandableControllerImpl( } } - private fun dialogController(): DialogLaunchAnimator.Controller { + private fun dialogController(cuj: DialogCuj?): DialogLaunchAnimator.Controller { return object : DialogLaunchAnimator.Controller { override val viewRoot: ViewRootImpl = composeViewRoot.viewRootImpl override val sourceIdentity: Any = this@ExpandableControllerImpl + override val cuj: DialogCuj? = cuj override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { val newOverlay = viewGroup.overlay as ViewGroupOverlay @@ -294,9 +307,7 @@ internal class ExpandableControllerImpl( isDialogShowing.value = false } - override fun jankConfigurationBuilder( - cuj: Int - ): InteractionJankMonitor.Configuration.Builder? { + override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? { // TODO(b/252723237): Add support for jank monitoring when animating from a // Composable. return null diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt index a850238eb52f..da612a9276a3 100644 --- a/packages/SystemUI/ktfmt_includes.txt +++ b/packages/SystemUI/ktfmt_includes.txt @@ -189,45 +189,8 @@ -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt --packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt --packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt --packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt --packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt --packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt --packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt --packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt --packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt --packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt --packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt --packages/SystemUI/src/com/android/systemui/media/MediaData.kt --packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt --packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt --packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt --packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt --packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt --packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt --packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt --packages/SystemUI/src/com/android/systemui/media/MediaHost.kt --packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt -packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt -packages/SystemUI/src/com/android/systemui/media/MediaProjectionCaptureTarget.kt --packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt --packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt --packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt --packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt --packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt --packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt --packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt --packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt --packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt --packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt --packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt --packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt --packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt --packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt --packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt --packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt --packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt -packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt -packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt -packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt @@ -653,26 +616,6 @@ -packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt -packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt -packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt -packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt -packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt -packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt @@ -832,7 +775,6 @@ -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/WalletControllerImplTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/window/StatusBarWindowStateControllerTest.kt -packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt -packages/SystemUI/tests/src/com/android/systemui/unfold/FoldStateLoggingProviderTest.kt -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt index b3dd95553ed0..dee0f5cd1979 100644 --- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt @@ -205,6 +205,13 @@ enum class Style(internal val coreSpec: CoreSpec) { n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)), n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666)) )), + MONOCHROMATIC(CoreSpec( + a1 = TonalSpec(HueSource(), ChromaConstant(.0)), + a2 = TonalSpec(HueSource(), ChromaConstant(.0)), + a3 = TonalSpec(HueSource(), ChromaConstant(.0)), + n1 = TonalSpec(HueSource(), ChromaConstant(.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(.0)) + )), } class ColorScheme( @@ -219,7 +226,7 @@ class ColorScheme( val neutral1: List<Int> val neutral2: List<Int> - constructor(@ColorInt seed: Int, darkTheme: Boolean): + constructor(@ColorInt seed: Int, darkTheme: Boolean) : this(seed, darkTheme, Style.TONAL_SPOT) @JvmOverloads @@ -227,7 +234,7 @@ class ColorScheme( wallpaperColors: WallpaperColors, darkTheme: Boolean, style: Style = Style.TONAL_SPOT - ): + ) : this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) val allAccentColors: List<Int> @@ -472,4 +479,4 @@ class ColorScheme( return huePopulation } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp index cafaaf854eed..7709f210f22f 100644 --- a/packages/SystemUI/plugin/Android.bp +++ b/packages/SystemUI/plugin/Android.bp @@ -33,6 +33,7 @@ java_library { static_libs: [ "androidx.annotation_annotation", + "error_prone_annotations", "PluginCoreLib", "SystemUIAnimationLib", ], diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 1e74c3d68efc..89f5c2c80e29 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -14,9 +14,11 @@ package com.android.systemui.plugins import android.content.res.Resources +import android.graphics.Rect import android.graphics.drawable.Drawable import android.view.View import com.android.systemui.plugins.annotations.ProvidesInterface +import com.android.systemui.plugins.log.LogBuffer import java.io.PrintWriter import java.util.Locale import java.util.TimeZone @@ -69,6 +71,9 @@ interface ClockController { /** Optional method for dumping debug information */ fun dump(pw: PrintWriter) { } + + /** Optional method for debug logging */ + fun setLogBuffer(logBuffer: LogBuffer) { } } /** Interface for a specific clock face version rendered by the clock */ @@ -114,6 +119,17 @@ interface ClockAnimations { /** Runs the battery animation (if any). */ fun charge() { } + + /** Move the clock, for example, if the notification tray appears in split-shade mode. */ + fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) { } + + /** + * Whether this clock has a custom position update animation. If true, the keyguard will call + * `onPositionUpdated` to notify the clock of a position update animation. If false, a default + * animation will be used (e.g. a simple translation). + */ + val hasCustomPositionUpdatedAnimation + get() = false } /** Events that have specific data about the related face */ diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt index 6124e10144f2..6436dcb5f613 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt @@ -14,12 +14,11 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log import android.os.Trace import android.util.Log -import com.android.systemui.log.dagger.LogModule -import com.android.systemui.util.collection.RingBuffer +import com.android.systemui.plugins.util.RingBuffer import com.google.errorprone.annotations.CompileTimeConstant import java.io.PrintWriter import java.util.concurrent.ArrayBlockingQueue @@ -61,15 +60,18 @@ import kotlin.math.max * In either case, `level` can be any of `verbose`, `debug`, `info`, `warn`, `error`, `assert`, or * the first letter of any of the previous. * - * Buffers are provided by [LogModule]. Instances should be created using a [LogBufferFactory]. + * In SystemUI, buffers are provided by LogModule. Instances should be created using a SysUI + * LogBufferFactory. * * @param name The name of this buffer, printed when the buffer is dumped and in some other * situations. * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start - * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches - * the maximum, it behaves like a ring buffer. + * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches the + * maximum, it behaves like a ring buffer. */ -class LogBuffer @JvmOverloads constructor( +class LogBuffer +@JvmOverloads +constructor( private val name: String, private val maxSize: Int, private val logcatEchoTracker: LogcatEchoTracker, @@ -78,7 +80,7 @@ class LogBuffer @JvmOverloads constructor( private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() } private val echoMessageQueue: BlockingQueue<LogMessage>? = - if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null + if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null init { if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) { @@ -133,11 +135,11 @@ class LogBuffer @JvmOverloads constructor( */ @JvmOverloads inline fun log( - tag: String, - level: LogLevel, - messageInitializer: MessageInitializer, - noinline messagePrinter: MessagePrinter, - exception: Throwable? = null, + tag: String, + level: LogLevel, + messageInitializer: MessageInitializer, + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, ) { val message = obtain(tag, level, messagePrinter, exception) messageInitializer(message) @@ -152,14 +154,13 @@ class LogBuffer @JvmOverloads constructor( * log message is built during runtime, use the [LogBuffer.log] overloaded method that takes in * an initializer and a message printer. * - * Log buffers are limited by the number of entries, so logging more frequently - * will limit the time window that the LogBuffer covers in a bug report. Richer logs, on the - * other hand, make a bug report more actionable, so using the [log] with a messagePrinter to - * add more detail to every log may do more to improve overall logging than adding more logs - * with this method. + * Log buffers are limited by the number of entries, so logging more frequently will limit the + * time window that the LogBuffer covers in a bug report. Richer logs, on the other hand, make a + * bug report more actionable, so using the [log] with a messagePrinter to add more detail to + * every log may do more to improve overall logging than adding more logs with this method. */ fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) = - log(tag, level, {str1 = message}, { str1!! }) + log(tag, level, { str1 = message }, { str1!! }) /** * You should call [log] instead of this method. @@ -172,10 +173,10 @@ class LogBuffer @JvmOverloads constructor( */ @Synchronized fun obtain( - tag: String, - level: LogLevel, - messagePrinter: MessagePrinter, - exception: Throwable? = null, + tag: String, + level: LogLevel, + messagePrinter: MessagePrinter, + exception: Throwable? = null, ): LogMessage { if (!mutable) { return FROZEN_MESSAGE @@ -189,8 +190,7 @@ class LogBuffer @JvmOverloads constructor( * You should call [log] instead of this method. * * After acquiring a message via [obtain], call this method to signal to the buffer that you - * have finished filling in its data fields. The message will be echoed to logcat if - * necessary. + * have finished filling in its data fields. The message will be echoed to logcat if necessary. */ @Synchronized fun commit(message: LogMessage) { @@ -213,7 +213,8 @@ class LogBuffer @JvmOverloads constructor( /** Sends message to echo after determining whether to use Logcat and/or systrace. */ private fun echoToDesiredEndpoints(message: LogMessage) { - val includeInLogcat = logcatEchoTracker.isBufferLoggable(name, message.level) || + val includeInLogcat = + logcatEchoTracker.isBufferLoggable(name, message.level) || logcatEchoTracker.isTagLoggable(message.tag, message.level) echo(message, toLogcat = includeInLogcat, toSystrace = systrace) } @@ -221,7 +222,12 @@ class LogBuffer @JvmOverloads constructor( /** Converts the entire buffer to a newline-delimited string */ @Synchronized fun dump(pw: PrintWriter, tailLength: Int) { - val iterationStart = if (tailLength <= 0) { 0 } else { max(0, buffer.size - tailLength) } + val iterationStart = + if (tailLength <= 0) { + 0 + } else { + max(0, buffer.size - tailLength) + } for (i in iterationStart until buffer.size) { buffer[i].dump(pw) @@ -229,9 +235,9 @@ class LogBuffer @JvmOverloads constructor( } /** - * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. - * Calls to [log], [obtain], and [commit] will not affect the buffer and will return dummy - * values if necessary. + * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. Calls + * to [log], [obtain], and [commit] will not affect the buffer and will return dummy values if + * necessary. */ @Synchronized fun freeze() { @@ -241,9 +247,7 @@ class LogBuffer @JvmOverloads constructor( } } - /** - * Undoes the effects of calling [freeze]. - */ + /** Undoes the effects of calling [freeze]. */ @Synchronized fun unfreeze() { if (frozen) { @@ -265,8 +269,11 @@ class LogBuffer @JvmOverloads constructor( } private fun echoToSystrace(message: LogMessage, strMessage: String) { - Trace.instantForTrack(Trace.TRACE_TAG_APP, "UI Events", - "$name - ${message.level.shortString} ${message.tag}: $strMessage") + Trace.instantForTrack( + Trace.TRACE_TAG_APP, + "UI Events", + "$name - ${message.level.shortString} ${message.tag}: $strMessage" + ) } private fun echoToLogcat(message: LogMessage, strMessage: String) { diff --git a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt index 53f231c9f9d2..b036cf0be1d6 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt @@ -14,17 +14,12 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log import android.util.Log -/** - * Enum version of @Log.Level - */ -enum class LogLevel( - @Log.Level val nativeLevel: Int, - val shortString: String -) { +/** Enum version of @Log.Level */ +enum class LogLevel(@Log.Level val nativeLevel: Int, val shortString: String) { VERBOSE(Log.VERBOSE, "V"), DEBUG(Log.DEBUG, "D"), INFO(Log.INFO, "I"), diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt index dae2592e116c..9468681289bf 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log import java.io.PrintWriter import java.text.SimpleDateFormat @@ -29,9 +29,10 @@ import java.util.Locale * * When a message is logged, the code doing the logging stores data in one or more of the generic * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the - * [messagePrinter] function reads the data stored in the generic fields and converts that to a human- - * readable string. Thus, for every log type there must be a specialized initializer function that - * stores data specific to that log type and a specialized printer function that prints that data. + * [messagePrinter] function reads the data stored in the generic fields and converts that to a + * human- readable string. Thus, for every log type there must be a specialized initializer function + * that stores data specific to that log type and a specialized printer function that prints that + * data. * * See [LogBuffer.log] for more information. */ @@ -55,9 +56,7 @@ interface LogMessage { var bool3: Boolean var bool4: Boolean - /** - * Function that dumps the [LogMessage] to the provided [writer]. - */ + /** Function that dumps the [LogMessage] to the provided [writer]. */ fun dump(writer: PrintWriter) { val formattedTimestamp = DATE_FORMAT.format(timestamp) val shortLevel = level.shortString @@ -68,12 +67,12 @@ interface LogMessage { } /** - * A function that will be called if and when the message needs to be dumped to - * logcat or a bug report. It should read the data stored by the initializer and convert it to - * a human-readable string. The value of `this` will be the LogMessage to be printed. - * **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and NEVER any - * variables in its enclosing scope. Otherwise, the runtime will need to allocate a new instance - * of the printer for each call, thwarting our attempts at avoiding any sort of allocation. + * A function that will be called if and when the message needs to be dumped to logcat or a bug + * report. It should read the data stored by the initializer and convert it to a human-readable + * string. The value of `this` will be the LogMessage to be printed. **IMPORTANT:** The printer + * should ONLY ever reference fields on the LogMessage and NEVER any variables in its enclosing + * scope. Otherwise, the runtime will need to allocate a new instance of the printer for each call, + * thwarting our attempts at avoiding any sort of allocation. */ typealias MessagePrinter = LogMessage.() -> String diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt index 4dd6f652d1c7..f2a6a91adcdf 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt @@ -14,11 +14,9 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log -/** - * Recyclable implementation of [LogMessage]. - */ +/** Recyclable implementation of [LogMessage]. */ data class LogMessageImpl( override var level: LogLevel, override var tag: String, @@ -68,23 +66,24 @@ data class LogMessageImpl( companion object Factory { fun create(): LogMessageImpl { return LogMessageImpl( - LogLevel.DEBUG, - DEFAULT_TAG, - 0, - DEFAULT_PRINTER, - null, - null, - null, - null, - 0, - 0, - 0, - 0, - 0.0, - false, - false, - false, - false) + LogLevel.DEBUG, + DEFAULT_TAG, + 0, + DEFAULT_PRINTER, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0.0, + false, + false, + false, + false + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt index 8cda4236bc87..cfe894f276a0 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt @@ -14,24 +14,16 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log -/** - * Keeps track of which [LogBuffer] messages should also appear in logcat. - */ +/** Keeps track of which [LogBuffer] messages should also appear in logcat. */ interface LogcatEchoTracker { - /** - * Whether [bufferName] should echo messages of [level] or higher to logcat. - */ + /** Whether [bufferName] should echo messages of [level] or higher to logcat. */ fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean - /** - * Whether [tagName] should echo messages of [level] or higher to logcat. - */ + /** Whether [tagName] should echo messages of [level] or higher to logcat. */ fun isTagLoggable(tagName: String, level: LogLevel): Boolean - /** - * Whether to log messages in a background thread. - */ + /** Whether to log messages in a background thread. */ val logInBackgroundThread: Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt index 40b0cdc173d8..d3fabaccb6d3 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log import android.content.ContentResolver import android.database.ContentObserver @@ -36,19 +36,15 @@ import android.provider.Settings * $ adb shell settings put global systemui/tag/<tag> <level> * ``` */ -class LogcatEchoTrackerDebug private constructor( - private val contentResolver: ContentResolver -) : LogcatEchoTracker { +class LogcatEchoTrackerDebug private constructor(private val contentResolver: ContentResolver) : + LogcatEchoTracker { private val cachedBufferLevels: MutableMap<String, LogLevel> = mutableMapOf() private val cachedTagLevels: MutableMap<String, LogLevel> = mutableMapOf() override val logInBackgroundThread = true companion object Factory { @JvmStatic - fun create( - contentResolver: ContentResolver, - mainLooper: Looper - ): LogcatEchoTrackerDebug { + fun create(contentResolver: ContentResolver, mainLooper: Looper): LogcatEchoTrackerDebug { val tracker = LogcatEchoTrackerDebug(contentResolver) tracker.attach(mainLooper) return tracker @@ -57,37 +53,35 @@ class LogcatEchoTrackerDebug private constructor( private fun attach(mainLooper: Looper) { contentResolver.registerContentObserver( - Settings.Global.getUriFor(BUFFER_PATH), - true, - object : ContentObserver(Handler(mainLooper)) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - super.onChange(selfChange, uri) - cachedBufferLevels.clear() - } - }) + Settings.Global.getUriFor(BUFFER_PATH), + true, + object : ContentObserver(Handler(mainLooper)) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + cachedBufferLevels.clear() + } + } + ) contentResolver.registerContentObserver( - Settings.Global.getUriFor(TAG_PATH), - true, - object : ContentObserver(Handler(mainLooper)) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - super.onChange(selfChange, uri) - cachedTagLevels.clear() - } - }) + Settings.Global.getUriFor(TAG_PATH), + true, + object : ContentObserver(Handler(mainLooper)) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + cachedTagLevels.clear() + } + } + ) } - /** - * Whether [bufferName] should echo messages of [level] or higher to logcat. - */ + /** Whether [bufferName] should echo messages of [level] or higher to logcat. */ @Synchronized override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean { return level.ordinal >= getLogLevel(bufferName, BUFFER_PATH, cachedBufferLevels).ordinal } - /** - * Whether [tagName] should echo messages of [level] or higher to logcat. - */ + /** Whether [tagName] should echo messages of [level] or higher to logcat. */ @Synchronized override fun isTagLoggable(tagName: String, level: LogLevel): Boolean { return level >= getLogLevel(tagName, TAG_PATH, cachedTagLevels) diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt index 1a4ad1907ff1..3c8bda4a44e0 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt @@ -14,11 +14,9 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log -/** - * Production version of [LogcatEchoTracker] that isn't configurable. - */ +/** Production version of [LogcatEchoTracker] that isn't configurable. */ class LogcatEchoTrackerProd : LogcatEchoTracker { override val logInBackgroundThread = false diff --git a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt index 97dc842ec699..68d78907f028 100644 --- a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.util.collection +package com.android.systemui.plugins.util import kotlin.math.max @@ -32,19 +32,16 @@ import kotlin.math.max * @param factory A function that creates a fresh instance of T. Used by the buffer while it's * growing to [maxSize]. */ -class RingBuffer<T>( - private val maxSize: Int, - private val factory: () -> T -) : Iterable<T> { +class RingBuffer<T>(private val maxSize: Int, private val factory: () -> T) : Iterable<T> { private val buffer = MutableList<T?>(maxSize) { null } /** * An abstract representation that points to the "end" of the buffer. Increments every time - * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into - * the backing array. Always points to the "next" available slot in the buffer. Before the - * buffer has completely filled, the value pointed to will be null. Afterward, it will be the - * value at the "beginning" of the buffer. + * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into the + * backing array. Always points to the "next" available slot in the buffer. Before the buffer + * has completely filled, the value pointed to will be null. Afterward, it will be the value at + * the "beginning" of the buffer. * * This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms, * omega will overflow after a little under three million years of continuous operation. @@ -60,24 +57,23 @@ class RingBuffer<T>( /** * Advances the buffer's position by one and returns the value that is now present at the "end" - * of the buffer. If the buffer is not yet full, uses [factory] to create a new item. - * Otherwise, reuses the value that was previously at the "beginning" of the buffer. + * of the buffer. If the buffer is not yet full, uses [factory] to create a new item. Otherwise, + * reuses the value that was previously at the "beginning" of the buffer. * - * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that - * was previously stored on it. + * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that was + * previously stored on it. */ fun advance(): T { val index = indexOf(omega) omega += 1 - val entry = buffer[index] ?: factory().also { - buffer[index] = it - } + val entry = buffer[index] ?: factory().also { buffer[index] = it } return entry } /** * Returns the value stored at [index], which can range from 0 (the "start", or oldest element - * of the buffer) to [size] - 1 (the "end", or newest element of the buffer). + * of the buffer) to [size] + * - 1 (the "end", or newest element of the buffer). */ operator fun get(index: Int): T { if (index < 0 || index >= size) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt index 56aff3c2fc8b..a39b856f0f49 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt +++ b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt @@ -2,6 +2,7 @@ package com.android.systemui.log import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.log.LogBuffer import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter @@ -18,8 +19,7 @@ class LogBufferTest : SysuiTestCase() { private lateinit var outputWriter: StringWriter - @Mock - private lateinit var logcatEchoTracker: LogcatEchoTracker + @Mock private lateinit var logcatEchoTracker: LogcatEchoTracker @Before fun setup() { @@ -67,15 +67,17 @@ class LogBufferTest : SysuiTestCase() { @Test fun dump_writesCauseAndStacktrace() { buffer = createBuffer() - val exception = createTestException("Exception message", + val exception = + createTestException( + "Exception message", "TestClass", - cause = createTestException("The real cause!", "TestClass")) + cause = createTestException("The real cause!", "TestClass") + ) buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception) val dumpedString = dumpBuffer() - assertThat(dumpedString) - .contains("Caused by: java.lang.RuntimeException: The real cause!") + assertThat(dumpedString).contains("Caused by: java.lang.RuntimeException: The real cause!") assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:1)") assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:2)") } @@ -85,49 +87,47 @@ class LogBufferTest : SysuiTestCase() { buffer = createBuffer() val exception = RuntimeException("Root exception message") exception.addSuppressed( - createTestException( - "First suppressed exception", - "FirstClass", - createTestException("Cause of suppressed exp", "ThirdClass") - )) - exception.addSuppressed( - createTestException("Second suppressed exception", "SecondClass")) + createTestException( + "First suppressed exception", + "FirstClass", + createTestException("Cause of suppressed exp", "ThirdClass") + ) + ) + exception.addSuppressed(createTestException("Second suppressed exception", "SecondClass")) buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception) val dumpedStr = dumpBuffer() // first suppressed exception assertThat(dumpedStr) - .contains("Suppressed: " + - "java.lang.RuntimeException: First suppressed exception") + .contains("Suppressed: " + "java.lang.RuntimeException: First suppressed exception") assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:1)") assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:2)") assertThat(dumpedStr) - .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp") + .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp") assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:1)") assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:2)") // second suppressed exception assertThat(dumpedStr) - .contains("Suppressed: " + - "java.lang.RuntimeException: Second suppressed exception") + .contains("Suppressed: " + "java.lang.RuntimeException: Second suppressed exception") assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:1)") assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:2)") } private fun createTestException( - message: String, - errorClass: String, - cause: Throwable? = null, + message: String, + errorClass: String, + cause: Throwable? = null, ): Exception { val exception = RuntimeException(message, cause) - exception.stackTrace = (1..5).map { lineNumber -> - StackTraceElement(errorClass, - "TestMethod", - "$errorClass.java", - lineNumber) - }.toTypedArray() + exception.stackTrace = + (1..5) + .map { lineNumber -> + StackTraceElement(errorClass, "TestMethod", "$errorClass.java", lineNumber) + } + .toTypedArray() return exception } diff --git a/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml new file mode 100644 index 000000000000..de0e526a97c3 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml @@ -0,0 +1,20 @@ +<?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 + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" > + <size android:height="@dimen/bouncer_user_switcher_popup_items_divider_height"/> + <solid android:color="@color/user_switcher_fullscreen_bg"/> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml index 3ad7c8c4369c..d64587dcf362 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml @@ -37,6 +37,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="@dimen/keyguard_large_clock_top_margin" + android:clipChildren="false" android:visibility="gone" /> <!-- Not quite optimal but needed to translate these items as a group. The diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml index 16a1d944c4d3..647abee9a99b 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml @@ -27,6 +27,7 @@ systemui:layout_constraintEnd_toEndOf="parent" systemui:layout_constraintTop_toTopOf="parent" android:layout_marginHorizontal="@dimen/status_view_margin_horizontal" + android:clipChildren="false" android:layout_width="0dp" android:layout_height="wrap_content"> <LinearLayout diff --git a/packages/SystemUI/res-keyguard/values-af/strings.xml b/packages/SystemUI/res-keyguard/values-af/strings.xml index d5e84f9e30eb..d5552f6ad44f 100644 --- a/packages/SystemUI/res-keyguard/values-af/strings.xml +++ b/packages/SystemUI/res-keyguard/values-af/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Patroon word vereis nadat toestel herbegin het"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN word vereis nadat toestel herbegin het"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Wagwoord word vereis nadat toestel herbegin het"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Patroon word vir bykomende sekuriteit vereis"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN word vir bykomende sekuriteit vereis"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Wagwoord word vir bykomende sekuriteit vereis"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Toestel is deur administrateur gesluit"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Toestel is handmatig gesluit"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nie herken nie"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Verstek"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Borrel"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analoog"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-am/strings.xml b/packages/SystemUI/res-keyguard/values-am/strings.xml index be52c448181d..533e5a299e4c 100644 --- a/packages/SystemUI/res-keyguard/values-am/strings.xml +++ b/packages/SystemUI/res-keyguard/values-am/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"መሣሪያ ዳግም ከጀመረ በኋላ ሥርዓተ ጥለት ያስፈልጋል"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"መሣሪያ ዳግም ከተነሳ በኋላ ፒን ያስፈልጋል"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"መሣሪያ ዳግም ከጀመረ በኋላ የይለፍ ቃል ያስፈልጋል"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ሥርዓተ ጥለት ለተጨማሪ ደህንነት ያስፈልጋል"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ፒን ለተጨማሪ ደህንነት ያስፈልጋል"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"የይለፍ ቃል ለተጨማሪ ደህንነት ያስፈልጋል"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"መሣሪያ በአስተዳዳሪ ተቆልፏል"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"መሣሪያ በተጠቃሚው ራሱ ተቆልፏል"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"አልታወቀም"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"ነባሪ"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"አረፋ"</string> <string name="clock_title_analog" msgid="8409262532900918273">"አናሎግ"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ar/strings.xml b/packages/SystemUI/res-keyguard/values-ar/strings.xml index adb57b6d4d67..81ce7d3c9361 100644 --- a/packages/SystemUI/res-keyguard/values-ar/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ar/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"يجب رسم النقش بعد إعادة تشغيل الجهاز"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"يجب إدخال رقم التعريف الشخصي بعد إعادة تشغيل الجهاز"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"يجب إدخال كلمة المرور بعد إعادة تشغيل الجهاز"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"يجب رسم النقش لمزيد من الأمان"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"يجب إدخال رقم التعريف الشخصي لمزيد من الأمان"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"يجب إدخال كلمة المرور لمزيد من الأمان"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"اختار المشرف قفل الجهاز"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"تم حظر الجهاز يدويًا"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"لم يتم التعرّف عليه."</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"تلقائي"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"فقاعة"</string> <string name="clock_title_analog" msgid="8409262532900918273">"ساعة تقليدية"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-as/strings.xml b/packages/SystemUI/res-keyguard/values-as/strings.xml index cbfb325fa9b3..443f666c0faa 100644 --- a/packages/SystemUI/res-keyguard/values-as/strings.xml +++ b/packages/SystemUI/res-keyguard/values-as/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পাছত আৰ্হি দিয়াটো বাধ্যতামূলক"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পাছত পিন দিয়াটো বাধ্যতামূলক"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পাছত পাছৱৰ্ড দিয়াটো বাধ্যতামূলক"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"অতিৰিক্ত সুৰক্ষাৰ বাবে আর্হি দিয়াটো বাধ্যতামূলক"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"অতিৰিক্ত সুৰক্ষাৰ বাবে পিন দিয়াটো বাধ্যতামূলক"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"অতিৰিক্ত সুৰক্ষাৰ বাবে পাছৱর্ড দিয়াটো বাধ্যতামূলক"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"প্ৰশাসকে ডিভাইচ লক কৰি ৰাখিছে"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ডিভাইচটো মেনুৱেলভাৱে লক কৰা হৈছিল"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"চিনাক্ত কৰিব পৰা নাই"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"ডিফ’ল্ট"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"বাবল"</string> <string name="clock_title_analog" msgid="8409262532900918273">"এনাল’গ"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-az/strings.xml b/packages/SystemUI/res-keyguard/values-az/strings.xml index 6ec1061ac8bb..e12569715eca 100644 --- a/packages/SystemUI/res-keyguard/values-az/strings.xml +++ b/packages/SystemUI/res-keyguard/values-az/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Cihaz yenidən başladıqdan sonra model tələb olunur"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Cihaz yeniden başladıqdan sonra PIN tələb olunur"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Cihaz yeniden başladıqdan sonra parol tələb olunur"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Əlavə təhlükəsizlik üçün model tələb olunur"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Əlavə təhlükəsizlik üçün PIN tələb olunur"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Əlavə təhlükəsizlik üçün parol tələb olunur"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Cihaz admin tərəfindən kilidlənib"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Cihaz əl ilə kilidləndi"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tanınmır"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Defolt"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Qabarcıq"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analoq"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml b/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml index 13d6613f0ff4..f0d1ef2d6bc3 100644 --- a/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml +++ b/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Treba da unesete šablon kada se uređaj ponovo pokrene"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Treba da unesete PIN kada se uređaj ponovo pokrene"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Treba da unesete lozinku kada se uređaj ponovo pokrene"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Treba da unesete šablon radi dodatne bezbednosti"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Treba da unesete PIN radi dodatne bezbednosti"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Treba da unesete lozinku radi dodatne bezbednosti"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administrator je zaključao uređaj"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Uređaj je ručno zaključan"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nije prepoznat"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Podrazumevani"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Mehurići"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogni"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-be/strings.xml b/packages/SystemUI/res-keyguard/values-be/strings.xml index 616d31ac6bee..e1af3eceada8 100644 --- a/packages/SystemUI/res-keyguard/values-be/strings.xml +++ b/packages/SystemUI/res-keyguard/values-be/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Пасля перазапуску прылады патрабуецца ўзор"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Пасля перазапуску прылады патрабуецца PIN-код"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Пасля перазапуску прылады патрабуецца пароль"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Для забеспячэння дадатковай бяспекі патрабуецца ўзор"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Для забеспячэння дадатковай бяспекі патрабуецца PIN-код"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Для забеспячэння дадатковай бяспекі патрабуецца пароль"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Прылада заблакіравана адміністратарам"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Прылада была заблакіравана ўручную"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не распазнана"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Стандартны"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Бурбалкі"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Са стрэлкамі"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-bg/strings.xml b/packages/SystemUI/res-keyguard/values-bg/strings.xml index 366a7f4cb817..0b4417ac30e8 100644 --- a/packages/SystemUI/res-keyguard/values-bg/strings.xml +++ b/packages/SystemUI/res-keyguard/values-bg/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"След рестартиране на устройството се изисква фигура"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"След рестартиране на устройството се изисква ПИН код"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"След рестартиране на устройството се изисква парола"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"За допълнителна сигурност се изисква фигура"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"За допълнителна сигурност се изисква ПИН код"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"За допълнителна сигурност се изисква парола"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Устройството е заключено от администратора"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Устройството бе заключено ръчно"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не е разпознато"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Стандартен"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Балонен"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Аналогов"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-bn/strings.xml b/packages/SystemUI/res-keyguard/values-bn/strings.xml index c20be5d70bc7..485157984749 100644 --- a/packages/SystemUI/res-keyguard/values-bn/strings.xml +++ b/packages/SystemUI/res-keyguard/values-bn/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ডিভাইসটি পুনরায় চালু হওয়ার পর প্যাটার্নের প্রয়োজন হবে"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ডিভাইসটি পুনরায় চালু হওয়ার পর পিন প্রয়োজন হবে"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ডিভাইসটি পুনরায় চালু হওয়ার পর পাসওয়ার্ডের প্রয়োজন হবে"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"অতিরিক্ত সুরক্ষার জন্য প্যাটার্ন দেওয়া প্রয়োজন"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"অতিরিক্ত সুরক্ষার জন্য পিন দেওয়া প্রয়োজন"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"অতিরিক্ত সুরক্ষার জন্য পাসওয়ার্ড দেওয়া প্রয়োজন"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"প্রশাসক ডিভাইসটি লক করেছেন"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ডিভাইসটিকে ম্যানুয়ালি লক করা হয়েছে"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"শনাক্ত করা যায়নি"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"ডিফল্ট"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"বাবল"</string> <string name="clock_title_analog" msgid="8409262532900918273">"অ্যানালগ"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-bs/strings.xml b/packages/SystemUI/res-keyguard/values-bs/strings.xml index f1c00a91a734..4705b4d9645f 100644 --- a/packages/SystemUI/res-keyguard/values-bs/strings.xml +++ b/packages/SystemUI/res-keyguard/values-bs/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Potreban je uzorak nakon što se uređaj ponovo pokrene"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Potreban je PIN nakon što se uređaj ponovo pokrene"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Potrebna je lozinka nakon što se uređaj ponovo pokrene"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Uzorak je potreban radi dodatne sigurnosti"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN je potreban radi dodatne sigurnosti"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Lozinka je potrebna radi dodatne sigurnosti"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Uređaj je zaključao administrator"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Uređaj je ručno zaključan"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nije prepoznato"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Zadano"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Mjehurići"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogni"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ca/strings.xml b/packages/SystemUI/res-keyguard/values-ca/strings.xml index 709407c128e0..284eaebfbd8e 100644 --- a/packages/SystemUI/res-keyguard/values-ca/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ca/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Cal introduir el patró quan es reinicia el dispositiu"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Cal introduir el PIN quan es reinicia el dispositiu"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Cal introduir la contrasenya quan es reinicia el dispositiu"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Cal introduir el patró per disposar de més seguretat"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Cal introduir el PIN per disposar de més seguretat"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Cal introduir la contrasenya per disposar de més seguretat"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"L\'administrador ha bloquejat el dispositiu"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositiu s\'ha bloquejat manualment"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"No s\'ha reconegut"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Predeterminada"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bombolla"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analògica"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-cs/strings.xml b/packages/SystemUI/res-keyguard/values-cs/strings.xml index a44658cc1de9..6b4f60742964 100644 --- a/packages/SystemUI/res-keyguard/values-cs/strings.xml +++ b/packages/SystemUI/res-keyguard/values-cs/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po restartování zařízení je vyžadováno gesto"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po restartování zařízení je vyžadován kód PIN"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po restartování zařízení je vyžadováno heslo"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pro ještě lepší zabezpečení je vyžadováno gesto"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Pro ještě lepší zabezpečení je vyžadován kód PIN"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Pro ještě lepší zabezpečení je vyžadováno heslo"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Zařízení je uzamknuto administrátorem"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Zařízení bylo ručně uzamčeno"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nerozpoznáno"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Výchozí"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bublina"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogové"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-da/strings.xml b/packages/SystemUI/res-keyguard/values-da/strings.xml index 331c35579c7b..85238dfdab0f 100644 --- a/packages/SystemUI/res-keyguard/values-da/strings.xml +++ b/packages/SystemUI/res-keyguard/values-da/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Du skal angive et mønster, når du har genstartet enheden"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Der skal angives en pinkode efter genstart af enheden"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Der skal angives en adgangskode efter genstart af enheden"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Der kræves et mønster som ekstra beskyttelse"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Der kræves en pinkode som ekstra beskyttelse"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Der kræves en adgangskode som ekstra beskyttelse"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Enheden er blevet låst af administratoren"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Enheden blev låst manuelt"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ikke genkendt"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Boble"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-de/strings.xml b/packages/SystemUI/res-keyguard/values-de/strings.xml index c19b357b5985..18befed429a9 100644 --- a/packages/SystemUI/res-keyguard/values-de/strings.xml +++ b/packages/SystemUI/res-keyguard/values-de/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Nach dem Neustart des Geräts ist die Eingabe des Musters erforderlich"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Nach dem Neustart des Geräts ist die Eingabe der PIN erforderlich"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Nach dem Neustart des Geräts ist die Eingabe des Passworts erforderlich"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Zur Verbesserung der Sicherheit ist ein Muster erforderlich"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Zur Verbesserung der Sicherheit ist eine PIN erforderlich"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Zur Verbesserung der Sicherheit ist ein Passwort erforderlich"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Gerät vom Administrator gesperrt"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Gerät manuell gesperrt"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nicht erkannt"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-el/strings.xml b/packages/SystemUI/res-keyguard/values-el/strings.xml index 1d6ec8240a9f..65b844862a9f 100644 --- a/packages/SystemUI/res-keyguard/values-el/strings.xml +++ b/packages/SystemUI/res-keyguard/values-el/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Απαιτείται μοτίβο μετά από την επανεκκίνηση της συσκευής"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Απαιτείται PIN μετά από την επανεκκίνηση της συσκευής"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Απαιτείται κωδικός πρόσβασης μετά από την επανεκκίνηση της συσκευής"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Απαιτείται μοτίβο για πρόσθετη ασφάλεια"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Απαιτείται PIN για πρόσθετη ασφάλεια"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Απαιτείται κωδικός πρόσβασης για πρόσθετη ασφάλεια"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Η συσκευή κλειδώθηκε από τον διαχειριστή"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Η συσκευή κλειδώθηκε με μη αυτόματο τρόπο"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Δεν αναγνωρίστηκε"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Προεπιλογή"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Συννεφάκι"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Αναλογικό"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml b/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml index 2b78f9678415..588f1b501f53 100644 --- a/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml +++ b/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Default"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml b/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml index e1c253202c60..08fc8d66c98d 100644 --- a/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml +++ b/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Default"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml b/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml index 2b78f9678415..588f1b501f53 100644 --- a/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml +++ b/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Default"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml b/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml index 2b78f9678415..588f1b501f53 100644 --- a/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml +++ b/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Default"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml b/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml index 9052e4f04e54..a23aeb0822f3 100644 --- a/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml +++ b/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"For additional security, use pattern instead"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"For additional security, use PIN instead"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"For additional security, use password instead"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognized"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"Default"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Unlock your device to continue"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml b/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml index 9dc054a53393..c71a67865925 100644 --- a/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml +++ b/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Se requiere el patrón después de reiniciar el dispositivo"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Se requiere el PIN después de reiniciar el dispositivo"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Se requiere la contraseña después de reiniciar el dispositivo"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Se requiere el patrón por razones de seguridad"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Se requiere el PIN por razones de seguridad"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Se requiere la contraseña por razones de seguridad"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado por el administrador"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositivo se bloqueó de forma manual"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"No se reconoció"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Predeterminado"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Burbuja"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-es/strings.xml b/packages/SystemUI/res-keyguard/values-es/strings.xml index f9f0452771af..c6ee6980ff3b 100644 --- a/packages/SystemUI/res-keyguard/values-es/strings.xml +++ b/packages/SystemUI/res-keyguard/values-es/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Debes introducir el patrón después de reiniciar el dispositivo"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Debes introducir el PIN después de reiniciar el dispositivo"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Debes introducir la contraseña después de reiniciar el dispositivo"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Debes introducir el patrón como medida de seguridad adicional"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Debes introducir el PIN como medida de seguridad adicional"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Debes introducir la contraseña como medida de seguridad adicional"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado por el administrador"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositivo se ha bloqueado manualmente"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"No se reconoce"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Predeterminado"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Burbuja"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-et/strings.xml b/packages/SystemUI/res-keyguard/values-et/strings.xml index dceb78efaca7..071ede8a0b25 100644 --- a/packages/SystemUI/res-keyguard/values-et/strings.xml +++ b/packages/SystemUI/res-keyguard/values-et/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pärast seadme taaskäivitamist tuleb sisestada muster"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pärast seadme taaskäivitamist tuleb sisestada PIN-kood"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Pärast seadme taaskäivitamist tuleb sisestada parool"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Lisaturvalisuse huvides tuleb sisestada muster"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Lisaturvalisuse huvides tuleb sisestada PIN-kood"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Lisaturvalisuse huvides tuleb sisestada parool"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administraator lukustas seadme"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Seade lukustati käsitsi"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ei tuvastatud"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Vaikenumbrilaud"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Mull"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analoog"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-eu/strings.xml b/packages/SystemUI/res-keyguard/values-eu/strings.xml index 8431268464eb..9b8e65b1dde7 100644 --- a/packages/SystemUI/res-keyguard/values-eu/strings.xml +++ b/packages/SystemUI/res-keyguard/values-eu/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Eredua marraztu beharko duzu gailua berrabiarazten denean"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PINa idatzi beharko duzu gailua berrabiarazten denean"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Pasahitza idatzi beharko duzu gailua berrabiarazten denean"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Eredua behar da gailua babestuago izateko"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PINa behar da gailua babestuago izateko"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Pasahitza behar da gailua babestuago izateko"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administratzaileak blokeatu egin du gailua"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Eskuz blokeatu da gailua"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ez da ezagutu"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Lehenetsia"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Puxikak"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogikoa"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-fa/strings.xml b/packages/SystemUI/res-keyguard/values-fa/strings.xml index 37bb260b28d3..3583f1e60e7a 100644 --- a/packages/SystemUI/res-keyguard/values-fa/strings.xml +++ b/packages/SystemUI/res-keyguard/values-fa/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"بعد از بازنشانی دستگاه باید الگو وارد شود"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"بعد از بازنشانی دستگاه باید پین وارد شود"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"بعد از بازنشانی دستگاه باید گذرواژه وارد شود"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"برای ایمنی بیشتر باید الگو وارد شود"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"برای ایمنی بیشتر باید پین وارد شود"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"برای ایمنی بیشتر باید گذرواژه وارد شود"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"دستگاه توسط سرپرست سیستم قفل شده است"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"دستگاه بهصورت دستی قفل شده است"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"شناسایی نشد"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"پیشفرض"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"حباب"</string> <string name="clock_title_analog" msgid="8409262532900918273">"آنالوگ"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-fi/strings.xml b/packages/SystemUI/res-keyguard/values-fi/strings.xml index f8cec421844c..a0ac6df2f029 100644 --- a/packages/SystemUI/res-keyguard/values-fi/strings.xml +++ b/packages/SystemUI/res-keyguard/values-fi/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Kuvio vaaditaan laitteen uudelleenkäynnistyksen jälkeen."</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN-koodi vaaditaan laitteen uudelleenkäynnistyksen jälkeen."</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Salasana vaaditaan laitteen uudelleenkäynnistyksen jälkeen."</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kuvio vaaditaan suojauksen parantamiseksi."</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN-koodi vaaditaan suojauksen parantamiseksi."</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Salasana vaaditaan suojauksen parantamiseksi."</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Järjestelmänvalvoja lukitsi laitteen."</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Laite lukittiin manuaalisesti"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ei tunnistettu"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Oletus"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Kupla"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analoginen"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml b/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml index 077fe11be4f2..66fd7c08f0e2 100644 --- a/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml +++ b/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Le schéma est exigé après le redémarrage de l\'appareil"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Le NIP est exigé après le redémarrage de l\'appareil"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Le mot de passe est exigé après le redémarrage de l\'appareil"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Le schéma est exigé pour plus de sécurité"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Le NIP est exigé pour plus de sécurité"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Le mot de passe est exigé pour plus de sécurité"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Pour plus de sécurité, utilisez plutôt un schéma"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Pour plus de sécurité, utilisez plutôt un NIP"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Pour plus de sécurité, utilisez plutôt un mot de passe"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"L\'appareil a été verrouillé par l\'administrateur"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"L\'appareil a été verrouillé manuellement"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Doigt non reconnu"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"Par défaut"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bulle"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogique"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Déverrouiller votre appareil pour continuer"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-fr/strings.xml b/packages/SystemUI/res-keyguard/values-fr/strings.xml index 45dadc1dfdc4..ec00ba3ae887 100644 --- a/packages/SystemUI/res-keyguard/values-fr/strings.xml +++ b/packages/SystemUI/res-keyguard/values-fr/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Veuillez dessiner le schéma après le redémarrage de l\'appareil"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Veuillez saisir le code après le redémarrage de l\'appareil"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Veuillez saisir le mot de passe après le redémarrage de l\'appareil"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Veuillez dessiner le schéma pour renforcer la sécurité"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Veuillez saisir le code pour renforcer la sécurité"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Veuillez saisir le mot de passe pour renforcer la sécurité"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Appareil verrouillé par l\'administrateur"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Appareil verrouillé manuellement"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Non reconnu"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Par défaut"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bulle"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogique"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-gl/strings.xml b/packages/SystemUI/res-keyguard/values-gl/strings.xml index 4fbdd676d23a..a3f8e86cd5e6 100644 --- a/packages/SystemUI/res-keyguard/values-gl/strings.xml +++ b/packages/SystemUI/res-keyguard/values-gl/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"É necesario o padrón despois do reinicio do dispositivo"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"É necesario o PIN despois do reinicio do dispositivo"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"É necesario o contrasinal despois do reinicio do dispositivo"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"É necesario o padrón para obter seguranza adicional"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"É necesario o PIN para obter seguranza adicional"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"É necesario o contrasinal para obter seguranza adicional"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"O administrador bloqueou o dispositivo"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo bloqueouse manualmente"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Non se recoñeceu"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Predeterminado"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Burbulla"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analóxico"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-gu/strings.xml b/packages/SystemUI/res-keyguard/values-gu/strings.xml index 6caac8a89a66..c97fe017ec60 100644 --- a/packages/SystemUI/res-keyguard/values-gu/strings.xml +++ b/packages/SystemUI/res-keyguard/values-gu/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ઉપકરણનો પુનઃપ્રારંભ થાય તે પછી પૅટર્ન જરૂરી છે"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ઉપકરણનો પુનઃપ્રારંભ થાય તે પછી પિન જરૂરી છે"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ઉપકરણનો પુનઃપ્રારંભ થાય તે પછી પાસવર્ડ જરૂરી છે"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"વધારાની સુરક્ષા માટે પૅટર્ન જરૂરી છે"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"વધારાની સુરક્ષા માટે પિન જરૂરી છે"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"વધારાની સુરક્ષા માટે પાસવર્ડ જરૂરી છે"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"વ્યવસ્થાપકે ઉપકરણ લૉક કરેલું છે"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ઉપકરણ મેન્યુઅલી લૉક કર્યું હતું"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"ઓળખાયેલ નથી"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"ડિફૉલ્ટ"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"બબલ"</string> <string name="clock_title_analog" msgid="8409262532900918273">"એનાલોગ"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-hi/strings.xml b/packages/SystemUI/res-keyguard/values-hi/strings.xml index 627576e1af66..128300488f27 100644 --- a/packages/SystemUI/res-keyguard/values-hi/strings.xml +++ b/packages/SystemUI/res-keyguard/values-hi/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"डिवाइस फिर से चालू होने के बाद पैटर्न ज़रूरी है"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"डिवाइस फिर से चालू होने के बाद पिन ज़रूरी है"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"डिवाइस फिर से चालू होने के बाद पासवर्ड ज़रूरी है"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"अतिरिक्त सुरक्षा के लिए पैटर्न ज़रूरी है"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"अतिरिक्त सुरक्षा के लिए पिन ज़रूरी है"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"अतिरिक्त सुरक्षा के लिए पासवर्ड ज़रूरी है"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"व्यवस्थापक ने डिवाइस को लॉक किया है"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"डिवाइस को मैन्युअल रूप से लॉक किया गया था"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"पहचान नहीं हो पाई"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"डिफ़ॉल्ट"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"बबल"</string> <string name="clock_title_analog" msgid="8409262532900918273">"एनालॉग"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-hr/strings.xml b/packages/SystemUI/res-keyguard/values-hr/strings.xml index 8b1b5042f7f6..7a14a80e9bb3 100644 --- a/packages/SystemUI/res-keyguard/values-hr/strings.xml +++ b/packages/SystemUI/res-keyguard/values-hr/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Nakon ponovnog pokretanja uređaja morate unijeti uzorak"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Nakon ponovnog pokretanja uređaja morate unijeti PIN"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Nakon ponovnog pokretanja uređaja morate unijeti zaporku"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Unesite uzorak radi dodatne sigurnosti"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Unesite PIN radi dodatne sigurnosti"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Unesite zaporku radi dodatne sigurnosti"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administrator je zaključao uređaj"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Uređaj je ručno zaključan"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nije prepoznato"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Zadano"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Mjehurić"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogni"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-hu/strings.xml b/packages/SystemUI/res-keyguard/values-hu/strings.xml index 6b75e722dbfc..a4fbf537d331 100644 --- a/packages/SystemUI/res-keyguard/values-hu/strings.xml +++ b/packages/SystemUI/res-keyguard/values-hu/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Az eszköz újraindítását követően meg kell adni a mintát"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Az eszköz újraindítását követően meg kell adni a PIN-kódot"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Az eszköz újraindítását követően meg kell adni a jelszót"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"A nagyobb biztonság érdekében minta szükséges"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"A nagyobb biztonság érdekében PIN-kód szükséges"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"A nagyobb biztonság érdekében jelszó szükséges"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"A rendszergazda zárolta az eszközt"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Az eszközt manuálisan lezárták"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nem ismerhető fel"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Alapértelmezett"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Buborék"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analóg"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-hy/strings.xml b/packages/SystemUI/res-keyguard/values-hy/strings.xml index 3412026b90f3..086eeb939941 100644 --- a/packages/SystemUI/res-keyguard/values-hy/strings.xml +++ b/packages/SystemUI/res-keyguard/values-hy/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Սարքը վերագործարկելուց հետո անհրաժեշտ է մուտքագրել նախշը"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Սարքը վերագործարկելուց հետո անհրաժեշտ է մուտքագրել PIN կոդը"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Սարքը վերագործարկելուց հետո անհրաժեշտ է մուտքագրել գաղտնաբառը"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Անվտանգության նկատառումներից ելնելով անհրաժեշտ է մուտքագրել նախշը"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Անվտանգության նկատառումներից ելնելով անհրաժեշտ է մուտքագրել PIN կոդը"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Անվտանգության նկատառումներից ելնելով անհրաժեշտ է մուտքագրել գաղտնաբառը"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Սարքը կողպված է ադմինիստրատորի կողմից"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Սարքը կողպվել է ձեռքով"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Չհաջողվեց ճանաչել"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Կանխադրված"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Պղպջակ"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Անալոգային"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-in/strings.xml b/packages/SystemUI/res-keyguard/values-in/strings.xml index 1afb7918dd83..b43a0322fc7a 100644 --- a/packages/SystemUI/res-keyguard/values-in/strings.xml +++ b/packages/SystemUI/res-keyguard/values-in/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pola diperlukan setelah perangkat dimulai ulang"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN diperlukan setelah perangkat dimulai ulang"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Sandi diperlukan setelah perangkat dimulai ulang"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pola diperlukan untuk keamanan tambahan"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN diperlukan untuk keamanan tambahan"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Sandi diperlukan untuk keamanan tambahan"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Perangkat dikunci oleh admin"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Perangkat dikunci secara manual"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tidak dikenali"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Default"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Balon"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-is/strings.xml b/packages/SystemUI/res-keyguard/values-is/strings.xml index 6abdc82ceecd..8bad961dae4b 100644 --- a/packages/SystemUI/res-keyguard/values-is/strings.xml +++ b/packages/SystemUI/res-keyguard/values-is/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Mynsturs er krafist þegar tækið er endurræst"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN-númers er krafist þegar tækið er endurræst"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Aðgangsorðs er krafist þegar tækið er endurræst"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Mynsturs er krafist af öryggisástæðum"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN-númers er krafist af öryggisástæðum"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Aðgangsorðs er krafist af öryggisástæðum"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Kerfisstjóri læsti tæki"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Tækinu var læst handvirkt"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Þekktist ekki"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Sjálfgefið"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Blaðra"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Með vísum"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-it/strings.xml b/packages/SystemUI/res-keyguard/values-it/strings.xml index 9fed5f72afa1..186177ff6d49 100644 --- a/packages/SystemUI/res-keyguard/values-it/strings.xml +++ b/packages/SystemUI/res-keyguard/values-it/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Sequenza obbligatoria dopo il riavvio del dispositivo"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN obbligatorio dopo il riavvio del dispositivo"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password obbligatoria dopo il riavvio del dispositivo"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Sequenza obbligatoria per maggiore sicurezza"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN obbligatorio per maggiore sicurezza"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password obbligatoria per maggiore sicurezza"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloccato dall\'amministratore"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Il dispositivo è stato bloccato manualmente"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Non riconosciuto"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Predefinito"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bolla"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogico"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-iw/strings.xml b/packages/SystemUI/res-keyguard/values-iw/strings.xml index b5b1c533778b..aab42069590b 100644 --- a/packages/SystemUI/res-keyguard/values-iw/strings.xml +++ b/packages/SystemUI/res-keyguard/values-iw/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"יש להזין את קו ביטול הנעילה לאחר הפעלה מחדש של המכשיר"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"צריך להזין קוד אימות לאחר הפעלה מחדש של המכשיר"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"יש להזין סיסמה לאחר הפעלה מחדש של המכשיר"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"יש להזין את קו ביטול הנעילה כדי להגביר את רמת האבטחה"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"יש להזין קוד אימות כדי להגביר את רמת האבטחה"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"יש להזין סיסמה כדי להגביר את רמת האבטחה"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"כדי להגביר את רמת האבטחה, כדאי להשתמש בקו ביטול נעילה במקום זאת"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"כדי להגביר את רמת האבטחה, כדאי להשתמש בקוד אימות במקום זאת"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"כדי להגביר את רמת האבטחה, כדאי להשתמש בסיסמה במקום זאת"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"המנהל של המכשיר נהל אותו"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"המכשיר ננעל באופן ידני"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"לא זוהתה"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"ברירת מחדל"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"בועה"</string> <string name="clock_title_analog" msgid="8409262532900918273">"אנלוגי"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"צריך לבטל את הנעילה של המכשיר כדי להמשיך"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ja/strings.xml b/packages/SystemUI/res-keyguard/values-ja/strings.xml index afe0159ca1d6..1a4fb0b8e243 100644 --- a/packages/SystemUI/res-keyguard/values-ja/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ja/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"デバイスの再起動後はパターンの入力が必要となります"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"デバイスの再起動後は PIN の入力が必要となります"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"デバイスの再起動後はパスワードの入力が必要となります"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"追加の確認のためパターンが必要です"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"追加の確認のため PIN が必要です"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"追加の確認のためパスワードが必要です"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"セキュリティを強化するには代わりにパターンを使用してください"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"セキュリティを強化するには代わりに PIN を使用してください"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"セキュリティを強化するには代わりにパスワードを使用してください"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"デバイスは管理者によりロックされています"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"デバイスは手動でロックされました"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"認識されませんでした"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"デフォルト"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"バブル"</string> <string name="clock_title_analog" msgid="8409262532900918273">"アナログ"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"続行するにはデバイスのロックを解除してください"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ka/strings.xml b/packages/SystemUI/res-keyguard/values-ka/strings.xml index b32caa75f453..123cc39e47da 100644 --- a/packages/SystemUI/res-keyguard/values-ka/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ka/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"მოწყობილობის გადატვირთვის შემდეგ საჭიროა ნიმუშის დახატვა"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"მოწყობილობის გადატვირთვის შემდეგ საჭიროა PIN-კოდის შეყვანა"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"მოწყობილობის გადატვირთვის შემდეგ საჭიროა პაროლის შეყვანა"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"დამატებითი უსაფრთხოებისთვის საჭიროა ნიმუშის დახატვა"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"დამატებითი უსაფრთხოებისთვის საჭიროა PIN-კოდის შეყვანა"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"დამატებითი უსაფრთხოებისთვის საჭიროა პაროლის შეყვანა"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"მოწყობილობა ჩაკეტილია ადმინისტრატორის მიერ"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"მოწყობილობა ხელით ჩაიკეტა"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"არ არის ამოცნობილი"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"ნაგულისხმევი"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"ბუშტი"</string> <string name="clock_title_analog" msgid="8409262532900918273">"ანალოგური"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-kk/strings.xml b/packages/SystemUI/res-keyguard/values-kk/strings.xml index d6d5bcdd4beb..8daca5c6d59a 100644 --- a/packages/SystemUI/res-keyguard/values-kk/strings.xml +++ b/packages/SystemUI/res-keyguard/values-kk/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Құрылғы қайта іске қосылғаннан кейін, өрнекті енгізу қажет"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Құрылғы қайта іске қосылғаннан кейін, PIN кодын енгізу қажет"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Құрылғы қайта іске қосылғаннан кейін, құпия сөзді енгізу қажет"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Қауіпсіздікті күшейту үшін өрнекті енгізу қажет"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Қауіпсіздікті күшейту үшін PIN кодын енгізу қажет"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Қауіпсіздікті күшейту үшін құпия сөзді енгізу қажет"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Құрылғыны әкімші құлыптаған"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Құрылғы қолмен құлыпталды"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Танылмады"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Әдепкі"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Көпіршік"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Аналогтық"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-km/strings.xml b/packages/SystemUI/res-keyguard/values-km/strings.xml index 00bfe05ada14..73f507c91ece 100644 --- a/packages/SystemUI/res-keyguard/values-km/strings.xml +++ b/packages/SystemUI/res-keyguard/values-km/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"តម្រូវឲ្យប្រើលំនាំ បន្ទាប់ពីឧបករណ៍ចាប់ផ្តើមឡើងវិញ"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"តម្រូវឲ្យបញ្ចូលកូដ PIN បន្ទាប់ពីឧបករណ៍ចាប់ផ្តើមឡើងវិញ"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"តម្រូវឲ្យបញ្ចូលពាក្យសម្ងាត់ បន្ទាប់ពីឧបករណ៍ចាប់ផ្តើមឡើងវិញ"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"តម្រូវឲ្យប្រើលំនាំ ដើម្បីទទួលបានសវុត្ថិភាពបន្ថែម"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"តម្រូវឲ្យបញ្ចូលកូដ PIN ដើម្បីទទួលបានសុវត្ថិភាពបន្ថែម"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"តម្រូវឲ្យបញ្ចូលពាក្យសម្ងាត់ ដើម្បីទទួលបានសុវត្ថិភាពបន្ថែម"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ឧបករណ៍ត្រូវបានចាក់សោដោយអ្នកគ្រប់គ្រង"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ឧបករណ៍ត្រូវបានចាក់សោដោយអ្នកប្រើផ្ទាល់"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"មិនអាចសម្គាល់បានទេ"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"លំនាំដើម"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"ពពុះ"</string> <string name="clock_title_analog" msgid="8409262532900918273">"អាណាឡូក"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-kn/strings.xml b/packages/SystemUI/res-keyguard/values-kn/strings.xml index 80a98e6ff6d7..c279ceac244e 100644 --- a/packages/SystemUI/res-keyguard/values-kn/strings.xml +++ b/packages/SystemUI/res-keyguard/values-kn/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ಸಾಧನ ಮರುಪ್ರಾರಂಭಗೊಂಡ ನಂತರ ಪ್ಯಾಟರ್ನ್ ಅಗತ್ಯವಿರುತ್ತದೆ"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ಸಾಧನ ಮರುಪ್ರಾರಂಭಗೊಂಡ ನಂತರ ಪಿನ್ ಅಗತ್ಯವಿರುತ್ತದೆ"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ಸಾಧನ ಮರುಪ್ರಾರಂಭಗೊಂಡ ನಂತರ ಪಾಸ್ವರ್ಡ್ ಅಗತ್ಯವಿರುತ್ತದೆ"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗೆ ಪ್ಯಾಟರ್ನ್ ಅಗತ್ಯವಿದೆ"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗೆ ಪಿನ್ ಅಗತ್ಯವಿದೆ"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗಾಗಿ ಪಾಸ್ವರ್ಡ್ ಅಗತ್ಯವಿದೆ"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ನಿರ್ವಾಹಕರು ಸಾಧನವನ್ನು ಲಾಕ್ ಮಾಡಿದ್ದಾರೆ"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ಸಾಧನವನ್ನು ಹಸ್ತಚಾಲಿತವಾಗಿ ಲಾಕ್ ಮಾಡಲಾಗಿದೆ"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"ಗುರುತಿಸಲಾಗಿಲ್ಲ"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"ಡೀಫಾಲ್ಟ್"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"ಬಬಲ್"</string> <string name="clock_title_analog" msgid="8409262532900918273">"ಅನಲಾಗ್"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ko/strings.xml b/packages/SystemUI/res-keyguard/values-ko/strings.xml index b952f1bba2dd..4c058edbf688 100644 --- a/packages/SystemUI/res-keyguard/values-ko/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ko/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"기기가 다시 시작되면 패턴이 필요합니다."</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"기기가 다시 시작되면 PIN이 필요합니다."</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"기기가 다시 시작되면 비밀번호가 필요합니다."</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"보안 강화를 위해 패턴이 필요합니다."</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"보안 강화를 위해 PIN이 필요합니다."</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"보안 강화를 위해 비밀번호가 필요합니다."</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"관리자가 기기를 잠갔습니다."</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"기기가 수동으로 잠금 설정되었습니다."</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"인식할 수 없음"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"기본"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"버블"</string> <string name="clock_title_analog" msgid="8409262532900918273">"아날로그"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ky/strings.xml b/packages/SystemUI/res-keyguard/values-ky/strings.xml index 485337d86408..7c7099e1cf61 100644 --- a/packages/SystemUI/res-keyguard/values-ky/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ky/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Түзмөк кайра күйгүзүлгөндөн кийин графикалык ачкычты тартуу талап кылынат"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Түзмөк кайра күйгүзүлгөндөн кийин PIN-кодду киргизүү талап кылынат"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Түзмөк кайра күйгүзүлгөндөн кийин сырсөздү киргизүү талап кылынат"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Коопсуздукту бекемдөө үчүн графикалык ачкыч талап кылынат"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Коопсуздукту бекемдөө үчүн PIN-код талап кылынат"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Коопсуздукту бекемдөө үчүн сырсөз талап кылынат"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Түзмөктү администратор кулпулап койгон"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Түзмөк кол менен кулпуланды"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Таанылган жок"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Демейки"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Көбүк"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Аналог"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-lo/strings.xml b/packages/SystemUI/res-keyguard/values-lo/strings.xml index 17584b5a74fe..5a6df42884b4 100644 --- a/packages/SystemUI/res-keyguard/values-lo/strings.xml +++ b/packages/SystemUI/res-keyguard/values-lo/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ຈຳເປັນຕ້ອງມີແບບຮູບປົດລັອກຫຼັງຈາກອຸປະກອນເລີ່ມລະບົບໃໝ່"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ຈຳເປັນຕ້ອງມີ PIN ຫຼັງຈາກອຸປະກອນເລີ່ມລະບົບໃໝ່"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ຈຳເປັນຕ້ອງມີລະຫັດຜ່ານຫຼັງຈາກອຸປະກອນເລີ່ມລະບົບໃໝ່"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ຈຳເປັນຕ້ອງມີແບບຮູບເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ຈຳເປັນຕ້ອງມີ PIN ເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ຈຳເປັນຕ້ອງມີລະຫັດຜ່ານເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ອຸປະກອນຖືກລັອກໂດຍຜູ້ເບິ່ງແຍງລະບົບ"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ອຸປະກອນຖືກສັ່ງໃຫ້ລັອກ"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"ບໍ່ຮູ້ຈັກ"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"ຄ່າເລີ່ມຕົ້ນ"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"ຟອງ"</string> <string name="clock_title_analog" msgid="8409262532900918273">"ໂມງເຂັມ"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-lt/strings.xml b/packages/SystemUI/res-keyguard/values-lt/strings.xml index a066a66ec6a4..4d98fd17baf8 100644 --- a/packages/SystemUI/res-keyguard/values-lt/strings.xml +++ b/packages/SystemUI/res-keyguard/values-lt/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Iš naujo paleidus įrenginį būtinas atrakinimo piešinys"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Iš naujo paleidus įrenginį būtinas PIN kodas"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Iš naujo paleidus įrenginį būtinas slaptažodis"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Norint užtikrinti papildomą saugą būtinas atrakinimo piešinys"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Norint užtikrinti papildomą saugą būtinas PIN kodas"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Norint užtikrinti papildomą saugą būtinas slaptažodis"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Įrenginį užrakino administratorius"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Įrenginys užrakintas neautomatiškai"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Neatpažinta"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Numatytasis"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Debesėlis"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analoginis"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-lv/strings.xml b/packages/SystemUI/res-keyguard/values-lv/strings.xml index d371a4b9cbab..2660a069c949 100644 --- a/packages/SystemUI/res-keyguard/values-lv/strings.xml +++ b/packages/SystemUI/res-keyguard/values-lv/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pēc ierīces restartēšanas ir jāievada atbloķēšanas kombinācija."</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pēc ierīces restartēšanas ir jāievada PIN kods."</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Pēc ierīces restartēšanas ir jāievada parole."</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Papildu drošībai ir jāievada atbloķēšanas kombinācija."</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Papildu drošībai ir jāievada PIN kods."</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Papildu drošībai ir jāievada parole."</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administrators bloķēja ierīci."</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Ierīce tika bloķēta manuāli."</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nav atpazīts"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Noklusējums"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Burbuļi"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogais"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-mk/strings.xml b/packages/SystemUI/res-keyguard/values-mk/strings.xml index ef22564318e8..77e1b50d7a79 100644 --- a/packages/SystemUI/res-keyguard/values-mk/strings.xml +++ b/packages/SystemUI/res-keyguard/values-mk/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Потребна е шема по рестартирање на уредот"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Потребен е PIN-код по рестартирање на уредот"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Потребна е лозинка по рестартирање на уредот"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Потребна е шема за дополнителна безбедност"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Потребен е PIN-код за дополнителна безбедност"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Потребна е лозинка за дополнителна безбедност"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"За дополнителна безбедност, користете шема"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"За дополнителна безбедност, користете PIN"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"За дополнителна безбедност, користете лозинка"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Уредот е заклучен од администраторот"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Уредот е заклучен рачно"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Непознат"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"Стандарден"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Балонче"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Аналоген"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Отклучете го уредот за да продолжите"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ml/strings.xml b/packages/SystemUI/res-keyguard/values-ml/strings.xml index 63a542a63e04..e62b4356822d 100644 --- a/packages/SystemUI/res-keyguard/values-ml/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ml/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ഉപകരണം റീസ്റ്റാർട്ടായശേഷം പാറ്റേൺ വരയ്ക്കേണ്ടതുണ്ട്"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ഉപകരണം റീസ്റ്റാർട്ടായശേഷം പിൻ നൽകേണ്ടതുണ്ട്"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ഉപകരണം റീസ്റ്റാർട്ടായശേഷം പാസ്വേഡ് നൽകേണ്ടതുണ്ട്"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"അധിക സുരക്ഷയ്ക്ക് പാറ്റേൺ ആവശ്യമാണ്"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"അധിക സുരക്ഷയ്ക്ക് പിൻ ആവശ്യമാണ്"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"അധിക സുരക്ഷയ്ക്ക് പാസ്വേഡ് ആവശ്യമാണ്"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ഉപകരണം അഡ്മിൻ ലോക്കുചെയ്തു"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ഉപകരണം നേരിട്ട് ലോക്കുചെയ്തു"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"തിരിച്ചറിയുന്നില്ല"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"ഡിഫോൾട്ട്"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"ബബിൾ"</string> <string name="clock_title_analog" msgid="8409262532900918273">"അനലോഗ്"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-mn/strings.xml b/packages/SystemUI/res-keyguard/values-mn/strings.xml index 71c913f988bd..f2cc5ab195a0 100644 --- a/packages/SystemUI/res-keyguard/values-mn/strings.xml +++ b/packages/SystemUI/res-keyguard/values-mn/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Төхөөрөмжийг дахин эхлүүлсний дараа загвар оруулах шаардлагатай"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Төхөөрөмжийг дахин эхлүүлсний дараа ПИН оруулах шаардлагатай"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Төхөөрөмжийг дахин эхлүүлсний дараа нууц үг оруулах шаардлагатай"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Аюулгүй байдлын үүднээс загвар оруулах шаардлагатай"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Аюулгүй байдлын үүднээс ПИН оруулах шаардлагатай"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Аюулгүй байдлын үүднээс нууц үг оруулах шаардлагатай"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Нэмэлт аюулгүй байдлын үүднээс оронд нь хээ ашиглана уу"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Нэмэлт аюулгүй байдлын үүднээс оронд нь ПИН ашиглана уу"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Нэмэлт аюулгүй байдлын үүднээс оронд нь нууц үг ашиглана уу"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Админ төхөөрөмжийг түгжсэн"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Төхөөрөмжийг гараар түгжсэн"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Таньж чадсангүй"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"Өгөгдмөл"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Бөмбөлөг"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Aналог"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Үргэлжлүүлэхийн тулд төхөөрөмжийнхөө түгжээг тайлна уу"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-mr/strings.xml b/packages/SystemUI/res-keyguard/values-mr/strings.xml index 6ac13bdde13a..1454b20d3797 100644 --- a/packages/SystemUI/res-keyguard/values-mr/strings.xml +++ b/packages/SystemUI/res-keyguard/values-mr/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"डिव्हाइस रीस्टार्ट झाल्यावर पॅटर्न आवश्यक आहे"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"डिव्हाइस रीस्टार्ट झाल्यावर पिन आवश्यक आहे"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"डिव्हाइस रीस्टार्ट झाल्यावर पासवर्ड आवश्यक आहे"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"अतिरिक्त सुरक्षिततेसाठी पॅटर्न आवश्यक आहे"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"अतिरिक्त सुरक्षिततेसाठी पिन आवश्यक आहे"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"अतिरिक्त सुरक्षिततेसाठी पासवर्ड आवश्यक आहे"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"प्रशासकाद्वारे लॉक केलेले डिव्हाइस"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"डिव्हाइस मॅन्युअली लॉक केले होते"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"ओळखले नाही"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"डीफॉल्ट"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"बबल"</string> <string name="clock_title_analog" msgid="8409262532900918273">"अॅनालॉग"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ms/strings.xml b/packages/SystemUI/res-keyguard/values-ms/strings.xml index 453afc3d6b37..a6d1af9368ec 100644 --- a/packages/SystemUI/res-keyguard/values-ms/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ms/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Corak diperlukan setelah peranti dimulakan semula"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN diperlukan setelah peranti dimulakan semula"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Kata laluan diperlukan setelah peranti dimulakan semula"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Corak diperlukan untuk keselamatan tambahan"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN diperlukan untuk keselamatan tambahan"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Kata laluan diperlukan untuk keselamatan tambahan"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Peranti dikunci oleh pentadbir"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Peranti telah dikunci secara manual"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tidak dikenali"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Lalai"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Gelembung"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-my/strings.xml b/packages/SystemUI/res-keyguard/values-my/strings.xml index 1cc46b16ec3f..5617a1188a40 100644 --- a/packages/SystemUI/res-keyguard/values-my/strings.xml +++ b/packages/SystemUI/res-keyguard/values-my/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"စက်ပစ္စည်းကို ပိတ်ပြီးပြန်ဖွင့်လိုက်သည့်အခါတွင် ပုံစံ လိုအပ်ပါသည်"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"စက်ပစ္စည်းကို ပိတ်ပြီးပြန်ဖွင့်လိုက်သည့်အခါတွင် ပင်နံပါတ် လိုအပ်ပါသည်"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"စက်ပစ္စည်းကို ပိတ်ပြီးပြန်ဖွင့်လိုက်သည့်အခါတွင် စကားဝှက် လိုအပ်ပါသည်"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ပိုမို၍ လုံခြုံမှု ရှိစေရန် ပုံစံ လိုအပ်ပါသည်"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ပိုမို၍ လုံခြုံမှု ရှိစေရန် ပင်နံပါတ် လိုအပ်ပါသည်"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ပိုမို၍ လုံခြုံမှု ရှိစေရန် စကားဝှက် လိုအပ်ပါသည်"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"စက်ပစ္စည်းကို စီမံခန့်ခွဲသူက လော့ခ်ချထားပါသည်"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"စက်ပစ္စည်းကို ကိုယ်တိုင်ကိုယ်ကျ လော့ခ်ချထားခဲ့သည်"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"မသိ"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"မူလ"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"ပူဖောင်းကွက်"</string> <string name="clock_title_analog" msgid="8409262532900918273">"ရိုးရိုး"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-nb/strings.xml b/packages/SystemUI/res-keyguard/values-nb/strings.xml index 5310a7301d4e..0ad9e951b1e4 100644 --- a/packages/SystemUI/res-keyguard/values-nb/strings.xml +++ b/packages/SystemUI/res-keyguard/values-nb/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Du må tegne mønsteret etter at enheten har startet på nytt"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Du må skrive inn PIN-koden etter at enheten har startet på nytt"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Du må skrive inn passordet etter at enheten har startet på nytt"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Du må tegne mønsteret for ekstra sikkerhet"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Du må skrive inn PIN-koden for ekstra sikkerhet"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Du må skrive inn passordet for ekstra sikkerhet"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Enheten er låst av administratoren"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Enheten ble låst manuelt"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ikke gjenkjent"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Boble"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ne/strings.xml b/packages/SystemUI/res-keyguard/values-ne/strings.xml index 534164b3f2c9..196b74a5658b 100644 --- a/packages/SystemUI/res-keyguard/values-ne/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ne/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"यन्त्र पुनः सुरु भएपछि ढाँचा आवश्यक पर्दछ"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"यन्त्र पुनः सुरु भएपछि PIN आवश्यक पर्दछ"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"यन्त्र पुनः सुरु भएपछि पासवर्ड आवश्यक पर्दछ"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"अतिरिक्त सुरक्षाको लागि ढाँचा आवश्यक छ"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"अतिरिक्त सुरक्षाको लागि PIN आवश्यक छ"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"अतिरिक्त सुरक्षाको लागि पासवर्ड आवश्यक छ"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"प्रशासकले यन्त्रलाई लक गर्नुभएको छ"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"यन्त्रलाई म्यानुअल तरिकाले लक गरिएको थियो"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"पहिचान भएन"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"डिफल्ट"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"बबल"</string> <string name="clock_title_analog" msgid="8409262532900918273">"एनालग"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-nl/strings.xml b/packages/SystemUI/res-keyguard/values-nl/strings.xml index 08e226d4ec07..747b3bbd9128 100644 --- a/packages/SystemUI/res-keyguard/values-nl/strings.xml +++ b/packages/SystemUI/res-keyguard/values-nl/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Patroon vereist nadat het apparaat opnieuw is opgestart"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pincode vereist nadat het apparaat opnieuw is opgestart"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Wachtwoord vereist nadat het apparaat opnieuw is opgestart"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Patroon vereist voor extra beveiliging"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Pincode vereist voor extra beveiliging"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Wachtwoord vereist voor extra beveiliging"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Apparaat vergrendeld door beheerder"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Apparaat is handmatig vergrendeld"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Niet herkend"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Standaard"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bel"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analoog"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-or/strings.xml b/packages/SystemUI/res-keyguard/values-or/strings.xml index 3cdd2649d1b4..75f7a898585b 100644 --- a/packages/SystemUI/res-keyguard/values-or/strings.xml +++ b/packages/SystemUI/res-keyguard/values-or/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ଡିଭାଇସ୍ ରିଷ୍ଟାର୍ଟ ହେବା ପରେ ପାଟର୍ନ ଆବଶ୍ୟକ ଅଟେ"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ଡିଭାଇସ୍ ରିଷ୍ଟାର୍ଟ ହେବାପରେ ପାସ୍ୱର୍ଡ ଆବଶ୍ୟକ"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ଡିଭାଇସ୍ ରିଷ୍ଟାର୍ଟ ହେବା ପରେ ପାସୱର୍ଡ ଆବଶ୍ୟକ ଅଟେ"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ ପାଟର୍ନ ଆବଶ୍ୟକ"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ PIN ଆବଶ୍ୟକ ଅଟେ"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ ପାସ୍ୱର୍ଡ ଆବଶ୍ୟକ"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ, ଏହା ପରିବର୍ତ୍ତେ ପାଟର୍ନ ବ୍ୟବହାର କରନ୍ତୁ"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ, ଏହା ପରିବର୍ତ୍ତେ PIN ବ୍ୟବହାର କରନ୍ତୁ"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ, ଏହା ପରିବର୍ତ୍ତେ ପାସୱାର୍ଡ ବ୍ୟବହାର କରନ୍ତୁ"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ଡିଭାଇସ୍ ଆଡମିନଙ୍କ ଦ୍ୱାରା ଲକ୍ କରାଯାଇଛି"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ଡିଭାଇସ୍ ମାନୁଆଲ ଭାବେ ଲକ୍ କରାଗଲା"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"ଚିହ୍ନଟ ହେଲାନାହିଁ"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"ଡିଫଲ୍ଟ"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"ବବଲ୍"</string> <string name="clock_title_analog" msgid="8409262532900918273">"ଆନାଲଗ୍"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ଜାରି ରଖିବା ପାଇଁ ଆପଣଙ୍କ ଡିଭାଇସକୁ ଅନଲକ କରନ୍ତୁ"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-pa/strings.xml b/packages/SystemUI/res-keyguard/values-pa/strings.xml index 409f72740649..bf1a359a2c75 100644 --- a/packages/SystemUI/res-keyguard/values-pa/strings.xml +++ b/packages/SystemUI/res-keyguard/values-pa/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ਡੀਵਾਈਸ ਦੇ ਮੁੜ-ਚਾਲੂ ਹੋਣ \'ਤੇ ਪੈਟਰਨ ਦੀ ਲੋੜ ਹੈ"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ਡੀਵਾਈਸ ਦੇ ਮੁੜ-ਚਾਲੂ ਹੋਣ \'ਤੇ ਪਿੰਨ ਦੀ ਲੋੜ ਹੈ"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ਡੀਵਾਈਸ ਦੇ ਮੁੜ-ਚਾਲੂ ਹੋਣ \'ਤੇ ਪਾਸਵਰਡ ਦੀ ਲੋੜ ਹੈ"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ ਪੈਟਰਨ ਦੀ ਲੋੜ ਹੈ"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ ਪਿੰਨ ਦੀ ਲੋੜ ਹੈ"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ ਪਾਸਵਰਡ ਦੀ ਲੋੜ ਹੈ"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ਪ੍ਰਸ਼ਾਸਕ ਵੱਲੋਂ ਡੀਵਾਈਸ ਨੂੰ ਲਾਕ ਕੀਤਾ ਗਿਆ"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ਡੀਵਾਈਸ ਨੂੰ ਹੱਥੀਂ ਲਾਕ ਕੀਤਾ ਗਿਆ"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"ਪਛਾਣ ਨਹੀਂ ਹੋਈ"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"ਪੂਰਵ-ਨਿਰਧਾਰਿਤ"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"ਬੁਲਬੁਲਾ"</string> <string name="clock_title_analog" msgid="8409262532900918273">"ਐਨਾਲੌਗ"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-pl/strings.xml b/packages/SystemUI/res-keyguard/values-pl/strings.xml index 52bc98236330..c49149baa93a 100644 --- a/packages/SystemUI/res-keyguard/values-pl/strings.xml +++ b/packages/SystemUI/res-keyguard/values-pl/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po ponownym uruchomieniu urządzenia wymagany jest wzór"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po ponownym uruchomieniu urządzenia wymagany jest kod PIN"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po ponownym uruchomieniu urządzenia wymagane jest hasło"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Dla większego bezpieczeństwa musisz narysować wzór"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Dla większego bezpieczeństwa musisz podać kod PIN"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Dla większego bezpieczeństwa musisz podać hasło"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Urządzenie zablokowane przez administratora"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Urządzenie zostało zablokowane ręcznie"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nie rozpoznano"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Domyślna"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bąbelkowy"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogowy"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml b/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml index b9348260d40e..3d60e8c45bcb 100644 --- a/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml +++ b/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"O padrão é exigido após a reinicialização do dispositivo"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"O PIN é exigido após a reinicialização do dispositivo"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"A senha é exigida após a reinicialização do dispositivo"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"O padrão é necessário para aumentar a segurança"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"O PIN é necessário para aumentar a segurança"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"A senha é necessária para aumentar a segurança"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para ter mais segurança, use o padrão"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para ter mais segurança, use o PIN"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para ter mais segurança, use a senha"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado pelo administrador"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo foi bloqueado manualmente"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Não reconhecido"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"Padrão"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bolha"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueie o dispositivo para continuar"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml b/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml index a67bfb020a3c..0a943496fba9 100644 --- a/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml +++ b/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"É necessário um padrão após reiniciar o dispositivo"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"É necessário um PIN após reiniciar o dispositivo"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"É necessária uma palavra-passe após reiniciar o dispositivo"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Para segurança adicional, é necessário um padrão"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Para segurança adicional, é necessário um PIN"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Para segurança adicional, é necessária uma palavra-passe"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para uma segurança adicional, use antes o padrão"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para uma segurança adicional, use antes o PIN"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para uma segurança adicional, use antes a palavra-passe"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado pelo gestor"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo foi bloqueado manualmente"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Não reconhecido."</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"Predefinido"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Balão"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueie o dispositivo para continuar"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-pt/strings.xml b/packages/SystemUI/res-keyguard/values-pt/strings.xml index b9348260d40e..3d60e8c45bcb 100644 --- a/packages/SystemUI/res-keyguard/values-pt/strings.xml +++ b/packages/SystemUI/res-keyguard/values-pt/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"O padrão é exigido após a reinicialização do dispositivo"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"O PIN é exigido após a reinicialização do dispositivo"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"A senha é exigida após a reinicialização do dispositivo"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"O padrão é necessário para aumentar a segurança"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"O PIN é necessário para aumentar a segurança"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"A senha é necessária para aumentar a segurança"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para ter mais segurança, use o padrão"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para ter mais segurança, use o PIN"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para ter mais segurança, use a senha"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado pelo administrador"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo foi bloqueado manualmente"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Não reconhecido"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"Padrão"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bolha"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueie o dispositivo para continuar"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ro/strings.xml b/packages/SystemUI/res-keyguard/values-ro/strings.xml index 5ee67d917768..547224e781b1 100644 --- a/packages/SystemUI/res-keyguard/values-ro/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ro/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Modelul este necesar după repornirea dispozitivului"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Codul PIN este necesar după repornirea dispozitivului"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Parola este necesară după repornirea dispozitivului"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Modelul este necesar pentru securitate suplimentară"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Codul PIN este necesar pentru securitate suplimentară"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Parola este necesară pentru securitate suplimentară"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispozitiv blocat de administrator"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Dispozitivul a fost blocat manual"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nu este recunoscut"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Prestabilit"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Balon"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogic"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ru/strings.xml b/packages/SystemUI/res-keyguard/values-ru/strings.xml index 2b8f8d6a93f8..f1945ad3fb03 100644 --- a/packages/SystemUI/res-keyguard/values-ru/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ru/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"После перезагрузки устройства необходимо ввести графический ключ"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"После перезагрузки устройства необходимо ввести PIN-код"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"После перезагрузки устройства необходимо ввести пароль"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"В качестве дополнительной меры безопасности введите графический ключ"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"В качестве дополнительной меры безопасности введите PIN-код"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"В качестве дополнительной меры безопасности введите пароль"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"В целях дополнительной безопасности используйте графический ключ"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"В целях дополнительной безопасности используйте PIN-код"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"В целях дополнительной безопасности используйте пароль"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Устройство заблокировано администратором"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Устройство было заблокировано вручную"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не распознано"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"По умолчанию"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Пузырь"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Стрелки"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Чтобы продолжить, разблокируйте устройство"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-si/strings.xml b/packages/SystemUI/res-keyguard/values-si/strings.xml index 4e911defffb6..e5862c358002 100644 --- a/packages/SystemUI/res-keyguard/values-si/strings.xml +++ b/packages/SystemUI/res-keyguard/values-si/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"උපාංගය නැවත ආරම්භ වූ පසු රටාව අවශ්යයි"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"උපාංගය නැවත ආරම්භ වූ පසු PIN අංකය අවශ්යයි"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"උපාංගය නැවත ආරම්භ වූ පසු මුරපදය අවශ්යයි"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"අමතර ආරක්ෂාව සඳහා රටාව අවශ්යයි"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"අමතර ආරක්ෂාව සඳහා PIN අංකය අවශ්යයි"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"අමතර ආරක්ෂාව සඳහා මුරපදය අවශ්යයි"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ඔබගේ පරිපාලක විසින් උපාංගය අගුළු දමා ඇත"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"උපාංගය හස්තීයව අගුලු දමන ලදී"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"හඳුනා නොගන්නා ලදී"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"පෙරනිමි"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"බුබුළ"</string> <string name="clock_title_analog" msgid="8409262532900918273">"ප්රතිසමය"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-sk/strings.xml b/packages/SystemUI/res-keyguard/values-sk/strings.xml index f2d68e3763d3..efe4ec864448 100644 --- a/packages/SystemUI/res-keyguard/values-sk/strings.xml +++ b/packages/SystemUI/res-keyguard/values-sk/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po reštartovaní zariadenia musíte zadať bezpečnostný vzor"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po reštartovaní zariadenia musíte zadať kód PIN"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po reštartovaní zariadenia musíte zadať heslo"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Na ďalšie zabezpečenie musíte zadať bezpečnostný vzor"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Na ďalšie zabezpečenie musíte zadať kód PIN"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Na ďalšie zabezpečenie musíte zadať heslo"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Zariadenie zamkol správca"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Zariadenie bolo uzamknuté ručne"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nerozpoznané"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Predvolený"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bublina"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analógový"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-sl/strings.xml b/packages/SystemUI/res-keyguard/values-sl/strings.xml index 772308f9f658..52726c225498 100644 --- a/packages/SystemUI/res-keyguard/values-sl/strings.xml +++ b/packages/SystemUI/res-keyguard/values-sl/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po vnovičnem zagonu naprave je treba vnesti vzorec"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po vnovičnem zagonu naprave je treba vnesti kodo PIN"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po vnovičnem zagonu naprave je treba vnesti geslo"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Zaradi dodatne varnosti morate vnesti vzorec"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Zaradi dodatne varnosti morate vnesti kodo PIN"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Zaradi dodatne varnosti morate vnesti geslo"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Napravo je zaklenil skrbnik"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Naprava je bila ročno zaklenjena"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ni prepoznano"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Privzeto"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Mehurček"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogno"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-sq/strings.xml b/packages/SystemUI/res-keyguard/values-sq/strings.xml index c7584622823c..a0a55944eced 100644 --- a/packages/SystemUI/res-keyguard/values-sq/strings.xml +++ b/packages/SystemUI/res-keyguard/values-sq/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Kërkohet motivi pas rinisjes së pajisjes"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Kërkohet kodi PIN pas rinisjes së pajisjes"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Kërkohet fjalëkalimi pas rinisjes së pajisjes"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kërkohet motivi për më shumë siguri"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Kërkohet kodi PIN për më shumë siguri"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Kërkohet fjalëkalimi për më shumë siguri"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Pajisja është e kyçur nga administratori"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Pajisja është kyçur manualisht"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nuk njihet"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"E parazgjedhur"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Flluskë"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analoge"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-sr/strings.xml b/packages/SystemUI/res-keyguard/values-sr/strings.xml index e6fe8531ff4c..e634fdcb586e 100644 --- a/packages/SystemUI/res-keyguard/values-sr/strings.xml +++ b/packages/SystemUI/res-keyguard/values-sr/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Треба да унесете шаблон када се уређај поново покрене"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Треба да унесете PIN када се уређај поново покрене"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Треба да унесете лозинку када се уређај поново покрене"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Треба да унесете шаблон ради додатне безбедности"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Треба да унесете PIN ради додатне безбедности"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Треба да унесете лозинку ради додатне безбедности"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Администратор је закључао уређај"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Уређај је ручно закључан"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Није препознат"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Подразумевани"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Мехурићи"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Аналогни"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-sv/strings.xml b/packages/SystemUI/res-keyguard/values-sv/strings.xml index fa241d96cfae..fc9beb1286a6 100644 --- a/packages/SystemUI/res-keyguard/values-sv/strings.xml +++ b/packages/SystemUI/res-keyguard/values-sv/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Du måste rita mönster när du har startat om enheten"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Du måste ange pinkod när du har startat om enheten"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Du måste ange lösenord när du har startat om enheten"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Du måste rita mönster för ytterligare säkerhet"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Du måste ange pinkod för ytterligare säkerhet"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Du måste ange lösenord för ytterligare säkerhet"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administratören har låst enheten"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Enheten har låsts manuellt"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Identifierades inte"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bubbla"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-sw/strings.xml b/packages/SystemUI/res-keyguard/values-sw/strings.xml index 791bceb071e9..bcab24b013bb 100644 --- a/packages/SystemUI/res-keyguard/values-sw/strings.xml +++ b/packages/SystemUI/res-keyguard/values-sw/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Unafaa kuchora mchoro baada ya kuwasha kifaa upya"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Unafaa kuweka PIN baada ya kuwasha kifaa upya"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Unafaa kuweka nenosiri baada ya kuwasha kifaa upya"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Mchoro unahitajika ili kuongeza usalama"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN inahitajika ili kuongeza usalama"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Nenosiri linahitajika ili kuongeza usalama."</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Msimamizi amefunga kifaa"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Umefunga kifaa mwenyewe"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Haitambuliwi"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Chaguomsingi"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Kiputo"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogi"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ta/strings.xml b/packages/SystemUI/res-keyguard/values-ta/strings.xml index 271657d9f0a1..88d5760e7f6c 100644 --- a/packages/SystemUI/res-keyguard/values-ta/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ta/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"சாதனத்தை மீண்டும் தொடங்கியதும், பேட்டர்னை வரைய வேண்டும்"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"சாதனத்தை மீண்டும் தொடங்கியதும், பின்னை உள்ளிட வேண்டும்"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"சாதனத்தை மீண்டும் தொடங்கியதும், கடவுச்சொல்லை உள்ளிட வேண்டும்"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"கூடுதல் பாதுகாப்பிற்கு, பேட்டர்னை வரைய வேண்டும்"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"கூடுதல் பாதுகாப்பிற்கு, பின்னை உள்ளிட வேண்டும்"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"கூடுதல் பாதுகாப்பிற்கு, கடவுச்சொல்லை உள்ளிட வேண்டும்"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"நிர்வாகி சாதனத்தைப் பூட்டியுள்ளார்"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"பயனர் சாதனத்தைப் பூட்டியுள்ளார்"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"அடையாளங்காணபடவில்லை"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"இயல்பு"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"பபிள்"</string> <string name="clock_title_analog" msgid="8409262532900918273">"அனலாக்"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-te/strings.xml b/packages/SystemUI/res-keyguard/values-te/strings.xml index f62e667ee26d..3a0111a193bd 100644 --- a/packages/SystemUI/res-keyguard/values-te/strings.xml +++ b/packages/SystemUI/res-keyguard/values-te/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"పరికరాన్ని పునఃప్రారంభించిన తర్వాత నమూనాను గీయాలి"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"డివైజ్ను పునఃప్రారంభించిన తర్వాత పిన్ నమోదు చేయాలి"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"పరికరాన్ని పునఃప్రారంభించిన తర్వాత పాస్వర్డ్ను నమోదు చేయాలి"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"అదనపు సెక్యూరిటీ కోసం ఆకృతి అవసరం"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"అదనపు సెక్యూరిటీ కోసం పిన్ ఎంటర్ చేయాలి"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"అదనపు సెక్యూరిటీ కోసం పాస్వర్డ్ను ఎంటర్ చేయాలి"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"పరికరం నిర్వాహకుల ద్వారా లాక్ చేయబడింది"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"పరికరం మాన్యువల్గా లాక్ చేయబడింది"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"గుర్తించలేదు"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"ఆటోమేటిక్"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"బబుల్"</string> <string name="clock_title_analog" msgid="8409262532900918273">"ఎనలాగ్"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-th/strings.xml b/packages/SystemUI/res-keyguard/values-th/strings.xml index 62a83bcf9d7a..14a65a074f87 100644 --- a/packages/SystemUI/res-keyguard/values-th/strings.xml +++ b/packages/SystemUI/res-keyguard/values-th/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ต้องวาดรูปแบบหลังจากอุปกรณ์รีสตาร์ท"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ต้องระบุ PIN หลังจากอุปกรณ์รีสตาร์ท"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ต้องป้อนรหัสผ่านหลังจากอุปกรณ์รีสตาร์ท"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ต้องวาดรูปแบบเพื่อความปลอดภัยเพิ่มเติม"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ต้องระบุ PIN เพื่อความปลอดภัยเพิ่มเติม"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ต้องป้อนรหัสผ่านเพื่อความปลอดภัยเพิ่มเติม"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ใช้รูปแบบแทนเพื่อเพิ่มความปลอดภัย"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ใช้ PIN แทนเพื่อเพิ่มความปลอดภัย"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ใช้รหัสผ่านแทนเพื่อเพิ่มความปลอดภัย"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ผู้ดูแลระบบล็อกอุปกรณ์"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"มีการล็อกอุปกรณ์ด้วยตัวเอง"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"ไม่รู้จัก"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"ค่าเริ่มต้น"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"บับเบิล"</string> <string name="clock_title_analog" msgid="8409262532900918273">"แอนะล็อก"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ปลดล็อกอุปกรณ์ของคุณเพื่อดำเนินการต่อ"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-tl/strings.xml b/packages/SystemUI/res-keyguard/values-tl/strings.xml index 524ea4782506..7936058581ae 100644 --- a/packages/SystemUI/res-keyguard/values-tl/strings.xml +++ b/packages/SystemUI/res-keyguard/values-tl/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Kailangan ng pattern pagkatapos mag-restart ng device"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Kailangan ng PIN pagkatapos mag-restart ng device"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Kailangan ng password pagkatapos mag-restart ng device"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kinakailangan ang pattern para sa karagdagang seguridad"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Kinakailangan ang PIN para sa karagdagang seguridad"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Kinakailangan ang password para sa karagdagang seguridad"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para sa karagdagang seguridad, gumamit na lang ng pattern"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para sa karagdagang seguridad, gumamit na lang ng PIN"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para sa karagdagang seguridad, gumamit na lang ng password"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Na-lock ng admin ang device"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Manual na na-lock ang device"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Hindi nakilala"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"Default"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"I-unlock ang iyong device para magpatuloy"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-tr/strings.xml b/packages/SystemUI/res-keyguard/values-tr/strings.xml index 54aaae38b18c..e5207623adc2 100644 --- a/packages/SystemUI/res-keyguard/values-tr/strings.xml +++ b/packages/SystemUI/res-keyguard/values-tr/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Cihaz yeniden başladıktan sonra desen gerekir"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Cihaz yeniden başladıktan sonra PIN gerekir"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Cihaz yeniden başladıktan sonra şifre gerekir"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Ek güvenlik için desen gerekir"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Ek güvenlik için PIN gerekir"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Ek güvenlik için şifre gerekir"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Cihaz, yönetici tarafından kilitlendi"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Cihazın manuel olarak kilitlendi"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tanınmadı"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Varsayılan"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Baloncuk"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-uk/strings.xml b/packages/SystemUI/res-keyguard/values-uk/strings.xml index 6144c1c4e0d5..613181d6c96f 100644 --- a/packages/SystemUI/res-keyguard/values-uk/strings.xml +++ b/packages/SystemUI/res-keyguard/values-uk/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Після перезавантаження пристрою потрібно ввести ключ"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Після перезавантаження пристрою потрібно ввести PIN-код"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Після перезавантаження пристрою потрібно ввести пароль"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Для додаткового захисту потрібно ввести ключ"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Для додаткового захисту потрібно ввести PIN-код"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Для додаткового захисту потрібно ввести пароль"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Адміністратор заблокував пристрій"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Пристрій заблоковано вручну"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не розпізнано"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"За умовчанням"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Бульбашковий"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Аналоговий"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-ur/strings.xml b/packages/SystemUI/res-keyguard/values-ur/strings.xml index 4e778413775e..a122f8537611 100644 --- a/packages/SystemUI/res-keyguard/values-ur/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ur/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"آلہ دوبارہ چالو ہونے کے بعد پیٹرن درکار ہوتا ہے"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"آلہ دوبارہ چالو ہونے کے بعد PIN درکار ہوتا ہے"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"آلہ دوبارہ چالو ہونے کے بعد پاس ورڈ درکار ہوتا ہے"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"اضافی سیکیورٹی کیلئے پیٹرن درکار ہے"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"اضافی سیکیورٹی کیلئے PIN درکار ہے"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"اضافی سیکیورٹی کیلئے پاس ورڈ درکار ہے"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"آلہ منتظم کی جانب سے مقفل ہے"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"آلہ کو دستی طور پر مقفل کیا گیا تھا"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"تسلیم شدہ نہیں ہے"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"ڈیفالٹ"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"بلبلہ"</string> <string name="clock_title_analog" msgid="8409262532900918273">"اینالاگ"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-uz/strings.xml b/packages/SystemUI/res-keyguard/values-uz/strings.xml index afaf7464d091..2cc9724dc53b 100644 --- a/packages/SystemUI/res-keyguard/values-uz/strings.xml +++ b/packages/SystemUI/res-keyguard/values-uz/strings.xml @@ -78,9 +78,9 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Qurilma qayta ishga tushganidan keyin grafik kalitni kiritish zarur"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Qurilma qayta ishga tushganidan keyin PIN kodni kiritish zarur"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Qurilma qayta ishga tushganidan keyin parolni kiritish zarur"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Qo‘shimcha xavfsizlik chorasi sifatida grafik kalit talab qilinadi"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Qo‘shimcha xavfsizlik chorasi sifatida PIN kod talab qilinadi"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Qo‘shimcha xavfsizlik chorasi sifatida parol talab qilinadi"</string> + <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Qoʻshimcha xavfsizlik maqsadida oʻrniga grafik kalitdan foydalaning"</string> + <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Qoʻshimcha xavfsizlik maqsadida oʻrniga PIN koddan foydalaning"</string> + <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Qoʻshimcha xavfsizlik maqsadida oʻrniga paroldan foydalaning"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Qurilma administrator tomonidan bloklangan"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Qurilma qo‘lda qulflangan"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Aniqlanmadi"</string> @@ -90,4 +90,5 @@ <string name="clock_title_default" msgid="6342735240617459864">"Odatiy"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Pufaklar"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string> + <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Davom etish uchun qurilmangizni qulfdan chiqaring"</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values-vi/strings.xml b/packages/SystemUI/res-keyguard/values-vi/strings.xml index 1d6cfa85e4ed..e7c9295815ad 100644 --- a/packages/SystemUI/res-keyguard/values-vi/strings.xml +++ b/packages/SystemUI/res-keyguard/values-vi/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Yêu cầu hình mở khóa sau khi thiết bị khởi động lại"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Yêu cầu mã PIN sau khi thiết bị khởi động lại"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Yêu cầu mật khẩu sau khi thiết bị khởi động lại"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Yêu cầu hình mở khóa để bảo mật thêm"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Yêu cầu mã PIN để bảo mật thêm"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Yêu cầu mật khẩu để bảo mật thêm"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Thiết bị đã bị quản trị viên khóa"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Thiết bị đã bị khóa theo cách thủ công"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Không nhận dạng được"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Mặc định"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bong bóng"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Đồng hồ kim"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml b/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml index 8c8507ed06fa..d37d645b15ee 100644 --- a/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml +++ b/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"重启设备后需要绘制解锁图案"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"重启设备后需要输入 PIN 码"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"重启设备后需要输入密码"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"需要绘制解锁图案以进一步确保安全"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"需要输入 PIN 码以进一步确保安全"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"需要输入密码以进一步确保安全"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"管理员已锁定设备"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"此设备已手动锁定"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"无法识别"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"默认"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"泡泡"</string> <string name="clock_title_analog" msgid="8409262532900918273">"指针"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml b/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml index c331a925be39..9dbb8f2dac73 100644 --- a/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml +++ b/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"裝置重新啟動後,必須畫出上鎖圖案才能使用"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"裝置重新啟動後,必須輸入 PIN 碼才能使用"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"裝置重新啟動後,必須輸入密碼才能使用"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"請務必畫出上鎖圖案,以進一步確保安全"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"請務必輸入 PIN 碼,以進一步確保安全"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"請務必輸入密碼,以進一步確保安全"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"裝置已由管理員鎖定"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"使用者已手動將裝置上鎖"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"未能識別"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"預設"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"泡泡"</string> <string name="clock_title_analog" msgid="8409262532900918273">"指針"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml b/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml index 1e1bec3ef76a..ebb88e13194b 100644 --- a/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml +++ b/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"裝置重新啟動後需要畫出解鎖圖案"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"裝置重新啟動後需要輸入 PIN 碼"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"裝置重新啟動後需要輸入密碼"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"請畫出解鎖圖案,以進一步確保資訊安全"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"請輸入 PIN 碼,以進一步確保資訊安全"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"請輸入密碼,以進一步確保資訊安全"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"管理員已鎖定裝置"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"裝置已手動鎖定"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"無法識別"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"預設"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"泡泡"</string> <string name="clock_title_analog" msgid="8409262532900918273">"類比"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values-zu/strings.xml b/packages/SystemUI/res-keyguard/values-zu/strings.xml index c8f78ea441f5..57e56f713536 100644 --- a/packages/SystemUI/res-keyguard/values-zu/strings.xml +++ b/packages/SystemUI/res-keyguard/values-zu/strings.xml @@ -78,9 +78,12 @@ <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Iphethini iyadingeka ngemuva kokuqala kabusha kwedivayisi"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Iphinikhodi iyadingeka ngemuva kokuqala kabusha kwedivayisi"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Iphasiwedi iyadingeka ngemuva kokuqala kabusha kwedivayisi"</string> - <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kudingeka iphethini ngokuvikeleka okungeziwe"</string> - <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Kudingeka iphinikhodi ngokuvikeleka okungeziwe"</string> - <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Iphasiwedi idingelwa ukuvikela okungeziwe"</string> + <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) --> + <skip /> + <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) --> + <skip /> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Idivayisi ikhiywe ngumlawuli"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Idivayisi ikhiywe ngokwenza"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Akwaziwa"</string> @@ -90,4 +93,6 @@ <string name="clock_title_default" msgid="6342735240617459864">"Okuzenzekelayo"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Ibhamuza"</string> <string name="clock_title_analog" msgid="8409262532900918273">"I-Analog"</string> + <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) --> + <skip /> </resources> diff --git a/packages/SystemUI/res-keyguard/values/config.xml b/packages/SystemUI/res-keyguard/values/config.xml index b1d33758f1b3..a25ab5109fa8 100644 --- a/packages/SystemUI/res-keyguard/values/config.xml +++ b/packages/SystemUI/res-keyguard/values/config.xml @@ -28,11 +28,6 @@ <!-- Will display the bouncer on one side of the display, and the current user icon and user switcher on the other side --> <bool name="config_enableBouncerUserSwitcher">false</bool> - <!-- Whether to show the face scanning animation on devices with face auth supported. - The face scanning animation renders in a SW layer in ScreenDecorations. - Enabling this will also render the camera protection in the SW layer - (instead of HW, if relevant)."=--> - <bool name="config_enableFaceScanningAnimation">true</bool> <!-- Time to be considered a consecutive fingerprint failure in ms --> <integer name="fp_consecutive_failure_time_ms">3500</integer> </resources> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index 46f6ab2399d1..0a55cf779683 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -119,6 +119,7 @@ <dimen name="bouncer_user_switcher_width">248dp</dimen> <dimen name="bouncer_user_switcher_popup_header_height">12dp</dimen> <dimen name="bouncer_user_switcher_popup_divider_height">4dp</dimen> + <dimen name="bouncer_user_switcher_popup_items_divider_height">2dp</dimen> <dimen name="bouncer_user_switcher_item_padding_vertical">10dp</dimen> <dimen name="bouncer_user_switcher_item_padding_horizontal">12dp</dimen> <dimen name="bouncer_user_switcher_header_padding_end">44dp</dimen> diff --git a/packages/SystemUI/res/drawable-hdpi/textfield_default_filled.9.png b/packages/SystemUI/res/drawable-hdpi/textfield_default_filled.9.png Binary files differnew file mode 100644 index 000000000000..3dd997fade6c --- /dev/null +++ b/packages/SystemUI/res/drawable-hdpi/textfield_default_filled.9.png diff --git a/packages/SystemUI/res/drawable-mdpi/textfield_default_filled.9.png b/packages/SystemUI/res/drawable-mdpi/textfield_default_filled.9.png Binary files differnew file mode 100644 index 000000000000..80aba01091fe --- /dev/null +++ b/packages/SystemUI/res/drawable-mdpi/textfield_default_filled.9.png diff --git a/packages/SystemUI/res/drawable-xhdpi/textfield_default_filled.9.png b/packages/SystemUI/res/drawable-xhdpi/textfield_default_filled.9.png Binary files differnew file mode 100644 index 000000000000..b3f89ed7ea7a --- /dev/null +++ b/packages/SystemUI/res/drawable-xhdpi/textfield_default_filled.9.png diff --git a/packages/SystemUI/res/drawable-xxhdpi/textfield_default_filled.9.png b/packages/SystemUI/res/drawable-xxhdpi/textfield_default_filled.9.png Binary files differnew file mode 100644 index 000000000000..efa2cb988ac1 --- /dev/null +++ b/packages/SystemUI/res/drawable-xxhdpi/textfield_default_filled.9.png diff --git a/packages/SystemUI/res/drawable/edit_text_filled.xml b/packages/SystemUI/res/drawable/edit_text_filled.xml new file mode 100644 index 000000000000..cca34d456078 --- /dev/null +++ b/packages/SystemUI/res/drawable/edit_text_filled.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:insetLeft="4dp" + android:insetRight="4dp" + android:insetTop="10dp" + android:insetBottom="10dp"> + <selector> + <item android:state_enabled="false"> + <nine-patch android:src="@drawable/textfield_default_filled" + android:tint="?android:attr/colorControlNormal" /> + </item> + <item android:state_pressed="false" android:state_focused="false"> + <nine-patch android:src="@drawable/textfield_default_filled" + android:tint="?android:attr/colorControlNormal" /> + </item> + <item> + <nine-patch android:src="@drawable/textfield_default_filled" + android:tint="?android:attr/colorControlActivated" /> + </item> + </selector> +</inset> diff --git a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml b/packages/SystemUI/res/drawable/media_squiggly_progress.xml index 9e61236aa7df..9cd3f6288b1d 100644 --- a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml +++ b/packages/SystemUI/res/drawable/media_squiggly_progress.xml @@ -14,4 +14,4 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<com.android.systemui.media.SquigglyProgress />
\ No newline at end of file +<com.android.systemui.media.controls.ui.SquigglyProgress />
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/overlay_badge_background.xml b/packages/SystemUI/res/drawable/overlay_badge_background.xml new file mode 100644 index 000000000000..857632edcf0d --- /dev/null +++ b/packages/SystemUI/res/drawable/overlay_badge_background.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="oval"> + <solid android:color="?androidprv:attr/colorSurface"/> +</shape> diff --git a/packages/SystemUI/res/drawable/qs_media_background.xml b/packages/SystemUI/res/drawable/qs_media_background.xml index 6ed3a0aed091..217656dab022 100644 --- a/packages/SystemUI/res/drawable/qs_media_background.xml +++ b/packages/SystemUI/res/drawable/qs_media_background.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<com.android.systemui.media.IlluminationDrawable +<com.android.systemui.media.controls.ui.IlluminationDrawable xmlns:systemui="http://schemas.android.com/apk/res-auto" systemui:highlight="15" systemui:cornerRadius="@dimen/notification_corner_radius" />
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/qs_media_light_source.xml b/packages/SystemUI/res/drawable/qs_media_light_source.xml index b2647c1f6697..849349a5f100 100644 --- a/packages/SystemUI/res/drawable/qs_media_light_source.xml +++ b/packages/SystemUI/res/drawable/qs_media_light_source.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<com.android.systemui.media.LightSourceDrawable +<com.android.systemui.media.controls.ui.LightSourceDrawable xmlns:systemui="http://schemas.android.com/apk/res-auto" systemui:rippleMinSize="25dp" systemui:rippleMaxSize="135dp" />
\ No newline at end of file diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml index da76c8d0b11a..3bcc37a478c9 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml @@ -16,46 +16,74 @@ <com.android.systemui.biometrics.AuthCredentialPasswordView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical" - android:gravity="center_horizontal" - android:elevation="@dimen/biometric_dialog_elevation"> + android:orientation="horizontal" + android:elevation="@dimen/biometric_dialog_elevation" + android:theme="?app:attr/lockPinPasswordStyle"> - <TextView - android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Title"/> + <RelativeLayout + android:id="@+id/auth_credential_header" + style="?headerStyle" + android:layout_width="wrap_content" + android:layout_height="match_parent"> - <TextView - android:id="@+id/subtitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Subtitle"/> + <ImageView + android:id="@+id/icon" + style="?headerIconStyle" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:contentDescription="@null"/> - <TextView - android:id="@+id/description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Description"/> + <TextView + android:id="@+id/title" + style="?titleTextAppearance" + android:layout_below="@id/icon" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> - <ImeAwareEditText - android:id="@+id/lockPassword" - android:layout_width="208dp" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:minHeight="48dp" - android:gravity="center" - android:inputType="textPassword" - android:maxLength="500" - android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" - style="@style/TextAppearance.AuthCredential.PasswordEntry"/> - - <TextView - android:id="@+id/error" - android:layout_width="match_parent" + <TextView + android:id="@+id/subtitle" + style="?subTitleTextAppearance" + android:layout_below="@id/title" + android:layout_alignParentLeft="true" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/description" + style="?descriptionTextAppearance" + android:layout_below="@id/subtitle" + android:layout_alignParentLeft="true" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + </RelativeLayout> + + <LinearLayout + android:id="@+id/auth_credential_input" + android:layout_width="wrap_content" android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Error"/> + android:orientation="vertical"> + + <ImeAwareEditText + android:id="@+id/lockPassword" + style="?passwordTextAppearance" + android:layout_width="208dp" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" + android:inputType="textPassword" + android:minHeight="48dp" /> + + <TextView + android:id="@+id/error" + style="?errorTextAppearance" + android:layout_gravity="center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + </LinearLayout> </com.android.systemui.biometrics.AuthCredentialPasswordView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml index 19a85fec1397..a3dd334bd667 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml @@ -16,91 +16,71 @@ <com.android.systemui.biometrics.AuthCredentialPatternView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" - android:elevation="@dimen/biometric_dialog_elevation"> + android:elevation="@dimen/biometric_dialog_elevation" + android:theme="?app:attr/lockPatternStyle"> - <LinearLayout + <RelativeLayout + android:id="@+id/auth_credential_header" + style="?headerStyle" android:layout_width="0dp" android:layout_height="match_parent" - android:layout_weight="1" - android:gravity="center" - android:orientation="vertical"> - - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_weight="1"/> + android:layout_weight="1"> <ImageView android:id="@+id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + style="?headerIconStyle" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:contentDescription="@null"/> <TextView android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Title"/> + style="?titleTextAppearance" + android:layout_below="@id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> <TextView android:id="@+id/subtitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Subtitle"/> + style="?subTitleTextAppearance" + android:layout_below="@id/title" + android:layout_alignParentLeft="true" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> <TextView android:id="@+id/description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Description"/> + style="?descriptionTextAppearance" + android:layout_below="@id/subtitle" + android:layout_alignParentLeft="true" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_weight="1"/> + </RelativeLayout> + + <FrameLayout + android:layout_weight="1" + style="?containerStyle" + android:layout_width="0dp" + android:layout_height="match_parent"> + + <com.android.internal.widget.LockPatternView + android:id="@+id/lockPattern" + android:layout_gravity="center" + android:layout_width="match_parent" + android:layout_height="match_parent"/> <TextView android:id="@+id/error" + style="?errorTextAppearance" android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Error"/> - - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_weight="1"/> - - </LinearLayout> - - <LinearLayout - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_weight="1" - android:orientation="vertical" - android:gravity="center" - android:paddingLeft="0dp" - android:paddingRight="0dp" - android:paddingTop="0dp" - android:paddingBottom="16dp" - android:clipToPadding="false"> - - <FrameLayout - android:layout_width="wrap_content" - android:layout_height="0dp" - android:layout_weight="1" - style="@style/LockPatternContainerStyle"> - - <com.android.internal.widget.LockPatternView - android:id="@+id/lockPattern" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - style="@style/LockPatternStyleBiometricPrompt"/> - - </FrameLayout> + android:layout_gravity="center_horizontal|bottom"/> - </LinearLayout> + </FrameLayout> </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml index 0ff1db2ef694..774b335f913e 100644 --- a/packages/SystemUI/res/layout/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml @@ -16,74 +16,71 @@ <com.android.systemui.biometrics.AuthCredentialPasswordView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:elevation="@dimen/biometric_dialog_elevation" - android:orientation="vertical"> + android:orientation="vertical" + android:theme="?app:attr/lockPinPasswordStyle"> <RelativeLayout + android:id="@+id/auth_credential_header" + style="?headerStyle" android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <LinearLayout - android:id="@+id/auth_credential_header" - style="@style/AuthCredentialHeaderStyle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_alignParentTop="true"> + android:layout_height="match_parent"> - <ImageView - android:id="@+id/icon" - android:layout_width="48dp" - android:layout_height="48dp" - android:contentDescription="@null" /> + <ImageView + android:id="@+id/icon" + style="?headerIconStyle" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:contentDescription="@null"/> - <TextView - android:id="@+id/title" - style="@style/TextAppearance.AuthNonBioCredential.Title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - - <TextView - android:id="@+id/subtitle" - style="@style/TextAppearance.AuthNonBioCredential.Subtitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - - <TextView - android:id="@+id/description" - style="@style/TextAppearance.AuthNonBioCredential.Description" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> + <TextView + android:id="@+id/title" + style="?titleTextAppearance" + android:layout_below="@id/icon" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> - </LinearLayout> + <TextView + android:id="@+id/subtitle" + style="?subTitleTextAppearance" + android:layout_below="@id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> - <LinearLayout + <TextView + android:id="@+id/description" + style="?descriptionTextAppearance" + android:layout_below="@id/subtitle" android:layout_width="match_parent" - android:layout_height="match_parent" - android:gravity="center" - android:orientation="vertical" - android:layout_alignParentBottom="true"> + android:layout_height="wrap_content"/> + </RelativeLayout> - <ImeAwareEditText - android:id="@+id/lockPassword" - style="@style/TextAppearance.AuthCredential.PasswordEntry" - android:layout_width="208dp" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" - android:inputType="textPassword" - android:minHeight="48dp" /> + <LinearLayout + android:id="@+id/auth_credential_input" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> - <TextView - android:id="@+id/error" - style="@style/TextAppearance.AuthNonBioCredential.Error" - android:layout_width="match_parent" - android:layout_height="wrap_content" /> + <ImeAwareEditText + android:id="@+id/lockPassword" + style="?passwordTextAppearance" + android:layout_width="208dp" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" + android:inputType="textPassword" + android:minHeight="48dp" /> - </LinearLayout> + <TextView + android:id="@+id/error" + style="?errorTextAppearance" + android:layout_gravity="center_horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> - </RelativeLayout> + </LinearLayout> </com.android.systemui.biometrics.AuthCredentialPasswordView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml index dada9813c320..4af997017bba 100644 --- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml @@ -16,87 +16,66 @@ <com.android.systemui.biometrics.AuthCredentialPatternView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - android:gravity="center_horizontal" - android:elevation="@dimen/biometric_dialog_elevation"> + android:elevation="@dimen/biometric_dialog_elevation" + android:theme="?app:attr/lockPatternStyle"> <RelativeLayout + android:id="@+id/auth_credential_header" + style="?headerStyle" android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <LinearLayout - android:id="@+id/auth_credential_header" - style="@style/AuthCredentialHeaderStyle" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <ImageView - android:id="@+id/icon" - android:layout_width="48dp" - android:layout_height="48dp" - android:contentDescription="@null" /> - - <TextView - android:id="@+id/title" - style="@style/TextAppearance.AuthNonBioCredential.Title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - - <TextView - android:id="@+id/subtitle" - style="@style/TextAppearance.AuthNonBioCredential.Subtitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/icon" + style="?headerIconStyle" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:contentDescription="@null"/> + + <TextView + android:id="@+id/title" + style="?titleTextAppearance" + android:layout_below="@id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <TextView + android:id="@+id/subtitle" + style="?subTitleTextAppearance" + android:layout_below="@id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <TextView + android:id="@+id/description" + style="?descriptionTextAppearance" + android:layout_below="@id/subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + </RelativeLayout> - <TextView - android:id="@+id/description" - style="@style/TextAppearance.AuthNonBioCredential.Description" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - </LinearLayout> + <FrameLayout + android:id="@+id/auth_credential_container" + style="?containerStyle" + android:layout_width="match_parent" + android:layout_height="match_parent"> - <LinearLayout + <com.android.internal.widget.LockPatternView + android:id="@+id/lockPattern" + android:layout_gravity="center" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/auth_credential_header" - android:gravity="center" - android:orientation="vertical" - android:paddingBottom="16dp" - android:paddingTop="60dp"> + android:layout_height="match_parent"/> - <FrameLayout - style="@style/LockPatternContainerStyle" - android:layout_width="wrap_content" - android:layout_height="0dp" - android:layout_weight="1"> - - <com.android.internal.widget.LockPatternView - android:id="@+id/lockPattern" - style="@style/LockPatternStyle" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" /> - - </FrameLayout> - - </LinearLayout> - - <LinearLayout + <TextView + android:id="@+id/error" + style="?errorTextAppearance" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentBottom="true"> - - <TextView - android:id="@+id/error" - style="@style/TextAppearance.AuthNonBioCredential.Error" - android:layout_width="match_parent" - android:layout_height="wrap_content" /> - - </LinearLayout> - - </RelativeLayout> + android:layout_gravity="center_horizontal|bottom"/> + </FrameLayout> </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml index 4da77118f00b..bc97e511e7f4 100644 --- a/packages/SystemUI/res/layout/chipbar.xml +++ b/packages/SystemUI/res/layout/chipbar.xml @@ -19,12 +19,12 @@ <com.android.systemui.temporarydisplay.chipbar.ChipbarRootView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:id="@+id/media_ttt_sender_chip" + android:id="@+id/chipbar_root_view" android:layout_width="wrap_content" android:layout_height="wrap_content"> <LinearLayout - android:id="@+id/media_ttt_sender_chip_inner" + android:id="@+id/chipbar_inner" android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -39,7 +39,7 @@ > <com.android.internal.widget.CachingIconView - android:id="@+id/app_icon" + android:id="@+id/start_icon" android:layout_width="@dimen/media_ttt_app_icon_size" android:layout_height="@dimen/media_ttt_app_icon_size" android:layout_marginEnd="12dp" @@ -69,7 +69,7 @@ /> <ImageView - android:id="@+id/failure_icon" + android:id="@+id/error" android:layout_width="@dimen/media_ttt_status_icon_size" android:layout_height="@dimen/media_ttt_status_icon_size" android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" @@ -78,11 +78,11 @@ android:alpha="0.0" /> + <!-- TODO(b/245610654): Re-name all the media-specific dimens to chipbar dimens instead. --> <TextView - android:id="@+id/undo" + android:id="@+id/end_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/media_transfer_undo" android:textColor="?androidprv:attr/textColorOnAccent" android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" android:textSize="@dimen/media_ttt_text_size" diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 1a1fc75a41a1..0e9abee2f050 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<com.android.systemui.screenshot.DraggableConstraintLayout +<com.android.systemui.clipboardoverlay.ClipboardOverlayView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" @@ -157,4 +157,4 @@ android:layout_margin="@dimen/overlay_dismiss_button_margin" android:src="@drawable/overlay_cancel"/> </FrameLayout> -</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file +</com.android.systemui.clipboardoverlay.ClipboardOverlayView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml b/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml new file mode 100644 index 000000000000..1a1fc75a41a1 --- /dev/null +++ b/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.systemui.screenshot.DraggableConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/clipboard_ui" + android:theme="@style/FloatingOverlay" + android:alpha="0" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/clipboard_overlay_window_name"> + <ImageView + android:id="@+id/actions_container_background" + android:visibility="gone" + android:layout_height="0dp" + android:layout_width="0dp" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" + app:layout_constraintBottom_toBottomOf="@+id/actions_container" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/actions_container" + app:layout_constraintEnd_toEndOf="@+id/actions_container"/> + <HorizontalScrollView + android:id="@+id/actions_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" + android:paddingEnd="@dimen/overlay_action_container_padding_right" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:scrollbars="none" + android:layout_marginBottom="4dp" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_percent="1.0" + app:layout_constraintWidth_max="wrap" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@+id/preview_border" + app:layout_constraintEnd_toEndOf="parent"> + <LinearLayout + android:id="@+id/actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:animateLayoutChanges="true"> + <include layout="@layout/overlay_action_chip" + android:id="@+id/share_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/remote_copy_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/edit_chip"/> + </LinearLayout> + </HorizontalScrollView> + <View + android:id="@+id/preview_border" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="@dimen/overlay_offset_x" + android:layout_marginBottom="12dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + android:elevation="7dp" + app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end" + app:layout_constraintTop_toTopOf="@id/clipboard_preview_top" + android:background="@drawable/overlay_border"/> + <androidx.constraintlayout.widget.Barrier + android:id="@+id/clipboard_preview_end" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierMargin="@dimen/overlay_border_width" + app:barrierDirection="end" + app:constraint_referenced_ids="clipboard_preview"/> + <androidx.constraintlayout.widget.Barrier + android:id="@+id/clipboard_preview_top" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="top" + app:barrierMargin="@dimen/overlay_border_width_neg" + app:constraint_referenced_ids="clipboard_preview"/> + <FrameLayout + android:id="@+id/clipboard_preview" + android:elevation="7dp" + android:background="@drawable/overlay_preview_background" + android:clipChildren="true" + android:clipToOutline="true" + android:clipToPadding="true" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_margin="@dimen/overlay_border_width" + android:layout_height="wrap_content" + android:layout_gravity="center" + app:layout_constraintBottom_toBottomOf="@id/preview_border" + app:layout_constraintStart_toStartOf="@id/preview_border" + app:layout_constraintEnd_toEndOf="@id/preview_border" + app:layout_constraintTop_toTopOf="@id/preview_border"> + <TextView android:id="@+id/text_preview" + android:textFontWeight="500" + android:padding="8dp" + android:gravity="center|start" + android:ellipsize="end" + android:autoSizeTextType="uniform" + android:autoSizeMinTextSize="@dimen/clipboard_overlay_min_font" + android:autoSizeMaxTextSize="@dimen/clipboard_overlay_max_font" + android:textColor="?attr/overlayButtonTextColor" + android:textColorLink="?attr/overlayButtonTextColor" + android:background="?androidprv:attr/colorAccentSecondary" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_height="@dimen/clipboard_preview_size"/> + <ImageView + android:id="@+id/image_preview" + android:scaleType="fitCenter" + android:adjustViewBounds="true" + android:contentDescription="@string/clipboard_image_preview" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + <TextView + android:id="@+id/hidden_preview" + android:visibility="gone" + android:textFontWeight="500" + android:padding="8dp" + android:gravity="center" + android:textSize="14sp" + android:textColor="?attr/overlayButtonTextColor" + android:background="?androidprv:attr/colorAccentSecondary" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_height="@dimen/clipboard_preview_size"/> + </FrameLayout> + <FrameLayout + android:id="@+id/dismiss_button" + android:layout_width="@dimen/overlay_dismiss_button_tappable_size" + android:layout_height="@dimen/overlay_dismiss_button_tappable_size" + android:elevation="10dp" + android:visibility="gone" + android:alpha="0" + app:layout_constraintStart_toEndOf="@id/clipboard_preview" + app:layout_constraintEnd_toEndOf="@id/clipboard_preview" + app:layout_constraintTop_toTopOf="@id/clipboard_preview" + app:layout_constraintBottom_toTopOf="@id/clipboard_preview" + android:contentDescription="@string/clipboard_dismiss_description"> + <ImageView + android:id="@+id/dismiss_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="@dimen/overlay_dismiss_button_margin" + android:src="@drawable/overlay_cancel"/> + </FrameLayout> +</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index 5dc34b9db594..a565988c14ad 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -73,8 +73,8 @@ android:singleLine="true" android:textDirection="locale" android:textAppearance="@style/TextAppearance.QS.Status" - android:transformPivotX="0sp" - android:transformPivotY="20sp" + android:transformPivotX="0dp" + android:transformPivotY="24dp" android:scaleX="1" android:scaleY="1" /> diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index 50d3cc4e8a7a..715c86957e02 100644 --- a/packages/SystemUI/res/layout/media_carousel.xml +++ b/packages/SystemUI/res/layout/media_carousel.xml @@ -24,7 +24,7 @@ android:clipToPadding="false" android:forceHasOverlappingRendering="false" android:theme="@style/MediaPlayer"> - <com.android.systemui.media.MediaScrollView + <com.android.systemui.media.controls.ui.MediaScrollView android:id="@+id/media_carousel_scroller" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -42,7 +42,7 @@ > <!-- QSMediaPlayers will be added here dynamically --> </LinearLayout> - </com.android.systemui.media.MediaScrollView> + </com.android.systemui.media.controls.ui.MediaScrollView> <com.android.systemui.qs.PageIndicator android:id="@+id/media_page_indicator" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml index 2fb6d6cb9bd5..9fc3f409642b 100644 --- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml +++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml @@ -23,6 +23,7 @@ android:layout_height="wrap_content" android:layout_gravity="@integer/notification_panel_layout_gravity" android:background="@android:color/transparent" + android:importantForAccessibility="no" android:baselineAligned="false" android:clickable="false" android:clipChildren="false" @@ -56,7 +57,7 @@ android:clipToPadding="false" android:focusable="true" android:paddingBottom="@dimen/qqs_layout_padding_bottom" - android:importantForAccessibility="yes"> + android:importantForAccessibility="no"> </com.android.systemui.qs.QuickQSPanel> </RelativeLayout> diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml index 60bc3732cde0..8b5d953c3fe7 100644 --- a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml +++ b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml @@ -25,6 +25,7 @@ android:gravity="center" android:layout_gravity="top" android:orientation="horizontal" + android:importantForAccessibility="no" android:clickable="true" android:minHeight="48dp"> diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml index 9c027495aa1e..1ac78d491d78 100644 --- a/packages/SystemUI/res/layout/screenshot_static.xml +++ b/packages/SystemUI/res/layout/screenshot_static.xml @@ -103,8 +103,18 @@ app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" app:layout_constraintStart_toStartOf="@id/screenshot_preview_border" app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border" - app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"> - </ImageView> + app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"/> + <ImageView + android:id="@+id/screenshot_badge" + android:layout_width="24dp" + android:layout_height="24dp" + android:padding="4dp" + android:visibility="gone" + android:background="@drawable/overlay_badge_background" + android:elevation="8dp" + android:src="@drawable/overlay_cancel" + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/> <FrameLayout android:id="@+id/screenshot_dismiss_button" android:layout_width="@dimen/overlay_dismiss_button_tappable_size" diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml index f0e49d5c2011..159323a5b557 100644 --- a/packages/SystemUI/res/layout/status_bar_expanded.xml +++ b/packages/SystemUI/res/layout/status_bar_expanded.xml @@ -32,41 +32,8 @@ android:layout_height="match_parent" android:layout_width="match_parent" /> - <include - layout="@layout/keyguard_bottom_area" - android:visibility="gone" /> - - <ViewStub - android:id="@+id/keyguard_user_switcher_stub" - android:layout="@layout/keyguard_user_switcher" - android:layout_height="match_parent" - android:layout_width="match_parent" /> - <include layout="@layout/status_bar_expanded_plugin_frame"/> - <include layout="@layout/dock_info_bottom_area_overlay" /> - - <com.android.keyguard.LockIconView - android:id="@+id/lock_icon_view" - android:layout_width="wrap_content" - android:layout_height="wrap_content"> - <!-- Background protection --> - <ImageView - android:id="@+id/lock_icon_bg" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@drawable/fingerprint_bg" - android:visibility="invisible"/> - - <ImageView - android:id="@+id/lock_icon" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - android:scaleType="centerCrop"/> - - </com.android.keyguard.LockIconView> - <com.android.systemui.shade.NotificationsQuickSettingsContainer android:layout_width="match_parent" android:layout_height="match_parent" @@ -75,12 +42,6 @@ android:clipToPadding="false" android:clipChildren="false"> - <ViewStub - android:id="@+id/qs_header_stub" - android:layout_height="wrap_content" - android:layout_width="match_parent" - /> - <include layout="@layout/keyguard_status_view" android:visibility="gone"/> @@ -102,6 +63,15 @@ systemui:layout_constraintBottom_toBottomOf="parent" /> + <!-- This view should be after qs_frame so touches are dispatched first to it. That gives + it a chance to capture clicks before the NonInterceptingScrollView disallows all + intercepts --> + <ViewStub + android:id="@+id/qs_header_stub" + android:layout_height="wrap_content" + android:layout_width="match_parent" + /> + <androidx.constraintlayout.widget.Guideline android:id="@+id/qs_edge_guideline" android:layout_width="wrap_content" @@ -145,6 +115,39 @@ /> </com.android.systemui.shade.NotificationsQuickSettingsContainer> + <include + layout="@layout/keyguard_bottom_area" + android:visibility="gone" /> + + <ViewStub + android:id="@+id/keyguard_user_switcher_stub" + android:layout="@layout/keyguard_user_switcher" + android:layout_height="match_parent" + android:layout_width="match_parent" /> + + <include layout="@layout/dock_info_bottom_area_overlay" /> + + <com.android.keyguard.LockIconView + android:id="@+id/lock_icon_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + <!-- Background protection --> + <ImageView + android:id="@+id/lock_icon_bg" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/fingerprint_bg" + android:visibility="invisible"/> + + <ImageView + android:id="@+id/lock_icon" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:scaleType="centerCrop"/> + + </com.android.keyguard.LockIconView> + <FrameLayout android:id="@+id/preview_container" android:layout_width="match_parent" diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml index 89191984b9e8..aefd9981d02e 100644 --- a/packages/SystemUI/res/values-land/styles.xml +++ b/packages/SystemUI/res/values-land/styles.xml @@ -18,4 +18,42 @@ <style name="BrightnessDialogContainer" parent="@style/BaseBrightnessDialogContainer"> <item name="android:layout_width">360dp</item> </style> + + <style name="AuthCredentialHeaderStyle"> + <item name="android:paddingStart">48dp</item> + <item name="android:paddingEnd">24dp</item> + <item name="android:paddingTop">48dp</item> + <item name="android:paddingBottom">10dp</item> + <item name="android:gravity">top|left</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">320dp</item> + <item name="android:maxWidth">320dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">60dp</item> + <item name="android:paddingVertical">20dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:textSize">36dp</item> + <item name="android:focusable">true</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Subtitle"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:textSize">18sp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Description"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:textSize">18sp</item> + </style> + </resources> diff --git a/packages/SystemUI/res/values-sw600dp-land/styles.xml b/packages/SystemUI/res/values-sw600dp-land/styles.xml new file mode 100644 index 000000000000..8148d3dfaf7d --- /dev/null +++ b/packages/SystemUI/res/values-sw600dp-land/styles.xml @@ -0,0 +1,47 @@ +<?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. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">120dp</item> + <item name="android:paddingVertical">40dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Subtitle"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Description"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> +</resources> diff --git a/packages/SystemUI/res/values-sw600dp-port/styles.xml b/packages/SystemUI/res/values-sw600dp-port/styles.xml new file mode 100644 index 000000000000..771de08cb360 --- /dev/null +++ b/packages/SystemUI/res/values-sw600dp-port/styles.xml @@ -0,0 +1,44 @@ +<?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. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialHeaderStyle"> + <item name="android:paddingStart">120dp</item> + <item name="android:paddingEnd">120dp</item> + <item name="android:paddingTop">80dp</item> + <item name="android:paddingBottom">10dp</item> + <item name="android:layout_gravity">top</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">180dp</item> + <item name="android:paddingVertical">80dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">24dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + +</resources> diff --git a/packages/SystemUI/res/values-sw720dp-land/styles.xml b/packages/SystemUI/res/values-sw720dp-land/styles.xml new file mode 100644 index 000000000000..f9ed67d89de7 --- /dev/null +++ b/packages/SystemUI/res/values-sw720dp-land/styles.xml @@ -0,0 +1,48 @@ +<?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. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">120dp</item> + <item name="android:paddingVertical">40dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Subtitle"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Description"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> + +</resources> diff --git a/packages/SystemUI/res/values-sw720dp-port/styles.xml b/packages/SystemUI/res/values-sw720dp-port/styles.xml new file mode 100644 index 000000000000..78d299c483e6 --- /dev/null +++ b/packages/SystemUI/res/values-sw720dp-port/styles.xml @@ -0,0 +1,44 @@ +<?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. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialHeaderStyle"> + <item name="android:paddingStart">120dp</item> + <item name="android:paddingEnd">120dp</item> + <item name="android:paddingTop">80dp</item> + <item name="android:paddingBottom">10dp</item> + <item name="android:layout_gravity">top</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">240dp</item> + <item name="android:paddingVertical">120dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">24dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + +</resources> diff --git a/packages/SystemUI/res/values-zh-rTW/strings.xml b/packages/SystemUI/res/values-zh-rTW/strings.xml index bbe4b6c70c1c..f5fc7ee53f2e 100644 --- a/packages/SystemUI/res/values-zh-rTW/strings.xml +++ b/packages/SystemUI/res/values-zh-rTW/strings.xml @@ -672,7 +672,7 @@ <string name="data_connection_no_internet" msgid="691058178914184544">"沒有網際網路連線"</string> <string name="accessibility_quick_settings_open_settings" msgid="536838345505030893">"開啟「<xliff:g id="ID_1">%s</xliff:g>」設定。"</string> <string name="accessibility_quick_settings_edit" msgid="1523745183383815910">"編輯設定順序。"</string> - <string name="accessibility_quick_settings_power_menu" msgid="6820426108301758412">"電源按鈕選單"</string> + <string name="accessibility_quick_settings_power_menu" msgid="6820426108301758412">"電源鍵選單"</string> <string name="accessibility_quick_settings_page" msgid="7506322631645550961">"第 <xliff:g id="ID_1">%1$d</xliff:g> 頁,共 <xliff:g id="ID_2">%2$d</xliff:g> 頁"</string> <string name="tuner_lock_screen" msgid="2267383813241144544">"鎖定畫面"</string> <string name="thermal_shutdown_title" msgid="2702966892682930264">"手機先前過熱,因此關閉電源"</string> diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index 9a71995383ac..df0659d67afe 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -191,5 +191,18 @@ <declare-styleable name="DelayableMarqueeTextView"> <attr name="marqueeDelay" format="integer" /> </declare-styleable> + + <declare-styleable name="AuthCredentialView"> + <attr name="lockPatternStyle" format="reference" /> + <attr name="lockPinPasswordStyle" format="reference" /> + <attr name="containerStyle" format="reference" /> + <attr name="headerStyle" format="reference" /> + <attr name="headerIconStyle" format="reference" /> + <attr name="titleTextAppearance" format="reference" /> + <attr name="subTitleTextAppearance" format="reference" /> + <attr name="descriptionTextAppearance" format="reference" /> + <attr name="passwordTextAppearance" format="reference" /> + <attr name="errorTextAppearance" format="reference"/> + </declare-styleable> </resources> diff --git a/packages/SystemUI/res/values/bools.xml b/packages/SystemUI/res/values/bools.xml index c67ac8d34aa6..8221d78fbfd7 100644 --- a/packages/SystemUI/res/values/bools.xml +++ b/packages/SystemUI/res/values/bools.xml @@ -18,6 +18,13 @@ <resources> <!-- Whether to show the user switcher in quick settings when only a single user is present. --> <bool name="qs_show_user_switcher_for_single_user">false</bool> + <!-- Whether to show a custom biometric prompt size--> <bool name="use_custom_bp_size">false</bool> + + <!-- Whether to enable clipping on Quick Settings --> + <bool name="qs_enable_clipping">true</bool> + + <!-- Whether to enable transparent background for notification scrims --> + <bool name="notification_scrim_transparent">false</bool> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 2eebdc61186c..93926ef9e780 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -519,7 +519,7 @@ <dimen name="qs_tile_margin_horizontal">8dp</dimen> <dimen name="qs_tile_margin_vertical">@dimen/qs_tile_margin_horizontal</dimen> <dimen name="qs_tile_margin_top_bottom">4dp</dimen> - <dimen name="qs_brightness_margin_top">8dp</dimen> + <dimen name="qs_brightness_margin_top">12dp</dimen> <dimen name="qs_brightness_margin_bottom">16dp</dimen> <dimen name="qqs_layout_margin_top">16dp</dimen> <dimen name="qqs_layout_padding_bottom">24dp</dimen> @@ -572,6 +572,7 @@ <dimen name="qs_header_row_min_height">48dp</dimen> <dimen name="qs_header_non_clickable_element_height">24dp</dimen> + <dimen name="new_qs_header_non_clickable_element_height">20dp</dimen> <dimen name="qs_footer_padding">20dp</dimen> <dimen name="qs_security_footer_height">88dp</dimen> @@ -949,6 +950,9 @@ <dimen name="biometric_dialog_width">240dp</dimen> <dimen name="biometric_dialog_height">240dp</dimen> + <!-- Biometric Auth Credential values --> + <dimen name="biometric_auth_icon_size">48dp</dimen> + <!-- Starting text size in sp of batteryLevel for wireless charging animation --> <item name="wireless_charging_anim_battery_level_text_size_start" format="float" type="dimen"> 0 diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml index 3164ed1e6751..e30d4415a0c4 100644 --- a/packages/SystemUI/res/values/integers.xml +++ b/packages/SystemUI/res/values/integers.xml @@ -28,4 +28,11 @@ <!-- The time it takes for the over scroll release animation to complete, in milli seconds. --> <integer name="lockscreen_shade_over_scroll_release_duration">0</integer> + + <!-- Values for transition of QS Headers --> + <integer name="fade_out_complete_frame">14</integer> + <integer name="fade_in_start_frame">58</integer> + <!-- Percentage of displacement for items in QQS to guarantee matching with bottom of clock at + fade_out_complete_frame --> + <dimen name="percent_displacement_at_fade_out" format="float">0.1066</dimen> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 9b9111fcf9a8..212c77b50477 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -641,7 +641,7 @@ <!-- QuickSettings: Label for the toggle that controls whether display color correction is enabled. [CHAR LIMIT=NONE] --> <string name="quick_settings_color_correction_label">Color correction</string> <!-- QuickSettings: Control panel: Label for button that navigates to user settings. [CHAR LIMIT=NONE] --> - <string name="quick_settings_more_user_settings">User settings</string> + <string name="quick_settings_more_user_settings">Manage users</string> <!-- QuickSettings: Control panel: Label for button that dismisses control panel. [CHAR LIMIT=NONE] --> <string name="quick_settings_done">Done</string> <!-- QuickSettings: Control panel: Label for button that dismisses user switcher control panel. [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index ac3eb7e18539..e76887babc50 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -128,11 +128,10 @@ <!-- This is hard coded to be sans-serif-condensed to match the icons --> <style name="TextAppearance.QS.Status"> - <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item> + <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> <item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textSize">14sp</item> <item name="android:letterSpacing">0.01</item> - <item name="android:lineHeight">20sp</item> </style> <style name="TextAppearance.QS.SecurityFooter" parent="@style/TextAppearance.QS.Status"> @@ -143,12 +142,10 @@ <style name="TextAppearance.QS.Status.Carriers" /> <style name="TextAppearance.QS.Status.Carriers.NoCarrierText"> - <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item> <item name="android:textColor">?android:attr/textColorSecondary</item> </style> <style name="TextAppearance.QS.Status.Build"> - <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item> <item name="android:textColor">?android:attr/textColorSecondary</item> </style> @@ -200,8 +197,9 @@ <style name="TextAppearance.AuthNonBioCredential.Title"> <item name="android:fontFamily">google-sans</item> - <item name="android:layout_marginTop">20dp</item> - <item name="android:textSize">36sp</item> + <item name="android:layout_marginTop">24dp</item> + <item name="android:textSize">36dp</item> + <item name="android:focusable">true</item> </style> <style name="TextAppearance.AuthNonBioCredential.Subtitle"> @@ -213,12 +211,10 @@ <style name="TextAppearance.AuthNonBioCredential.Description"> <item name="android:fontFamily">google-sans</item> <item name="android:layout_marginTop">20dp</item> - <item name="android:textSize">16sp</item> + <item name="android:textSize">18sp</item> </style> <style name="TextAppearance.AuthNonBioCredential.Error"> - <item name="android:paddingTop">6dp</item> - <item name="android:paddingBottom">18dp</item> <item name="android:paddingHorizontal">24dp</item> <item name="android:textSize">14sp</item> <item name="android:textColor">?android:attr/colorError</item> @@ -227,20 +223,43 @@ <style name="TextAppearance.AuthCredential.PasswordEntry" parent="@android:style/TextAppearance.DeviceDefault"> <item name="android:gravity">center</item> + <item name="android:paddingTop">28dp</item> <item name="android:singleLine">true</item> <item name="android:textColor">?android:attr/colorForeground</item> <item name="android:textSize">24sp</item> + <item name="android:background">@drawable/edit_text_filled</item> </style> <style name="AuthCredentialHeaderStyle"> <item name="android:paddingStart">48dp</item> - <item name="android:paddingEnd">24dp</item> - <item name="android:paddingTop">28dp</item> - <item name="android:paddingBottom">20dp</item> - <item name="android:orientation">vertical</item> + <item name="android:paddingEnd">48dp</item> + <item name="android:paddingTop">48dp</item> + <item name="android:paddingBottom">10dp</item> <item name="android:layout_gravity">top</item> </style> + <style name="AuthCredentialIconStyle"> + <item name="android:layout_width">@dimen/biometric_auth_icon_size</item> + <item name="android:layout_height">@dimen/biometric_auth_icon_size</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:padding">20dp</item> + </style> + + <style name="AuthCredentialPinPasswordContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">48dp</item> + <item name="android:maxWidth">600dp</item> + <item name="android:minHeight">48dp</item> + <item name="android:minWidth">200dp</item> + </style> + <style name="DeviceManagementDialogTitle"> <item name="android:gravity">center</item> <item name="android:textAppearance">@style/TextAppearance.DeviceManagementDialog.Title</item> @@ -278,7 +297,9 @@ <item name="wallpaperTextColorSecondary">@*android:color/secondary_text_material_dark</item> <item name="wallpaperTextColorAccent">@color/material_dynamic_primary90</item> <item name="android:colorError">@*android:color/error_color_material_dark</item> - <item name="*android:lockPatternStyle">@style/LockPatternStyle</item> + <item name="*android:lockPatternStyle">@style/LockPatternViewStyle</item> + <item name="lockPatternStyle">@style/LockPatternContainerStyle</item> + <item name="lockPinPasswordStyle">@style/LockPinPasswordContainerStyle</item> <item name="passwordStyle">@style/PasswordTheme</item> <item name="numPadKeyStyle">@style/NumPadKey</item> <item name="backgroundProtectedStyle">@style/BackgroundProtectedStyle</item> @@ -304,27 +325,33 @@ <item name="android:textColor">?attr/wallpaperTextColor</item> </style> - <style name="LockPatternContainerStyle"> - <item name="android:maxHeight">400dp</item> - <item name="android:maxWidth">420dp</item> - <item name="android:minHeight">0dp</item> - <item name="android:minWidth">0dp</item> - <item name="android:paddingHorizontal">60dp</item> - <item name="android:paddingBottom">40dp</item> + <style name="AuthCredentialStyle"> + <item name="*android:regularColor">?android:attr/colorForeground</item> + <item name="*android:successColor">?android:attr/colorForeground</item> + <item name="*android:errorColor">?android:attr/colorError</item> + <item name="*android:dotColor">?android:attr/textColorSecondary</item> + <item name="headerStyle">@style/AuthCredentialHeaderStyle</item> + <item name="headerIconStyle">@style/AuthCredentialIconStyle</item> + <item name="titleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Title</item> + <item name="subTitleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Subtitle</item> + <item name="descriptionTextAppearance">@style/TextAppearance.AuthNonBioCredential.Description</item> + <item name="passwordTextAppearance">@style/TextAppearance.AuthCredential.PasswordEntry</item> + <item name="errorTextAppearance">@style/TextAppearance.AuthNonBioCredential.Error</item> </style> - <style name="LockPatternStyle"> + <style name="LockPatternViewStyle" > <item name="*android:regularColor">?android:attr/colorAccent</item> <item name="*android:successColor">?android:attr/textColorPrimary</item> <item name="*android:errorColor">?android:attr/colorError</item> <item name="*android:dotColor">?android:attr/textColorSecondary</item> </style> - <style name="LockPatternStyleBiometricPrompt"> - <item name="*android:regularColor">?android:attr/colorForeground</item> - <item name="*android:successColor">?android:attr/colorForeground</item> - <item name="*android:errorColor">?android:attr/colorError</item> - <item name="*android:dotColor">?android:attr/textColorSecondary</item> + <style name="LockPatternContainerStyle" parent="@style/AuthCredentialStyle"> + <item name="containerStyle">@style/AuthCredentialPatternContainerStyle</item> + </style> + + <style name="LockPinPasswordContainerStyle" parent="@style/AuthCredentialStyle"> + <item name="containerStyle">@style/AuthCredentialPinPasswordContainerStyle</item> </style> <style name="Theme.SystemUI.QuickSettings" parent="@*android:style/Theme.DeviceDefault"> diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml index f3866c08cbfc..de855e275f5f 100644 --- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml +++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml @@ -27,67 +27,60 @@ <KeyPosition app:keyPositionType="deltaRelative" app:percentX="0" - app:percentY="0" - app:framePosition="49" + app:percentY="@dimen/percent_displacement_at_fade_out" + app:framePosition="@integer/fade_out_complete_frame" app:sizePercent="0" app:curveFit="linear" app:motionTarget="@id/date" /> <KeyPosition app:keyPositionType="deltaRelative" app:percentX="1" - app:percentY="0.51" + app:percentY="0.5" app:sizePercent="1" - app:framePosition="51" + app:framePosition="50" app:curveFit="linear" app:motionTarget="@id/date" /> <KeyAttribute app:motionTarget="@id/date" - app:framePosition="30" + app:framePosition="14" android:alpha="0" /> <KeyAttribute app:motionTarget="@id/date" - app:framePosition="70" + app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> <KeyPosition - app:keyPositionType="pathRelative" - app:percentX="0" - app:percentY="0" - app:framePosition="0" - app:curveFit="linear" - app:motionTarget="@id/statusIcons" /> - <KeyPosition - app:keyPositionType="pathRelative" + app:keyPositionType="deltaRelative" app:percentX="0" - app:percentY="0" - app:framePosition="50" + app:percentY="@dimen/percent_displacement_at_fade_out" + app:framePosition="@integer/fade_out_complete_frame" app:sizePercent="0" app:curveFit="linear" app:motionTarget="@id/statusIcons" /> <KeyPosition app:keyPositionType="deltaRelative" app:percentX="1" - app:percentY="0.51" - app:framePosition="51" + app:percentY="0.5" + app:framePosition="50" app:sizePercent="1" app:curveFit="linear" app:motionTarget="@id/statusIcons" /> <KeyAttribute app:motionTarget="@id/statusIcons" - app:framePosition="30" + app:framePosition="@integer/fade_out_complete_frame" android:alpha="0" /> <KeyAttribute app:motionTarget="@id/statusIcons" - app:framePosition="70" + app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> <KeyPosition app:keyPositionType="deltaRelative" app:percentX="0" - app:percentY="0" - app:framePosition="50" + app:percentY="@dimen/percent_displacement_at_fade_out" + app:framePosition="@integer/fade_out_complete_frame" app:percentWidth="1" app:percentHeight="1" app:curveFit="linear" @@ -95,27 +88,27 @@ <KeyPosition app:keyPositionType="deltaRelative" app:percentX="1" - app:percentY="0.51" - app:framePosition="51" + app:percentY="0.5" + app:framePosition="50" app:percentWidth="1" app:percentHeight="1" app:curveFit="linear" app:motionTarget="@id/batteryRemainingIcon" /> <KeyAttribute app:motionTarget="@id/batteryRemainingIcon" - app:framePosition="30" + app:framePosition="@integer/fade_out_complete_frame" android:alpha="0" /> <KeyAttribute app:motionTarget="@id/batteryRemainingIcon" - app:framePosition="70" + app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> <KeyPosition app:motionTarget="@id/carrier_group" app:percentX="1" - app:percentY="0.51" - app:framePosition="51" + app:percentY="0.5" + app:framePosition="50" app:percentWidth="1" app:percentHeight="1" app:curveFit="linear" @@ -126,7 +119,7 @@ android:alpha="0" /> <KeyAttribute app:motionTarget="@id/carrier_group" - app:framePosition="70" + app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> </KeyFrameSet> </Transition> diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml index a82684d0358b..88b4f43b440b 100644 --- a/packages/SystemUI/res/xml/qqs_header.xml +++ b/packages/SystemUI/res/xml/qqs_header.xml @@ -43,7 +43,8 @@ android:id="@+id/date"> <Layout android:layout_width="0dp" - android:layout_height="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + android:layout_marginStart="8dp" app:layout_constrainedWidth="true" app:layout_constraintStart_toEndOf="@id/clock" app:layout_constraintEnd_toStartOf="@id/barrier" @@ -57,8 +58,8 @@ android:id="@+id/statusIcons"> <Layout android:layout_width="0dp" - android:layout_height="@dimen/qs_header_non_clickable_element_height" - app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toEndOf="@id/date" app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" app:layout_constraintTop_toTopOf="parent" @@ -71,9 +72,9 @@ android:id="@+id/batteryRemainingIcon"> <Layout android:layout_width="wrap_content" - android:layout_height="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" app:layout_constrainedWidth="true" - app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height" + app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toEndOf="@id/statusIcons" app:layout_constraintEnd_toEndOf="@id/end_guide" app:layout_constraintTop_toTopOf="parent" diff --git a/packages/SystemUI/res/xml/qs_header_new.xml b/packages/SystemUI/res/xml/qs_header_new.xml index f39e6bd65b86..d8a4e7752960 100644 --- a/packages/SystemUI/res/xml/qs_header_new.xml +++ b/packages/SystemUI/res/xml/qs_header_new.xml @@ -40,13 +40,13 @@ android:layout_height="@dimen/large_screen_shade_header_min_height" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/privacy_container" - app:layout_constraintBottom_toTopOf="@id/date" + app:layout_constraintBottom_toBottomOf="@id/carrier_group" app:layout_constraintEnd_toStartOf="@id/carrier_group" app:layout_constraintHorizontal_bias="0" /> <Transform - android:scaleX="2.4" - android:scaleY="2.4" + android:scaleX="2.57" + android:scaleY="2.57" /> </Constraint> @@ -54,11 +54,11 @@ android:id="@+id/date"> <Layout android:layout_width="0dp" - android:layout_height="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/space" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintTop_toBottomOf="@id/clock" + app:layout_constraintTop_toBottomOf="@id/carrier_group" app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_chainStyle="spread_inside" /> @@ -87,7 +87,7 @@ android:id="@+id/statusIcons"> <Layout android:layout_width="0dp" - android:layout_height="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" app:layout_constrainedWidth="true" app:layout_constraintStart_toEndOf="@id/space" app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" @@ -101,8 +101,8 @@ android:id="@+id/batteryRemainingIcon"> <Layout android:layout_width="wrap_content" - android:layout_height="@dimen/qs_header_non_clickable_element_height" - app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toEndOf="@id/statusIcons" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/date" diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt index 2e391c7aacbe..49cc48321d77 100644 --- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt @@ -19,6 +19,7 @@ package com.android.systemui.testing.screenshot import android.app.Activity import android.graphics.Color import android.view.View +import android.view.Window import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat @@ -51,13 +52,14 @@ class ExternalViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestR /** * Compare the content of the [view] with the golden image identified by [goldenIdentifier] in - * the context of [emulationSpec]. + * the context of [emulationSpec]. Window must be specified to capture views that render + * hardware buffers. */ - fun screenshotTest(goldenIdentifier: String, view: View) { + fun screenshotTest(goldenIdentifier: String, view: View, window: Window? = null) { view.removeElevationRecursively() ScreenshotRuleAsserter.Builder(screenshotRule) - .setScreenshotProvider { view.toBitmap() } + .setScreenshotProvider { view.toBitmap(window) } .withMatcher(matcher) .build() .assertGoldenImage(goldenIdentifier) @@ -94,6 +96,6 @@ class ExternalViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestR activity.currentFocus?.clearFocus() } - screenshotTest(goldenIdentifier, rootView) + screenshotTest(goldenIdentifier, rootView, activity.window) } } diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index 18bd6b42386a..91fd6a60d0df 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -59,7 +59,9 @@ android_library { resource_dirs: [ "res", ], - java_version: "1.8", + optimize: { + proguard_flags_files: ["proguard.flags"], + }, min_sdk_version: "current", plugins: ["dagger2-compiler"], } diff --git a/packages/SystemUI/shared/proguard.flags b/packages/SystemUI/shared/proguard.flags new file mode 100644 index 000000000000..5eda04500190 --- /dev/null +++ b/packages/SystemUI/shared/proguard.flags @@ -0,0 +1,4 @@ +# Retain signatures of TypeToken and its subclasses for gson usage in ClockRegistry +-keepattributes Signature +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 860a5da44088..236aa669eaa9 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -20,25 +20,28 @@ import android.annotation.ColorInt import android.annotation.FloatRange import android.annotation.IntRange import android.annotation.SuppressLint -import android.app.compat.ChangeIdStateCache.invalidate import android.content.Context import android.graphics.Canvas +import android.graphics.Rect import android.text.Layout import android.text.TextUtils import android.text.format.DateFormat import android.util.AttributeSet +import android.util.MathUtils import android.widget.TextView -import com.android.internal.R.attr.contentDescription -import com.android.internal.R.attr.format import com.android.internal.annotations.VisibleForTesting import com.android.systemui.animation.GlyphCallback import com.android.systemui.animation.Interpolators import com.android.systemui.animation.TextAnimator +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG import com.android.systemui.shared.R import java.io.PrintWriter import java.util.Calendar import java.util.Locale import java.util.TimeZone +import kotlin.math.max +import kotlin.math.min /** * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30) @@ -51,14 +54,8 @@ class AnimatableClockView @JvmOverloads constructor( defStyleAttr: Int = 0, defStyleRes: Int = 0 ) : TextView(context, attrs, defStyleAttr, defStyleRes) { - - private var lastMeasureCall: CharSequence? = null - private var lastDraw: CharSequence? = null - private var lastTextUpdate: CharSequence? = null - private var lastOnTextChanged: CharSequence? = null - private var lastInvalidate: CharSequence? = null - private var lastTimeZoneChange: CharSequence? = null - private var lastAnimationCall: CharSequence? = null + var tag: String = "UnnamedClockView" + var logBuffer: LogBuffer? = null private val time = Calendar.getInstance() @@ -135,6 +132,7 @@ class AnimatableClockView @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() + logBuffer?.log(tag, DEBUG, "onAttachedToWindow") refreshFormat() } @@ -150,27 +148,39 @@ class AnimatableClockView @JvmOverloads constructor( time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis() contentDescription = DateFormat.format(descFormat, time) val formattedText = DateFormat.format(format, time) + logBuffer?.log(tag, DEBUG, + { str1 = formattedText?.toString() }, + { "refreshTime: new formattedText=$str1" } + ) // Setting text actually triggers a layout pass (because the text view is set to // wrap_content width and TextView always relayouts for this). Avoid needless // relayout if the text didn't actually change. if (!TextUtils.equals(text, formattedText)) { text = formattedText + logBuffer?.log(tag, DEBUG, + { str1 = formattedText?.toString() }, + { "refreshTime: done setting new time text to: $str1" } + ) // Because the TextLayout may mutate under the hood as a result of the new text, we // notify the TextAnimator that it may have changed and request a measure/layout. A // crash will occur on the next invocation of setTextStyle if the layout is mutated // without being notified TextInterpolator being notified. if (layout != null) { textAnimator?.updateLayout(layout) + logBuffer?.log(tag, DEBUG, "refreshTime: done updating textAnimator layout") } requestLayout() - lastTextUpdate = getTimestamp() + logBuffer?.log(tag, DEBUG, "refreshTime: after requestLayout") } } fun onTimeZoneChanged(timeZone: TimeZone?) { time.timeZone = timeZone refreshFormat() - lastTimeZoneChange = "${getTimestamp()} timeZone=${time.timeZone}" + logBuffer?.log(tag, DEBUG, + { str1 = timeZone?.toString() }, + { "onTimeZoneChanged newTimeZone=$str1" } + ) } @SuppressLint("DrawAllocation") @@ -184,22 +194,24 @@ class AnimatableClockView @JvmOverloads constructor( } else { animator.updateLayout(layout) } - lastMeasureCall = getTimestamp() + logBuffer?.log(tag, DEBUG, "onMeasure") } override fun onDraw(canvas: Canvas) { - lastDraw = getTimestamp() - // intentionally doesn't call super.onDraw here or else the text will be rendered twice - textAnimator?.draw(canvas) + // Use textAnimator to render text if animation is enabled. + // Otherwise default to using standard draw functions. + if (isAnimationEnabled) { + // intentionally doesn't call super.onDraw here or else the text will be rendered twice + textAnimator?.draw(canvas) + } else { + super.onDraw(canvas) + } + logBuffer?.log(tag, DEBUG, "onDraw lastDraw") } override fun invalidate() { super.invalidate() - lastInvalidate = getTimestamp() - } - - private fun getTimestamp(): CharSequence { - return "${DateFormat.format("HH:mm:ss", System.currentTimeMillis())} text=$text" + logBuffer?.log(tag, DEBUG, "invalidate") } override fun onTextChanged( @@ -209,7 +221,10 @@ class AnimatableClockView @JvmOverloads constructor( lengthAfter: Int ) { super.onTextChanged(text, start, lengthBefore, lengthAfter) - lastOnTextChanged = "${getTimestamp()}" + logBuffer?.log(tag, DEBUG, + { str1 = text.toString() }, + { "onTextChanged text=$str1" } + ) } fun setLineSpacingScale(scale: Float) { @@ -223,7 +238,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateAppearOnLockscreen() { - lastAnimationCall = "${getTimestamp()} call=animateAppearOnLockscreen" + logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen") setTextStyle( weight = dozingWeight, textSize = -1f, @@ -248,7 +263,7 @@ class AnimatableClockView @JvmOverloads constructor( if (isAnimationEnabled && textAnimator == null) { return } - lastAnimationCall = "${getTimestamp()} call=animateFoldAppear" + logBuffer?.log(tag, DEBUG, "animateFoldAppear") setTextStyle( weight = lockScreenWeightInternal, textSize = -1f, @@ -275,7 +290,7 @@ class AnimatableClockView @JvmOverloads constructor( // Skip charge animation if dozing animation is already playing. return } - lastAnimationCall = "${getTimestamp()} call=animateCharge" + logBuffer?.log(tag, DEBUG, "animateCharge") val startAnimPhase2 = Runnable { setTextStyle( weight = if (isDozing()) dozingWeight else lockScreenWeight, @@ -299,7 +314,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateDoze(isDozing: Boolean, animate: Boolean) { - lastAnimationCall = "${getTimestamp()} call=animateDoze" + logBuffer?.log(tag, DEBUG, "animateDoze") setTextStyle( weight = if (isDozing) dozingWeight else lockScreenWeight, textSize = -1f, @@ -311,7 +326,24 @@ class AnimatableClockView @JvmOverloads constructor( ) } - private val glyphFilter: GlyphCallback? = null // Add text animation tweak here. + // The offset of each glyph from where it should be. + private var glyphOffsets = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f) + + private var lastSeenAnimationProgress = 1.0f + + // If the animation is being reversed, the target offset for each glyph for the "stop". + private var animationCancelStartPosition = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f) + private var animationCancelStopPosition = 0.0f + + // Whether the currently playing animation needed a stop (and thus, is shortened). + private var currentAnimationNeededStop = false + + private val glyphFilter: GlyphCallback = { positionedGlyph, _ -> + val offset = positionedGlyph.lineNo * DIGITS_PER_LINE + positionedGlyph.glyphIndex + if (offset < glyphOffsets.size) { + positionedGlyph.x += glyphOffsets[offset] + } + } /** * Set text style with an optional animation. @@ -345,6 +377,9 @@ class AnimatableClockView @JvmOverloads constructor( onAnimationEnd = onAnimationEnd ) textAnimator?.glyphFilter = glyphFilter + if (color != null && !isAnimationEnabled) { + setTextColor(color) + } } else { // when the text animator is set, update its start values onTextAnimatorInitialized = Runnable { @@ -359,6 +394,9 @@ class AnimatableClockView @JvmOverloads constructor( onAnimationEnd = onAnimationEnd ) textAnimator?.glyphFilter = glyphFilter + if (color != null && !isAnimationEnabled) { + setTextColor(color) + } } } } @@ -394,9 +432,12 @@ class AnimatableClockView @JvmOverloads constructor( isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12 else -> DOUBLE_LINE_FORMAT_12_HOUR } + logBuffer?.log(tag, DEBUG, + { str1 = format?.toString() }, + { "refreshFormat format=$str1" } + ) descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12 - refreshTime() } @@ -405,15 +446,8 @@ class AnimatableClockView @JvmOverloads constructor( pw.println(" measuredWidth=$measuredWidth") pw.println(" measuredHeight=$measuredHeight") pw.println(" singleLineInternal=$isSingleLineInternal") - pw.println(" lastTextUpdate=$lastTextUpdate") - pw.println(" lastOnTextChanged=$lastOnTextChanged") - pw.println(" lastInvalidate=$lastInvalidate") - pw.println(" lastMeasureCall=$lastMeasureCall") - pw.println(" lastDraw=$lastDraw") - pw.println(" lastTimeZoneChange=$lastTimeZoneChange") pw.println(" currText=$text") pw.println(" currTimeContextDesc=$contentDescription") - pw.println(" lastAnimationCall=$lastAnimationCall") pw.println(" dozingWeightInternal=$dozingWeightInternal") pw.println(" lockScreenWeightInternal=$lockScreenWeightInternal") pw.println(" dozingColor=$dozingColor") @@ -421,6 +455,124 @@ class AnimatableClockView @JvmOverloads constructor( pw.println(" time=$time") } + fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) { + // Do we need to cancel an in-flight animation? + // Need to also check against 0.0f here; we can sometimes get two calls with fraction == 0, + // which trips up the check otherwise. + if (lastSeenAnimationProgress != 1.0f && + lastSeenAnimationProgress != 0.0f && + fraction == 0.0f) { + // New animation, but need to stop the old one. Figure out where each glyph currently + // is in relation to the box position. After that, use the leading digit's current + // position as the stop target. + currentAnimationNeededStop = true + + // We assume that the current glyph offsets would be relative to the "from" position. + val moveAmount = toRect.left - fromRect.left + + // Remap the current glyph offsets to be relative to the new "end" position, and figure + // out the start/end positions for the stop animation. + for (i in 0 until NUM_DIGITS) { + glyphOffsets[i] = -moveAmount + glyphOffsets[i] + animationCancelStartPosition[i] = glyphOffsets[i] + } + + // Use the leading digit's offset as the stop position. + if (toRect.left > fromRect.left) { + // It _was_ moving left + animationCancelStopPosition = glyphOffsets[0] + } else { + // It was moving right + animationCancelStopPosition = glyphOffsets[1] + } + } + + // Is there a cancellation in progress? + if (currentAnimationNeededStop && fraction < ANIMATION_CANCELLATION_TIME) { + val animationStopProgress = MathUtils.constrainedMap( + 0.0f, 1.0f, 0.0f, ANIMATION_CANCELLATION_TIME, fraction + ) + + // One of the digits has already stopped. + val animationStopStep = 1.0f / (NUM_DIGITS - 1) + + for (i in 0 until NUM_DIGITS) { + val stopAmount = if (toRect.left > fromRect.left) { + // It was moving left (before flipping) + MOVE_LEFT_DELAYS[i] * animationStopStep + } else { + // It was moving right (before flipping) + MOVE_RIGHT_DELAYS[i] * animationStopStep + } + + // Leading digit stops immediately. + if (stopAmount == 0.0f) { + glyphOffsets[i] = animationCancelStopPosition + } else { + val actualStopAmount = MathUtils.constrainedMap( + 0.0f, 1.0f, 0.0f, stopAmount, animationStopProgress + ) + val easedProgress = MOVE_INTERPOLATOR.getInterpolation(actualStopAmount) + val glyphMoveAmount = + animationCancelStopPosition - animationCancelStartPosition[i] + glyphOffsets[i] = + animationCancelStartPosition[i] + glyphMoveAmount * easedProgress + } + } + } else { + // Normal part of the animation. + // Do we need to remap the animation progress to take account of the cancellation? + val actualFraction = if (currentAnimationNeededStop) { + MathUtils.constrainedMap( + 0.0f, 1.0f, ANIMATION_CANCELLATION_TIME, 1.0f, fraction + ) + } else { + fraction + } + + val digitFractions = (0 until NUM_DIGITS).map { + // The delay for each digit, in terms of fraction (i.e. the digit should not move + // during 0.0 - 0.1). + val initialDelay = if (toRect.left > fromRect.left) { + MOVE_RIGHT_DELAYS[it] * MOVE_DIGIT_STEP + } else { + MOVE_LEFT_DELAYS[it] * MOVE_DIGIT_STEP + } + + val f = MathUtils.constrainedMap( + 0.0f, 1.0f, + initialDelay, initialDelay + AVAILABLE_ANIMATION_TIME, + actualFraction + ) + MOVE_INTERPOLATOR.getInterpolation(max(min(f, 1.0f), 0.0f)) + } + + // Was there an animation halt? + val moveAmount = if (currentAnimationNeededStop) { + // Only need to animate over the remaining space if the animation was aborted. + -animationCancelStopPosition + } else { + toRect.left.toFloat() - fromRect.left.toFloat() + } + + for (i in 0 until NUM_DIGITS) { + glyphOffsets[i] = -moveAmount + (moveAmount * digitFractions[i]) + } + } + + invalidate() + + if (fraction == 1.0f) { + // Reset + currentAnimationNeededStop = false + } + + lastSeenAnimationProgress = fraction + + // Ensure that the actual clock container is always in the "end" position. + this.setLeftTopRightBottom(toRect.left, toRect.top, toRect.right, toRect.bottom) + } + // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. // This is an optimization to ensure we only recompute the patterns when the inputs change. private object Patterns { @@ -444,6 +596,7 @@ class AnimatableClockView @JvmOverloads constructor( if (!clockView12Skel.contains("a")) { sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' } } + sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel) sCacheKey = key } @@ -458,5 +611,36 @@ class AnimatableClockView @JvmOverloads constructor( private const val APPEAR_ANIM_DURATION: Long = 350 private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500 private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000 + + // Constants for the animation + private val MOVE_INTERPOLATOR = Interpolators.STANDARD + + // Calculate the positions of all of the digits... + // Offset each digit by, say, 0.1 + // This means that each digit needs to move over a slice of "fractions", i.e. digit 0 should + // move from 0.0 - 0.7, digit 1 from 0.1 - 0.8, digit 2 from 0.2 - 0.9, and digit 3 + // from 0.3 - 1.0. + private const val NUM_DIGITS = 4 + private const val DIGITS_PER_LINE = 2 + + // How much of "fraction" to spend on canceling the animation, if needed + private const val ANIMATION_CANCELLATION_TIME = 0.4f + + // Delays. Each digit's animation should have a slight delay, so we get a nice + // "stepping" effect. When moving right, the second digit of the hour should move first. + // When moving left, the first digit of the hour should move first. The lists encode + // the delay for each digit (hour[0], hour[1], minute[0], minute[1]), to be multiplied + // by delayMultiplier. + private val MOVE_LEFT_DELAYS = listOf(0, 1, 2, 3) + private val MOVE_RIGHT_DELAYS = listOf(1, 0, 3, 2) + + // How much delay to apply to each subsequent digit. This is measured in terms of "fraction" + // (i.e. a value of 0.1 would cause a digit to wait until fraction had hit 0.1, or 0.2 etc + // before moving). + private const val MOVE_DIGIT_STEP = 0.1f + + // Total available transition time for each digit, taking into account the step. If step is + // 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7. + private val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1) } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt index f03fee4b0c2d..48821e8d0bd3 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -18,10 +18,9 @@ import android.database.ContentObserver import android.graphics.drawable.Drawable import android.net.Uri import android.os.Handler -import android.os.UserHandle import android.provider.Settings import android.util.Log -import com.android.systemui.dagger.qualifiers.Main +import com.android.internal.annotations.Keep import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockId import com.android.systemui.plugins.ClockMetadata @@ -30,7 +29,6 @@ import com.android.systemui.plugins.ClockProviderPlugin import com.android.systemui.plugins.PluginListener import com.android.systemui.shared.plugins.PluginManager import com.google.gson.Gson -import javax.inject.Inject private val TAG = ClockRegistry::class.simpleName private const val DEBUG = true @@ -40,22 +38,15 @@ open class ClockRegistry( val context: Context, val pluginManager: PluginManager, val handler: Handler, - defaultClockProvider: ClockProvider + val isEnabled: Boolean, + userHandle: Int, + defaultClockProvider: ClockProvider, ) { - @Inject constructor( - context: Context, - pluginManager: PluginManager, - @Main handler: Handler, - defaultClockProvider: DefaultClockProvider - ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { } - // Usually this would be a typealias, but a SAM provides better java interop fun interface ClockChangeListener { fun onClockChanged() } - var isEnabled: Boolean = false - private val gson = Gson() private val availableClocks = mutableMapOf<ClockId, ClockInfo>() private val clockChangeListeners = mutableListOf<ClockChangeListener>() @@ -105,14 +96,19 @@ open class ClockRegistry( ) } - pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java, - true /* allowMultiple */) - context.contentResolver.registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), - false, - settingObserver, - UserHandle.USER_ALL - ) + if (isEnabled) { + pluginManager.addPluginListener( + pluginListener, + ClockProviderPlugin::class.java, + /*allowMultiple=*/ true + ) + context.contentResolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), + /*notifyForDescendants=*/ false, + settingObserver, + userHandle + ) + } } private fun connectClocks(provider: ClockProvider) { @@ -201,6 +197,7 @@ open class ClockRegistry( val provider: ClockProvider ) + @Keep private data class ClockSetting( val clockId: ClockId, val _applied_timestamp: Long? diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt index b88795157a43..da1d233949cf 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -16,6 +16,7 @@ package com.android.systemui.shared.clocks import android.content.Context import android.content.res.Resources import android.graphics.Color +import android.graphics.Rect import android.icu.text.NumberFormat import android.util.TypedValue import android.view.LayoutInflater @@ -26,6 +27,7 @@ import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockEvents import com.android.systemui.plugins.ClockFaceController import com.android.systemui.plugins.ClockFaceEvents +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.shared.R import java.io.PrintWriter import java.util.Locale @@ -85,9 +87,17 @@ class DefaultClockController( events.onTimeTick() } + override fun setLogBuffer(logBuffer: LogBuffer) { + smallClock.view.tag = "smallClockView" + largeClock.view.tag = "largeClockView" + smallClock.view.logBuffer = logBuffer + largeClock.view.logBuffer = logBuffer + } + open inner class DefaultClockFaceController( override val view: AnimatableClockView, ) : ClockFaceController { + // MAGENTA is a placeholder, and will be assigned correctly in initialize private var currentColor = Color.MAGENTA private var isRegionDark = false @@ -130,6 +140,10 @@ class DefaultClockController( lp.topMargin = (-0.5f * view.bottom).toInt() view.setLayoutParams(lp) } + + fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) { + view.moveForSplitShade(fromRect, toRect, fraction) + } } inner class DefaultClockEvents : ClockEvents { @@ -209,6 +223,13 @@ class DefaultClockController( clocks.forEach { it.animateDoze(dozeState.isActive, !hasJumped) } } } + + override fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) { + largeClock.moveForSplitShade(fromRect, toRect, fraction) + } + + override val hasCustomPositionUpdatedAnimation: Boolean + get() = true } private class AnimationState( diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java index 72f8b7b09dca..40c8774d4f34 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java @@ -1,13 +1,16 @@ package com.android.systemui.shared.recents.utilities; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; -import android.view.Surface; import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.wm.shell.util.SplitBounds; /** * Utility class to position the thumbnail in the TaskView @@ -16,10 +19,26 @@ public class PreviewPositionHelper { public static final float MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT = 0.1f; + /** + * Specifies that a stage is positioned at the top half of the screen if + * in portrait mode or at the left half of the screen if in landscape mode. + * TODO(b/254378592): Remove after consolidation + */ + public static final int STAGE_POSITION_TOP_OR_LEFT = 0; + + /** + * Specifies that a stage is positioned at the bottom half of the screen if + * in portrait mode or at the right half of the screen if in landscape mode. + * TODO(b/254378592): Remove after consolidation + */ + public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1; + // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1. private final RectF mClippedInsets = new RectF(); private final Matrix mMatrix = new Matrix(); private boolean mIsOrientationChanged; + private SplitBounds mSplitBounds; + private int mDesiredStagePosition; public Matrix getMatrix() { return mMatrix; @@ -33,6 +52,11 @@ public class PreviewPositionHelper { return mIsOrientationChanged; } + public void setSplitBounds(SplitBounds splitBounds, int desiredStagePosition) { + mSplitBounds = splitBounds; + mDesiredStagePosition = desiredStagePosition; + } + /** * Updates the matrix based on the provided parameters */ @@ -42,10 +66,19 @@ public class PreviewPositionHelper { boolean isRotated = false; boolean isOrientationDifferent; + float fullscreenTaskWidth = screenWidthPx; + if (mSplitBounds != null && !mSplitBounds.appsStackedVertically) { + // For landscape, scale the width + float taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT + ? mSplitBounds.leftTaskPercent + : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent)); + // Scale landscape width to that of actual screen + fullscreenTaskWidth = screenWidthPx * taskPercent; + } int thumbnailRotation = thumbnailData.rotation; int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation); RectF thumbnailClipHint = new RectF(); - float canvasScreenRatio = canvasWidth / (float) screenWidthPx; + float canvasScreenRatio = canvasWidth / fullscreenTaskWidth; float scaledTaskbarSize = taskbarSize * canvasScreenRatio; thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0; @@ -180,7 +213,7 @@ public class PreviewPositionHelper { * portrait or vice versa, {@code false} otherwise */ private boolean isOrientationChange(int deltaRotation) { - return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270; + return deltaRotation == ROTATION_90 || deltaRotation == ROTATION_270; } private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) { @@ -189,13 +222,13 @@ public class PreviewPositionHelper { mMatrix.setRotate(90 * deltaRotate); switch (deltaRotate) { /* Counter-clockwise */ - case Surface.ROTATION_90: + case ROTATION_90: translateX = thumbnailPosition.height(); break; - case Surface.ROTATION_270: + case ROTATION_270: translateY = thumbnailPosition.width(); break; - case Surface.ROTATION_180: + case ROTATION_180: translateX = thumbnailPosition.width(); translateY = thumbnailPosition.height(); break; 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 dd2e55d4e7d7..cd4b9994ccca 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 @@ -15,6 +15,7 @@ */ package com.android.systemui.shared.regionsampling +import android.graphics.Color import android.graphics.Rect import android.view.View import androidx.annotation.VisibleForTesting @@ -33,18 +34,19 @@ open class RegionSamplingInstance( regionSamplingEnabled: Boolean, updateFun: UpdateColorCallback ) { - private var isDark = RegionDarkness.DEFAULT + private var regionDarkness = RegionDarkness.DEFAULT private var samplingBounds = Rect() private val tmpScreenLocation = IntArray(2) @VisibleForTesting var regionSampler: RegionSamplingHelper? = null - + private var lightForegroundColor = Color.WHITE + private var darkForegroundColor = Color.BLACK /** * Interface for method to be passed into RegionSamplingHelper */ @FunctionalInterface interface UpdateColorCallback { /** - * Method to update the text colors after clock darkness changed. + * Method to update the foreground colors after clock darkness changed. */ fun updateColors() } @@ -59,6 +61,30 @@ open class RegionSamplingInstance( return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor) } + /** + * Sets the colors to be used for Dark and Light Foreground. + * + * @param lightColor The color used for Light Foreground. + * @param darkColor The color used for Dark Foreground. + */ + fun setForegroundColors(lightColor: Int, darkColor: Int) { + lightForegroundColor = lightColor + darkForegroundColor = darkColor + } + + /** + * Determines which foreground color to use based on region darkness. + * + * @return the determined foreground color + */ + fun currentForegroundColor(): Int{ + return if (regionDarkness.isDark) { + lightForegroundColor + } else { + darkForegroundColor + } + } + private fun convertToClockDarkness(isRegionDark: Boolean): RegionDarkness { return if (isRegionDark) { RegionDarkness.DARK @@ -68,7 +94,7 @@ open class RegionSamplingInstance( } fun currentRegionDarkness(): RegionDarkness { - return isDark + return regionDarkness } /** @@ -97,7 +123,7 @@ open class RegionSamplingInstance( regionSampler = createRegionSamplingHelper(sampledView, object : SamplingCallback { override fun onRegionDarknessChanged(isRegionDark: Boolean) { - isDark = convertToClockDarkness(isRegionDark) + regionDarkness = convertToClockDarkness(isRegionDark) updateFun.updateColors() } /** diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index ce337bb70e5b..fd41cb0630dd 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -194,12 +194,8 @@ public class ActivityManagerWrapper { Rect homeContentInsets, Rect minimizedHomeBounds) { final RecentsAnimationControllerCompat controllerCompat = new RecentsAnimationControllerCompat(controller); - final RemoteAnimationTargetCompat[] appsCompat = - RemoteAnimationTargetCompat.wrap(apps); - final RemoteAnimationTargetCompat[] wallpapersCompat = - RemoteAnimationTargetCompat.wrap(wallpapers); - animationHandler.onAnimationStart(controllerCompat, appsCompat, - wallpapersCompat, homeContentInsets, minimizedHomeBounds); + animationHandler.onAnimationStart(controllerCompat, apps, + wallpapers, homeContentInsets, minimizedHomeBounds); } @Override @@ -210,12 +206,7 @@ public class ActivityManagerWrapper { @Override public void onTasksAppeared(RemoteAnimationTarget[] apps) { - final RemoteAnimationTargetCompat[] compats = - new RemoteAnimationTargetCompat[apps.length]; - for (int i = 0; i < apps.length; ++i) { - compats[i] = new RemoteAnimationTargetCompat(apps[i]); - } - animationHandler.onTasksAppeared(compats); + animationHandler.onTasksAppeared(apps); } }; } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java index 5d6598d63a1b..8a2509610310 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java @@ -51,6 +51,8 @@ public final class InteractionJankMonitorWrapper { InteractionJankMonitor.CUJ_SPLIT_SCREEN_ENTER; public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = InteractionJankMonitor.CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION; + public static final int CUJ_RECENTS_SCROLLING = + InteractionJankMonitor.CUJ_RECENTS_SCROLLING; @IntDef({ CUJ_APP_LAUNCH_FROM_RECENTS, @@ -59,7 +61,8 @@ public final class InteractionJankMonitorWrapper { CUJ_APP_CLOSE_TO_PIP, CUJ_QUICK_SWITCH, CUJ_APP_LAUNCH_FROM_WIDGET, - CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION + CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, + CUJ_RECENTS_SCROLLING }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index f2742b7889b1..766266d9cc94 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -110,6 +110,9 @@ public class QuickStepContract { public static final int SYSUI_STATE_IMMERSIVE_MODE = 1 << 24; // The voice interaction session window is showing public static final int SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING = 1 << 25; + // Freeform windows are showing in desktop mode + public static final int SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1 << 26; + @Retention(RetentionPolicy.SOURCE) @IntDef({SYSUI_STATE_SCREEN_PINNING, @@ -137,7 +140,8 @@ public class QuickStepContract { SYSUI_STATE_BACK_DISABLED, SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED, SYSUI_STATE_IMMERSIVE_MODE, - SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING + SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING, + SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE }) public @interface SystemUiStateFlags {} @@ -173,6 +177,8 @@ public class QuickStepContract { ? "bubbles_mange_menu_expanded" : ""); str.add((flags & SYSUI_STATE_IMMERSIVE_MODE) != 0 ? "immersive_mode" : ""); str.add((flags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0 ? "vis_win_showing" : ""); + str.add((flags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0 + ? "freeform_active_in_desktop_mode" : ""); return str.toString(); } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java index 5cca4a6a65f0..8bddf217ccb4 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java @@ -17,6 +17,7 @@ package com.android.systemui.shared.system; import android.graphics.Rect; +import android.view.RemoteAnimationTarget; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -27,7 +28,7 @@ public interface RecentsAnimationListener { * Called when the animation into Recents can start. This call is made on the binder thread. */ void onAnimationStart(RecentsAnimationControllerCompat controller, - RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers, + RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, Rect homeContentInsets, Rect minimizedHomeBounds); /** @@ -39,7 +40,7 @@ public interface RecentsAnimationListener { * Called when the task of an activity that has been started while the recents animation * was running becomes ready for control. */ - void onTasksAppeared(RemoteAnimationTargetCompat[] app); + void onTasksAppeared(RemoteAnimationTarget[] app); /** * Called to request that the current task tile be switched out for a screenshot (if not diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java index 09cf7c57c08a..37e706a9a4c9 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java @@ -83,12 +83,6 @@ public class RemoteAnimationAdapterCompat { RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, final IRemoteAnimationFinishedCallback finishedCallback) { - final RemoteAnimationTargetCompat[] appsCompat = - RemoteAnimationTargetCompat.wrap(apps); - final RemoteAnimationTargetCompat[] wallpapersCompat = - RemoteAnimationTargetCompat.wrap(wallpapers); - final RemoteAnimationTargetCompat[] nonAppsCompat = - RemoteAnimationTargetCompat.wrap(nonApps); final Runnable animationFinishedCallback = new Runnable() { @Override public void run() { @@ -100,8 +94,8 @@ public class RemoteAnimationAdapterCompat { } } }; - remoteAnimationAdapter.onAnimationStart(transit, appsCompat, wallpapersCompat, - nonAppsCompat, animationFinishedCallback); + remoteAnimationAdapter.onAnimationStart(transit, apps, wallpapers, + nonApps, animationFinishedCallback); } @Override @@ -121,12 +115,12 @@ public class RemoteAnimationAdapterCompat { SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) { final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>(); - final RemoteAnimationTargetCompat[] appsCompat = + final RemoteAnimationTarget[] apps = RemoteAnimationTargetCompat.wrapApps(info, t, leashMap); - final RemoteAnimationTargetCompat[] wallpapersCompat = + final RemoteAnimationTarget[] wallpapers = RemoteAnimationTargetCompat.wrapNonApps( info, true /* wallpapers */, t, leashMap); - final RemoteAnimationTargetCompat[] nonAppsCompat = + final RemoteAnimationTarget[] nonApps = RemoteAnimationTargetCompat.wrapNonApps( info, false /* wallpapers */, t, leashMap); @@ -189,9 +183,9 @@ public class RemoteAnimationAdapterCompat { } } // Make wallpaper visible immediately since launcher apparently won't do this. - for (int i = wallpapersCompat.length - 1; i >= 0; --i) { - t.show(wallpapersCompat[i].leash); - t.setAlpha(wallpapersCompat[i].leash, 1.f); + for (int i = wallpapers.length - 1; i >= 0; --i) { + t.show(wallpapers[i].leash); + t.setAlpha(wallpapers[i].leash, 1.f); } } else { if (launcherTask != null) { @@ -237,7 +231,7 @@ public class RemoteAnimationAdapterCompat { } // TODO(bc-unlcok): Pass correct transit type. remoteAnimationAdapter.onAnimationStart(TRANSIT_OLD_NONE, - appsCompat, wallpapersCompat, nonAppsCompat, () -> { + apps, wallpapers, nonApps, () -> { synchronized (mFinishRunnables) { if (mFinishRunnables.remove(token) == null) return; } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java index 007629254c7c..5809c8124946 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java @@ -16,11 +16,12 @@ package com.android.systemui.shared.system; +import android.view.RemoteAnimationTarget; import android.view.WindowManager; public interface RemoteAnimationRunnerCompat { void onAnimationStart(@WindowManager.TransitionOldType int transit, - RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers, - RemoteAnimationTargetCompat[] nonApps, Runnable finishedCallback); + RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, Runnable finishedCallback); void onAnimationCancelled(); }
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java index 7c3b5fc52f0a..8d1768c41589 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java @@ -11,12 +11,15 @@ * 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 + * limitations under the License. */ package com.android.systemui.shared.system; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.view.RemoteAnimationTarget.MODE_CHANGING; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_CLOSE; @@ -29,88 +32,28 @@ import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPI import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; +import android.app.TaskInfo; import android.app.WindowConfiguration; -import android.graphics.Point; import android.graphics.Rect; import android.util.ArrayMap; -import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; +import android.window.TransitionInfo.Change; import java.util.ArrayList; +import java.util.function.BiPredicate; /** - * @see RemoteAnimationTarget + * Some utility methods for creating {@link RemoteAnimationTarget} instances. */ public class RemoteAnimationTargetCompat { - public static final int MODE_OPENING = RemoteAnimationTarget.MODE_OPENING; - public static final int MODE_CLOSING = RemoteAnimationTarget.MODE_CLOSING; - public static final int MODE_CHANGING = RemoteAnimationTarget.MODE_CHANGING; - public final int mode; - - public static final int ACTIVITY_TYPE_UNDEFINED = WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; - public static final int ACTIVITY_TYPE_STANDARD = WindowConfiguration.ACTIVITY_TYPE_STANDARD; - public static final int ACTIVITY_TYPE_HOME = WindowConfiguration.ACTIVITY_TYPE_HOME; - public static final int ACTIVITY_TYPE_RECENTS = WindowConfiguration.ACTIVITY_TYPE_RECENTS; - public static final int ACTIVITY_TYPE_ASSISTANT = WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; - public final int activityType; - - public int taskId; - public final SurfaceControl leash; - public final boolean isTranslucent; - public final Rect clipRect; - public final int prefixOrderIndex; - public final Point position; - public final Rect localBounds; - public final Rect sourceContainerBounds; - public final Rect screenSpaceBounds; - public final Rect startScreenSpaceBounds; - public final boolean isNotInRecents; - public final Rect contentInsets; - public ActivityManager.RunningTaskInfo taskInfo; - public final boolean allowEnterPip; - public final int rotationChange; - public final int windowType; - public final WindowConfiguration windowConfiguration; - - private final SurfaceControl mStartLeash; - - // Fields used only to unwrap into RemoteAnimationTarget - private final Rect startBounds; - - public final boolean willShowImeOnTarget; - - public RemoteAnimationTargetCompat(RemoteAnimationTarget app) { - taskId = app.taskId; - mode = app.mode; - leash = app.leash; - isTranslucent = app.isTranslucent; - clipRect = app.clipRect; - position = app.position; - localBounds = app.localBounds; - sourceContainerBounds = app.sourceContainerBounds; - screenSpaceBounds = app.screenSpaceBounds; - startScreenSpaceBounds = screenSpaceBounds; - prefixOrderIndex = app.prefixOrderIndex; - isNotInRecents = app.isNotInRecents; - contentInsets = app.contentInsets; - activityType = app.windowConfiguration.getActivityType(); - taskInfo = app.taskInfo; - allowEnterPip = app.allowEnterPip; - rotationChange = 0; - - mStartLeash = app.startLeash; - windowType = app.windowType; - windowConfiguration = app.windowConfiguration; - startBounds = app.startBounds; - willShowImeOnTarget = app.willShowImeOnTarget; - } - private static int newModeToLegacyMode(int newMode) { switch (newMode) { case WindowManager.TRANSIT_OPEN: @@ -120,20 +63,10 @@ public class RemoteAnimationTargetCompat { case WindowManager.TRANSIT_TO_BACK: return MODE_CLOSING; default: - return 2; // MODE_CHANGING + return MODE_CHANGING; } } - public RemoteAnimationTarget unwrap() { - final RemoteAnimationTarget target = new RemoteAnimationTarget( - taskId, mode, leash, isTranslucent, clipRect, contentInsets, - prefixOrderIndex, position, localBounds, screenSpaceBounds, windowConfiguration, - isNotInRecents, mStartLeash, startBounds, taskInfo, allowEnterPip, windowType - ); - target.setWillShowImeOnTarget(willShowImeOnTarget); - return target; - } - /** * Almost a copy of Transitions#setupStartState. * TODO: remove when there is proper cross-process transaction sync. @@ -205,54 +138,61 @@ public class RemoteAnimationTargetCompat { return leashSurface; } - public RemoteAnimationTargetCompat(TransitionInfo.Change change, int order, - TransitionInfo info, SurfaceControl.Transaction t) { - mode = newModeToLegacyMode(change.getMode()); + /** + * Creates a new RemoteAnimationTarget from the provided change info + */ + public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order, + TransitionInfo info, SurfaceControl.Transaction t, + @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) { + int taskId; + boolean isNotInRecents; + ActivityManager.RunningTaskInfo taskInfo; + WindowConfiguration windowConfiguration; + taskInfo = change.getTaskInfo(); if (taskInfo != null) { taskId = taskInfo.taskId; isNotInRecents = !taskInfo.isRunning; - activityType = taskInfo.getActivityType(); windowConfiguration = taskInfo.configuration.windowConfiguration; } else { taskId = INVALID_TASK_ID; isNotInRecents = true; - activityType = ACTIVITY_TYPE_UNDEFINED; windowConfiguration = new WindowConfiguration(); } - // TODO: once we can properly sync transactions across process, then get rid of this leash. - leash = createLeash(info, change, order, t); - - isTranslucent = (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0; - clipRect = null; - position = null; - localBounds = new Rect(change.getEndAbsBounds()); + Rect localBounds = new Rect(change.getEndAbsBounds()); localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y); - sourceContainerBounds = null; - screenSpaceBounds = new Rect(change.getEndAbsBounds()); - startScreenSpaceBounds = new Rect(change.getStartAbsBounds()); - - prefixOrderIndex = order; - // TODO(shell-transitions): I guess we need to send content insets? evaluate how its used. - contentInsets = new Rect(0, 0, 0, 0); - allowEnterPip = change.getAllowEnterPip(); - mStartLeash = null; - rotationChange = change.getEndRotation() - change.getStartRotation(); - windowType = (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0 - ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE; - - startBounds = change.getStartAbsBounds(); - willShowImeOnTarget = (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0; - } - public static RemoteAnimationTargetCompat[] wrap(RemoteAnimationTarget[] apps) { - final int length = apps != null ? apps.length : 0; - final RemoteAnimationTargetCompat[] appsCompat = new RemoteAnimationTargetCompat[length]; - for (int i = 0; i < length; i++) { - appsCompat[i] = new RemoteAnimationTargetCompat(apps[i]); + RemoteAnimationTarget target = new RemoteAnimationTarget( + taskId, + newModeToLegacyMode(change.getMode()), + // TODO: once we can properly sync transactions across process, + // then get rid of this leash. + createLeash(info, change, order, t), + (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0, + null, + // TODO(shell-transitions): we need to send content insets? evaluate how its used. + new Rect(0, 0, 0, 0), + order, + null, + localBounds, + new Rect(change.getEndAbsBounds()), + windowConfiguration, + isNotInRecents, + null, + new Rect(change.getStartAbsBounds()), + taskInfo, + change.getAllowEnterPip(), + (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0 + ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE + ); + target.setWillShowImeOnTarget( + (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0); + target.setRotationChange(change.getEndRotation() - change.getStartRotation()); + if (leashMap != null) { + leashMap.put(change.getLeash(), target.leash); } - return appsCompat; + return target; } /** @@ -261,35 +201,20 @@ public class RemoteAnimationTargetCompat { * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be * populated by this function. If null, it is ignored. */ - public static RemoteAnimationTargetCompat[] wrapApps(TransitionInfo info, + public static RemoteAnimationTarget[] wrapApps(TransitionInfo info, SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) { - final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>(); - final SparseArray<TransitionInfo.Change> childTaskTargets = new SparseArray<>(); - for (int i = 0; i < info.getChanges().size(); i++) { - final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getTaskInfo() == null) continue; - - final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + SparseBooleanArray childTaskTargets = new SparseBooleanArray(); + return wrap(info, t, leashMap, (change, taskInfo) -> { // Children always come before parent since changes are in top-to-bottom z-order. - if (taskInfo != null) { - if (childTaskTargets.contains(taskInfo.taskId)) { - // has children, so not a leaf. Skip. - continue; - } - if (taskInfo.hasParentTask()) { - childTaskTargets.put(taskInfo.parentTaskId, change); - } + if ((taskInfo == null) || childTaskTargets.get(taskInfo.taskId)) { + // has children, so not a leaf. Skip. + return false; } - - final RemoteAnimationTargetCompat targetCompat = - new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t); - if (leashMap != null) { - leashMap.put(change.getLeash(), targetCompat.leash); + if (taskInfo.hasParentTask()) { + childTaskTargets.put(taskInfo.parentTaskId, true); } - out.add(targetCompat); - } - - return out.toArray(new RemoteAnimationTargetCompat[out.size()]); + return true; + }); } /** @@ -300,38 +225,22 @@ public class RemoteAnimationTargetCompat { * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be * populated by this function. If null, it is ignored. */ - public static RemoteAnimationTargetCompat[] wrapNonApps(TransitionInfo info, boolean wallpapers, + public static RemoteAnimationTarget[] wrapNonApps(TransitionInfo info, boolean wallpapers, SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) { - final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>(); + return wrap(info, t, leashMap, (change, taskInfo) -> (taskInfo == null) + && wallpapers == ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0)); + } + private static RemoteAnimationTarget[] wrap(TransitionInfo info, + SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap, + BiPredicate<Change, TaskInfo> filter) { + final ArrayList<RemoteAnimationTarget> out = new ArrayList<>(); for (int i = 0; i < info.getChanges().size(); i++) { - final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getTaskInfo() != null) continue; - - final boolean changeIsWallpaper = - (change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0; - if (wallpapers != changeIsWallpaper) continue; - - final RemoteAnimationTargetCompat targetCompat = - new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t); - if (leashMap != null) { - leashMap.put(change.getLeash(), targetCompat.leash); + TransitionInfo.Change change = info.getChanges().get(i); + if (filter.test(change, change.getTaskInfo())) { + out.add(newTarget(change, info.getChanges().size() - i, info, t, leashMap)); } - out.add(targetCompat); - } - - return out.toArray(new RemoteAnimationTargetCompat[out.size()]); - } - - /** - * @see SurfaceControl#release() - */ - public void release() { - if (leash != null) { - leash.release(); - } - if (mStartLeash != null) { - mStartLeash.release(); } + return out.toArray(new RemoteAnimationTarget[out.size()]); } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java index f6792251d282..d6655a74219c 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java @@ -17,7 +17,9 @@ package com.android.systemui.shared.system; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; @@ -27,8 +29,7 @@ import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionFilter.CONTAINER_ORDER_TOP; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_RECENTS; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.newTarget; import android.annotation.NonNull; import android.annotation.Nullable; @@ -45,6 +46,7 @@ import android.util.ArrayMap; import android.util.Log; import android.util.SparseArray; import android.view.IRecentsAnimationController; +import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; @@ -127,9 +129,9 @@ public class RemoteTransitionCompat implements Parcelable { SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishedCallback) { final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>(); - final RemoteAnimationTargetCompat[] apps = + final RemoteAnimationTarget[] apps = RemoteAnimationTargetCompat.wrapApps(info, t, leashMap); - final RemoteAnimationTargetCompat[] wallpapers = + final RemoteAnimationTarget[] wallpapers = RemoteAnimationTargetCompat.wrapNonApps( info, true /* wallpapers */, t, leashMap); // TODO(b/177438007): Move this set-up logic into launcher's animation impl. @@ -230,7 +232,7 @@ public class RemoteTransitionCompat implements Parcelable { private PictureInPictureSurfaceTransaction mPipTransaction = null; private IBinder mTransition = null; private boolean mKeyguardLocked = false; - private RemoteAnimationTargetCompat[] mAppearedTargets; + private RemoteAnimationTarget[] mAppearedTargets; private boolean mWillFinishToHome = false; void setup(RecentsAnimationControllerCompat wrapped, TransitionInfo info, @@ -325,18 +327,15 @@ public class RemoteTransitionCompat implements Parcelable { final int layer = mInfo.getChanges().size() * 3; mOpeningLeashes = new ArrayList<>(); mOpeningHome = cancelRecents; - final RemoteAnimationTargetCompat[] targets = - new RemoteAnimationTargetCompat[openingTasks.size()]; + final RemoteAnimationTarget[] targets = + new RemoteAnimationTarget[openingTasks.size()]; for (int i = 0; i < openingTasks.size(); ++i) { final TransitionInfo.Change change = openingTasks.valueAt(i); mOpeningLeashes.add(change.getLeash()); // We are receiving new opening tasks, so convert to onTasksAppeared. - final RemoteAnimationTargetCompat target = new RemoteAnimationTargetCompat( - change, layer, info, t); - mLeashMap.put(mOpeningLeashes.get(i), target.leash); - t.reparent(target.leash, mInfo.getRootLeash()); - t.setLayer(target.leash, layer); - targets[i] = target; + targets[i] = newTarget(change, layer, info, t, mLeashMap); + t.reparent(targets[i].leash, mInfo.getRootLeash()); + t.setLayer(targets[i].leash, layer); } t.apply(); mAppearedTargets = targets; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java deleted file mode 100644 index 30c062b66da9..000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.shared.system; - -import android.graphics.HardwareRenderer; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.os.Handler; -import android.os.Handler.Callback; -import android.os.Message; -import android.os.Trace; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Transaction; -import android.view.View; -import android.view.ViewRootImpl; - -import java.util.function.Consumer; - -/** - * Helper class to apply surface transactions in sync with RenderThread. - * - * NOTE: This is a modification of {@link android.view.SyncRtSurfaceTransactionApplier}, we can't - * currently reference that class from the shared lib as it is hidden. - */ -public class SyncRtSurfaceTransactionApplierCompat { - - public static final int FLAG_ALL = 0xffffffff; - public static final int FLAG_ALPHA = 1; - public static final int FLAG_MATRIX = 1 << 1; - public static final int FLAG_WINDOW_CROP = 1 << 2; - public static final int FLAG_LAYER = 1 << 3; - public static final int FLAG_CORNER_RADIUS = 1 << 4; - public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5; - public static final int FLAG_VISIBILITY = 1 << 6; - public static final int FLAG_RELATIVE_LAYER = 1 << 7; - public static final int FLAG_SHADOW_RADIUS = 1 << 8; - - private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0; - - private final SurfaceControl mBarrierSurfaceControl; - private final ViewRootImpl mTargetViewRootImpl; - private final Handler mApplyHandler; - - private int mSequenceNumber = 0; - private int mPendingSequenceNumber = 0; - private Runnable mAfterApplyCallback; - - /** - * @param targetView The view in the surface that acts as synchronization anchor. - */ - public SyncRtSurfaceTransactionApplierCompat(View targetView) { - mTargetViewRootImpl = targetView != null ? targetView.getViewRootImpl() : null; - mBarrierSurfaceControl = mTargetViewRootImpl != null - ? mTargetViewRootImpl.getSurfaceControl() : null; - - mApplyHandler = new Handler(new Callback() { - @Override - public boolean handleMessage(Message msg) { - if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) { - onApplyMessage(msg.arg1); - return true; - } - return false; - } - }); - } - - private void onApplyMessage(int seqNo) { - mSequenceNumber = seqNo; - if (mSequenceNumber == mPendingSequenceNumber && mAfterApplyCallback != null) { - Runnable r = mAfterApplyCallback; - mAfterApplyCallback = null; - r.run(); - } - } - - /** - * Schedules applying surface parameters on the next frame. - * - * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into - * this method to avoid synchronization issues. - */ - public void scheduleApply(final SyncRtSurfaceTransactionApplierCompat.SurfaceParams... params) { - if (mTargetViewRootImpl == null || mTargetViewRootImpl.getView() == null) { - return; - } - - mPendingSequenceNumber++; - final int toApplySeqNo = mPendingSequenceNumber; - mTargetViewRootImpl.registerRtFrameCallback(new HardwareRenderer.FrameDrawingCallback() { - @Override - public void onFrameDraw(long frame) { - if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) { - Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0) - .sendToTarget(); - return; - } - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Sync transaction frameNumber=" + frame); - Transaction t = new Transaction(); - for (int i = params.length - 1; i >= 0; i--) { - SyncRtSurfaceTransactionApplierCompat.SurfaceParams surfaceParams = - params[i]; - surfaceParams.applyTo(t); - } - if (mTargetViewRootImpl != null) { - mTargetViewRootImpl.mergeWithNextTransaction(t, frame); - } else { - t.apply(); - } - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0) - .sendToTarget(); - } - }); - - // Make sure a frame gets scheduled. - mTargetViewRootImpl.getView().invalidate(); - } - - /** - * Calls the runnable when any pending apply calls have completed - */ - public void addAfterApplyCallback(final Runnable afterApplyCallback) { - if (mSequenceNumber == mPendingSequenceNumber) { - afterApplyCallback.run(); - } else { - if (mAfterApplyCallback == null) { - mAfterApplyCallback = afterApplyCallback; - } else { - final Runnable oldCallback = mAfterApplyCallback; - mAfterApplyCallback = new Runnable() { - @Override - public void run() { - afterApplyCallback.run(); - oldCallback.run(); - } - }; - } - } - } - - public static void applyParams(TransactionCompat t, - SyncRtSurfaceTransactionApplierCompat.SurfaceParams params) { - params.applyTo(t.mTransaction); - } - - /** - * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is - * attached if necessary. - */ - public static void create(final View targetView, - final Consumer<SyncRtSurfaceTransactionApplierCompat> callback) { - if (targetView == null) { - // No target view, no applier - callback.accept(null); - } else if (targetView.getViewRootImpl() != null) { - // Already attached, we're good to go - callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView)); - } else { - // Haven't been attached before we can get the view root - targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - targetView.removeOnAttachStateChangeListener(this); - callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView)); - } - - @Override - public void onViewDetachedFromWindow(View v) { - // Do nothing - } - }); - } - } - - public static class SurfaceParams { - public static class Builder { - final SurfaceControl surface; - int flags; - float alpha; - float cornerRadius; - int backgroundBlurRadius; - Matrix matrix; - Rect windowCrop; - int layer; - SurfaceControl relativeTo; - int relativeLayer; - boolean visible; - float shadowRadius; - - /** - * @param surface The surface to modify. - */ - public Builder(SurfaceControl surface) { - this.surface = surface; - } - - /** - * @param alpha The alpha value to apply to the surface. - * @return this Builder - */ - public Builder withAlpha(float alpha) { - this.alpha = alpha; - flags |= FLAG_ALPHA; - return this; - } - - /** - * @param matrix The matrix to apply to the surface. - * @return this Builder - */ - public Builder withMatrix(Matrix matrix) { - this.matrix = new Matrix(matrix); - flags |= FLAG_MATRIX; - return this; - } - - /** - * @param windowCrop The window crop to apply to the surface. - * @return this Builder - */ - public Builder withWindowCrop(Rect windowCrop) { - this.windowCrop = new Rect(windowCrop); - flags |= FLAG_WINDOW_CROP; - return this; - } - - /** - * @param layer The layer to assign the surface. - * @return this Builder - */ - public Builder withLayer(int layer) { - this.layer = layer; - flags |= FLAG_LAYER; - return this; - } - - /** - * @param relativeTo The surface that's set relative layer to. - * @param relativeLayer The relative layer. - * @return this Builder - */ - public Builder withRelativeLayerTo(SurfaceControl relativeTo, int relativeLayer) { - this.relativeTo = relativeTo; - this.relativeLayer = relativeLayer; - flags |= FLAG_RELATIVE_LAYER; - return this; - } - - /** - * @param radius the Radius for rounded corners to apply to the surface. - * @return this Builder - */ - public Builder withCornerRadius(float radius) { - this.cornerRadius = radius; - flags |= FLAG_CORNER_RADIUS; - return this; - } - - /** - * @param radius the Radius for the shadows to apply to the surface. - * @return this Builder - */ - public Builder withShadowRadius(float radius) { - this.shadowRadius = radius; - flags |= FLAG_SHADOW_RADIUS; - return this; - } - - /** - * @param radius the Radius for blur to apply to the background surfaces. - * @return this Builder - */ - public Builder withBackgroundBlur(int radius) { - this.backgroundBlurRadius = radius; - flags |= FLAG_BACKGROUND_BLUR_RADIUS; - return this; - } - - /** - * @param visible The visibility to apply to the surface. - * @return this Builder - */ - public Builder withVisibility(boolean visible) { - this.visible = visible; - flags |= FLAG_VISIBILITY; - return this; - } - - /** - * @return a new SurfaceParams instance - */ - public SurfaceParams build() { - return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer, - relativeTo, relativeLayer, cornerRadius, backgroundBlurRadius, visible, - shadowRadius); - } - } - - private SurfaceParams(SurfaceControl surface, int flags, float alpha, Matrix matrix, - Rect windowCrop, int layer, SurfaceControl relativeTo, int relativeLayer, - float cornerRadius, int backgroundBlurRadius, boolean visible, float shadowRadius) { - this.flags = flags; - this.surface = surface; - this.alpha = alpha; - this.matrix = matrix; - this.windowCrop = windowCrop; - this.layer = layer; - this.relativeTo = relativeTo; - this.relativeLayer = relativeLayer; - this.cornerRadius = cornerRadius; - this.backgroundBlurRadius = backgroundBlurRadius; - this.visible = visible; - this.shadowRadius = shadowRadius; - } - - private final int flags; - private final float[] mTmpValues = new float[9]; - - public final SurfaceControl surface; - public final float alpha; - public final float cornerRadius; - public final int backgroundBlurRadius; - public final Matrix matrix; - public final Rect windowCrop; - public final int layer; - public final SurfaceControl relativeTo; - public final int relativeLayer; - public final boolean visible; - public final float shadowRadius; - - public void applyTo(SurfaceControl.Transaction t) { - if ((flags & FLAG_MATRIX) != 0) { - t.setMatrix(surface, matrix, mTmpValues); - } - if ((flags & FLAG_WINDOW_CROP) != 0) { - t.setWindowCrop(surface, windowCrop); - } - if ((flags & FLAG_ALPHA) != 0) { - t.setAlpha(surface, alpha); - } - if ((flags & FLAG_LAYER) != 0) { - t.setLayer(surface, layer); - } - if ((flags & FLAG_CORNER_RADIUS) != 0) { - t.setCornerRadius(surface, cornerRadius); - } - if ((flags & FLAG_BACKGROUND_BLUR_RADIUS) != 0) { - t.setBackgroundBlurRadius(surface, backgroundBlurRadius); - } - if ((flags & FLAG_VISIBILITY) != 0) { - if (visible) { - t.show(surface); - } else { - t.hide(surface); - } - } - if ((flags & FLAG_RELATIVE_LAYER) != 0) { - t.setRelativeLayer(surface, relativeTo, relativeLayer); - } - if ((flags & FLAG_SHADOW_RADIUS) != 0) { - t.setShadowRadius(surface, shadowRadius); - } - } - } -} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java deleted file mode 100644 index 43a882a5f6be..000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.shared.system; - -import android.graphics.Matrix; -import android.graphics.Rect; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Transaction; - -public class TransactionCompat { - - final Transaction mTransaction; - - final float[] mTmpValues = new float[9]; - - public TransactionCompat() { - mTransaction = new Transaction(); - } - - public void apply() { - mTransaction.apply(); - } - - public TransactionCompat show(SurfaceControl surfaceControl) { - mTransaction.show(surfaceControl); - return this; - } - - public TransactionCompat hide(SurfaceControl surfaceControl) { - mTransaction.hide(surfaceControl); - return this; - } - - public TransactionCompat setPosition(SurfaceControl surfaceControl, float x, float y) { - mTransaction.setPosition(surfaceControl, x, y); - return this; - } - - public TransactionCompat setSize(SurfaceControl surfaceControl, int w, int h) { - mTransaction.setBufferSize(surfaceControl, w, h); - return this; - } - - public TransactionCompat setLayer(SurfaceControl surfaceControl, int z) { - mTransaction.setLayer(surfaceControl, z); - return this; - } - - public TransactionCompat setAlpha(SurfaceControl surfaceControl, float alpha) { - mTransaction.setAlpha(surfaceControl, alpha); - return this; - } - - public TransactionCompat setOpaque(SurfaceControl surfaceControl, boolean opaque) { - mTransaction.setOpaque(surfaceControl, opaque); - return this; - } - - public TransactionCompat setMatrix(SurfaceControl surfaceControl, float dsdx, float dtdx, - float dtdy, float dsdy) { - mTransaction.setMatrix(surfaceControl, dsdx, dtdx, dtdy, dsdy); - return this; - } - - public TransactionCompat setMatrix(SurfaceControl surfaceControl, Matrix matrix) { - mTransaction.setMatrix(surfaceControl, matrix, mTmpValues); - return this; - } - - public TransactionCompat setWindowCrop(SurfaceControl surfaceControl, Rect crop) { - mTransaction.setWindowCrop(surfaceControl, crop); - return this; - } - - public TransactionCompat setCornerRadius(SurfaceControl surfaceControl, float radius) { - mTransaction.setCornerRadius(surfaceControl, radius); - return this; - } - - public TransactionCompat setBackgroundBlurRadius(SurfaceControl surfaceControl, int radius) { - mTransaction.setBackgroundBlurRadius(surfaceControl, radius); - return this; - } - - public TransactionCompat setColor(SurfaceControl surfaceControl, float[] color) { - mTransaction.setColor(surfaceControl, color); - return this; - } - - public static void setRelativeLayer(Transaction t, SurfaceControl surfaceControl, - SurfaceControl relativeTo, int z) { - t.setRelativeLayer(surfaceControl, relativeTo, z); - } -} diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java deleted file mode 100644 index 6064be94bc0a..000000000000 --- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.keyguard; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.Rect; -import android.icu.text.NumberFormat; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; - -import com.android.settingslib.Utils; -import com.android.systemui.R; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shared.clocks.AnimatableClockView; -import com.android.systemui.shared.navigationbar.RegionSamplingHelper; -import com.android.systemui.statusbar.policy.BatteryController; -import com.android.systemui.util.ViewController; - -import java.io.PrintWriter; -import java.util.Locale; -import java.util.Objects; -import java.util.Optional; -import java.util.TimeZone; -import java.util.concurrent.Executor; - -import javax.inject.Inject; - -/** - * Controller for an AnimatableClockView on the keyguard. Instantiated by - * {@link KeyguardClockSwitchController}. - */ -public class AnimatableClockController extends ViewController<AnimatableClockView> { - private static final String TAG = "AnimatableClockCtrl"; - private static final int FORMAT_NUMBER = 1234567890; - - private final StatusBarStateController mStatusBarStateController; - private final BroadcastDispatcher mBroadcastDispatcher; - private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private final BatteryController mBatteryController; - private final int mDozingColor = Color.WHITE; - private Optional<RegionSamplingHelper> mRegionSamplingHelper = Optional.empty(); - private Rect mSamplingBounds = new Rect(); - private int mLockScreenColor; - private final boolean mRegionSamplingEnabled; - - private boolean mIsDozing; - private boolean mIsCharging; - private float mDozeAmount; - boolean mKeyguardShowing; - private Locale mLocale; - - private final NumberFormat mBurmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my")); - private final String mBurmeseNumerals; - private final float mBurmeseLineSpacing; - private final float mDefaultLineSpacing; - - @Inject - public AnimatableClockController( - AnimatableClockView view, - StatusBarStateController statusBarStateController, - BroadcastDispatcher broadcastDispatcher, - BatteryController batteryController, - KeyguardUpdateMonitor keyguardUpdateMonitor, - @Main Resources resources, - @Main Executor mainExecutor, - @Background Executor bgExecutor, - FeatureFlags featureFlags - ) { - super(view); - mStatusBarStateController = statusBarStateController; - mBroadcastDispatcher = broadcastDispatcher; - mKeyguardUpdateMonitor = keyguardUpdateMonitor; - mBatteryController = batteryController; - - mBurmeseNumerals = mBurmeseNf.format(FORMAT_NUMBER); - mBurmeseLineSpacing = resources.getFloat( - R.dimen.keyguard_clock_line_spacing_scale_burmese); - mDefaultLineSpacing = resources.getFloat( - R.dimen.keyguard_clock_line_spacing_scale); - - mRegionSamplingEnabled = featureFlags.isEnabled(Flags.REGION_SAMPLING); - if (!mRegionSamplingEnabled) { - return; - } - - mRegionSamplingHelper = Optional.of(new RegionSamplingHelper(mView, - new RegionSamplingHelper.SamplingCallback() { - @Override - public void onRegionDarknessChanged(boolean isRegionDark) { - if (isRegionDark) { - mLockScreenColor = Color.WHITE; - } else { - mLockScreenColor = Color.BLACK; - } - initColors(); - } - - @Override - public Rect getSampledRegion(View sampledView) { - mSamplingBounds = new Rect(sampledView.getLeft(), sampledView.getTop(), - sampledView.getRight(), sampledView.getBottom()); - return mSamplingBounds; - } - - @Override - public boolean isSamplingEnabled() { - return mRegionSamplingEnabled; - } - }, mainExecutor, bgExecutor) - ); - mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> { - regionSamplingHelper.setWindowVisible(true); - }); - } - - private void reset() { - mView.animateDoze(mIsDozing, false); - } - - private final BatteryController.BatteryStateChangeCallback mBatteryCallback = - new BatteryController.BatteryStateChangeCallback() { - @Override - public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { - if (mKeyguardShowing && !mIsCharging && charging) { - mView.animateCharge(mStatusBarStateController::isDozing); - } - mIsCharging = charging; - } - }; - - private final BroadcastReceiver mLocaleBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - updateLocale(); - } - }; - - private final StatusBarStateController.StateListener mStatusBarStateListener = - new StatusBarStateController.StateListener() { - @Override - public void onDozeAmountChanged(float linear, float eased) { - boolean noAnimation = (mDozeAmount == 0f && linear == 1f) - || (mDozeAmount == 1f && linear == 0f); - boolean isDozing = linear > mDozeAmount; - mDozeAmount = linear; - if (mIsDozing != isDozing) { - mIsDozing = isDozing; - mView.animateDoze(mIsDozing, !noAnimation); - } - } - }; - - private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = - new KeyguardUpdateMonitorCallback() { - @Override - public void onKeyguardVisibilityChanged(boolean showing) { - mKeyguardShowing = showing; - if (!mKeyguardShowing) { - // reset state (ie: after weight animations) - reset(); - } - } - - @Override - public void onTimeFormatChanged(String timeFormat) { - mView.refreshFormat(); - } - - @Override - public void onTimeZoneChanged(TimeZone timeZone) { - mView.onTimeZoneChanged(timeZone); - } - - @Override - public void onUserSwitchComplete(int userId) { - mView.refreshFormat(); - } - }; - - @Override - protected void onInit() { - mIsDozing = mStatusBarStateController.isDozing(); - } - - @Override - protected void onViewAttached() { - updateLocale(); - mBroadcastDispatcher.registerReceiver(mLocaleBroadcastReceiver, - new IntentFilter(Intent.ACTION_LOCALE_CHANGED)); - mDozeAmount = mStatusBarStateController.getDozeAmount(); - mIsDozing = mStatusBarStateController.isDozing() || mDozeAmount != 0; - mBatteryController.addCallback(mBatteryCallback); - mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); - - mStatusBarStateController.addCallback(mStatusBarStateListener); - - mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> { - regionSamplingHelper.start(mSamplingBounds); - }); - - mView.onTimeZoneChanged(TimeZone.getDefault()); - initColors(); - mView.animateDoze(mIsDozing, false); - } - - @Override - protected void onViewDetached() { - mBroadcastDispatcher.unregisterReceiver(mLocaleBroadcastReceiver); - mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback); - mBatteryController.removeCallback(mBatteryCallback); - mStatusBarStateController.removeCallback(mStatusBarStateListener); - mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> { - regionSamplingHelper.stop(); - }); - } - - /** Animate the clock appearance */ - public void animateAppear() { - if (!mIsDozing) mView.animateAppearOnLockscreen(); - } - - /** Animate the clock appearance when a foldable device goes from fully-open/half-open state to - * fully folded state and it goes to sleep (always on display screen) */ - public void animateFoldAppear() { - mView.animateFoldAppear(true); - } - - /** - * Updates the time for the view. - */ - public void refreshTime() { - mView.refreshTime(); - } - - /** - * Return locallly stored dozing state. - */ - @VisibleForTesting - public boolean isDozing() { - return mIsDozing; - } - - private void updateLocale() { - Locale currLocale = Locale.getDefault(); - if (!Objects.equals(currLocale, mLocale)) { - mLocale = currLocale; - NumberFormat nf = NumberFormat.getInstance(mLocale); - if (nf.format(FORMAT_NUMBER).equals(mBurmeseNumerals)) { - mView.setLineSpacingScale(mBurmeseLineSpacing); - } else { - mView.setLineSpacingScale(mDefaultLineSpacing); - } - mView.refreshFormat(); - } - } - - private void initColors() { - if (!mRegionSamplingEnabled) { - mLockScreenColor = Utils.getColorAttrDefaultColor(getContext(), - com.android.systemui.R.attr.wallpaperTextColorAccent); - } - mView.setColors(mDozingColor, mLockScreenColor); - mView.animateDoze(mIsDozing, false); - } - - /** - * Dump information for debugging - */ - public void dump(@NonNull PrintWriter pw) { - pw.println(this); - mView.dump(pw); - mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> { - regionSamplingHelper.dump(pw); - }); - } -} diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt index 0075ddd73cd3..450784ea8f03 100644 --- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt +++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt @@ -16,19 +16,29 @@ package com.android.keyguard +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator import android.content.Context import android.content.res.ColorStateList import android.content.res.TypedArray import android.graphics.Color import android.util.AttributeSet +import android.view.View import com.android.settingslib.Utils +import com.android.systemui.animation.Interpolators /** Displays security messages for the keyguard bouncer. */ -class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : +open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : KeyguardMessageArea(context, attrs) { private val DEFAULT_COLOR = -1 private var mDefaultColorState: ColorStateList? = null private var mNextMessageColorState: ColorStateList? = ColorStateList.valueOf(DEFAULT_COLOR) + private val animatorSet = AnimatorSet() + private var textAboutToShow: CharSequence? = null + protected open val SHOW_DURATION_MILLIS = 150L + protected open val HIDE_DURATION_MILLIS = 200L override fun updateTextColor() { var colorState = mDefaultColorState @@ -58,4 +68,46 @@ class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : mDefaultColorState = Utils.getColorAttr(context, android.R.attr.textColorPrimary) super.reloadColor() } + + override fun setMessage(msg: CharSequence?) { + if ((msg == textAboutToShow && msg != null) || msg == text) { + return + } + textAboutToShow = msg + + if (animatorSet.isRunning) { + animatorSet.cancel() + textAboutToShow = null + } + + val hideAnimator = + ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f).apply { + duration = HIDE_DURATION_MILLIS + interpolator = Interpolators.STANDARD_ACCELERATE + } + + hideAnimator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + super@BouncerKeyguardMessageArea.setMessage(msg) + } + } + ) + val showAnimator = + ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f).apply { + duration = SHOW_DURATION_MILLIS + interpolator = Interpolators.STANDARD_DECELERATE + } + + showAnimator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + textAboutToShow = null + } + } + ) + + animatorSet.playSequentially(hideAnimator, showAnimator) + animatorSet.start() + } } diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 9151238499dc..40a96b060bc0 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -23,12 +23,21 @@ import android.content.res.Resources import android.text.format.DateFormat import android.util.TypedValue import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags.DOZING_MIGRATION_1 +import com.android.systemui.flags.Flags.REGION_SAMPLING +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.log.dagger.KeyguardClockLog import com.android.systemui.plugins.ClockController -import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.shared.regionsampling.RegionSamplingInstance import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -38,13 +47,20 @@ import java.util.Locale import java.util.TimeZone import java.util.concurrent.Executor import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch /** * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController]. */ open class ClockEventController @Inject constructor( - private val statusBarStateController: StatusBarStateController, + private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val broadcastDispatcher: BroadcastDispatcher, private val batteryController: BatteryController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, @@ -53,12 +69,14 @@ open class ClockEventController @Inject constructor( private val context: Context, @Main private val mainExecutor: Executor, @Background private val bgExecutor: Executor, - private val featureFlags: FeatureFlags, + @KeyguardClockLog private val logBuffer: LogBuffer, + private val featureFlags: FeatureFlags ) { var clock: ClockController? = null set(value) { field = value if (value != null) { + value.setLogBuffer(logBuffer) value.initialize(resources, dozeAmount, 0f) updateRegionSamplers(value) } @@ -70,9 +88,9 @@ open class ClockEventController @Inject constructor( private var isCharging = false private var dozeAmount = 0f private var isKeyguardVisible = false - - private val regionSamplingEnabled = - featureFlags.isEnabled(com.android.systemui.flags.Flags.REGION_SAMPLING) + private var isRegistered = false + private var disposableHandle: DisposableHandle? = null + private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING) private fun updateColors() { if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) { @@ -165,15 +183,6 @@ open class ClockEventController @Inject constructor( } } - private val statusBarStateListener = object : StatusBarStateController.StateListener { - override fun onDozeAmountChanged(linear: Float, eased: Float) { - clock?.animations?.doze(linear) - - isDozing = linear > dozeAmount - dozeAmount = linear - } - } - private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { override fun onKeyguardVisibilityChanged(visible: Boolean) { isKeyguardVisible = visible @@ -195,13 +204,11 @@ open class ClockEventController @Inject constructor( } } - init { - isDozing = statusBarStateController.isDozing - } - - fun registerListeners() { - dozeAmount = statusBarStateController.dozeAmount - isDozing = statusBarStateController.isDozing || dozeAmount != 0f + fun registerListeners(parent: View) { + if (isRegistered) { + return + } + isRegistered = true broadcastDispatcher.registerReceiver( localeBroadcastReceiver, @@ -210,17 +217,31 @@ open class ClockEventController @Inject constructor( configurationController.addCallback(configListener) batteryController.addCallback(batteryCallback) keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) - statusBarStateController.addCallback(statusBarStateListener) smallRegionSampler?.startRegionSampler() largeRegionSampler?.startRegionSampler() + disposableHandle = parent.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + listenForDozing(this) + if (featureFlags.isEnabled(DOZING_MIGRATION_1)) { + listenForDozeAmountTransition(this) + } else { + listenForDozeAmount(this) + } + } + } } fun unregisterListeners() { + if (!isRegistered) { + return + } + isRegistered = false + + disposableHandle?.dispose() broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver) configurationController.removeCallback(configListener) batteryController.removeCallback(batteryCallback) keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) - statusBarStateController.removeCallback(statusBarStateListener) smallRegionSampler?.stopRegionSampler() largeRegionSampler?.stopRegionSampler() } @@ -235,8 +256,38 @@ open class ClockEventController @Inject constructor( largeRegionSampler?.dump(pw) } - companion object { - private val TAG = ClockEventController::class.simpleName - private const val FORMAT_NUMBER = 1234567890 + @VisibleForTesting + internal fun listenForDozeAmount(scope: CoroutineScope): Job { + return scope.launch { + keyguardInteractor.dozeAmount.collect { + dozeAmount = it + clock?.animations?.doze(dozeAmount) + } + } + } + + @VisibleForTesting + internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job { + return scope.launch { + keyguardTransitionInteractor.dozeAmountTransition.collect { + dozeAmount = it.value + clock?.animations?.doze(dozeAmount) + } + } + } + + @VisibleForTesting + internal fun listenForDozing(scope: CoroutineScope): Job { + return scope.launch { + combine ( + keyguardInteractor.dozeAmount, + keyguardInteractor.isDozing, + ) { localDozeAmount, localIsDozing -> + localDozeAmount > dozeAmount || localIsDozing + } + .collect { localIsDozing -> + isDozing = localIsDozing + } + } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index d03ef984d42c..8ebad6c0fdbf 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -127,7 +127,7 @@ public class KeyguardClockSwitch extends RelativeLayout { if (useLargeClock) { out = mSmallClockFrame; in = mLargeClockFrame; - if (indexOfChild(in) == -1) addView(in); + if (indexOfChild(in) == -1) addView(in, 0); direction = -1; statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop() + mSmartspaceTopOffset; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index b450ec35db32..d3cc7ed08a82 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -37,9 +37,8 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.plugins.ClockController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.ClockRegistry; @@ -119,8 +118,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS SecureSettings secureSettings, @Main Executor uiExecutor, DumpManager dumpManager, - ClockEventController clockEventController, - FeatureFlags featureFlags) { + ClockEventController clockEventController) { super(keyguardClockSwitch); mStatusBarStateController = statusBarStateController; mClockRegistry = clockRegistry; @@ -133,7 +131,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mDumpManager = dumpManager; mClockEventController = clockEventController; - mClockRegistry.setEnabled(featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS)); mClockChangedListener = () -> { setClock(mClockRegistry.createCurrentClock()); }; @@ -164,7 +161,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS protected void onViewAttached() { mClockRegistry.registerClockChangeListener(mClockChangedListener); setClock(mClockRegistry.createCurrentClock()); - mClockEventController.registerListeners(); + mClockEventController.registerListeners(mView); mKeyguardClockTopMargin = mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin); @@ -404,5 +401,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS clock.dump(pw); } } + + /** Gets the animations for the current clock. */ + public ClockAnimations getClockAnimations() { + return getClock().getAnimations(); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index f26b9057dc7c..73229c321079 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -152,6 +152,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> } public void startAppearAnimation() { + mMessageAreaController.setMessage(getInitialMessageResId()); mView.startAppearAnimation(); } @@ -169,6 +170,11 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> return view.indexOfChild(mView); } + /** Determines the message to show in the bouncer when it first appears. */ + protected int getInitialMessageResId() { + return 0; + } + /** Factory for a {@link KeyguardInputViewController}. */ public static class Factory { private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java index c2802f7b6843..2bd3ca59b740 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java @@ -18,7 +18,6 @@ package com.android.keyguard; import android.content.res.ColorStateList; import android.content.res.Configuration; -import android.text.TextUtils; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; @@ -100,15 +99,6 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> mView.setMessage(resId); } - /** - * Set Text if KeyguardMessageArea is empty. - */ - public void setMessageIfEmpty(int resId) { - if (TextUtils.isEmpty(mView.getText())) { - setMessage(resId); - } - } - public void setNextMessageColor(ColorStateList colorState) { mView.setNextMessageColor(colorState); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java index 29e912fdab32..0025986c0e5c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java @@ -187,7 +187,7 @@ public class KeyguardPasswordViewController @Override void resetState() { mPasswordEntry.setTextOperationUser(UserHandle.of(KeyguardUpdateMonitor.getCurrentUser())); - mMessageAreaController.setMessage(""); + mMessageAreaController.setMessage(getInitialMessageResId()); final boolean wasDisabled = mPasswordEntry.isEnabled(); mView.setPasswordEntryEnabled(true); mView.setPasswordEntryInputEnabled(true); @@ -207,7 +207,6 @@ public class KeyguardPasswordViewController if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) { showInput(); } - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_password); } private void showInput() { @@ -324,4 +323,9 @@ public class KeyguardPasswordViewController //enabled input method subtype (The current IME should be LatinIME.) || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1; } + + @Override + protected int getInitialMessageResId() { + return R.string.keyguard_enter_your_password; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index 987164557a7a..1f0bd54f8e09 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -298,12 +298,6 @@ public class KeyguardPatternViewController } @Override - public void onResume(int reason) { - super.onResume(reason); - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pattern); - } - - @Override public boolean needsInput() { return false; } @@ -361,7 +355,7 @@ public class KeyguardPatternViewController } private void displayDefaultSecurityMessage() { - mMessageAreaController.setMessage(""); + mMessageAreaController.setMessage(getInitialMessageResId()); } private void handleAttemptLockout(long elapsedRealtimeDeadline) { @@ -392,4 +386,9 @@ public class KeyguardPatternViewController }.start(); } + + @Override + protected int getInitialMessageResId() { + return R.string.keyguard_enter_your_pattern; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 59a018ad51df..f7423ed12e68 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -127,7 +127,6 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB public void onResume(int reason) { super.onResume(reason); mPasswordEntry.requestFocus(); - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin); } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index 89fcc47caf57..7876f071fdf5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -76,20 +76,13 @@ public class KeyguardPinViewController } @Override - void resetState() { - super.resetState(); - mMessageAreaController.setMessage(""); - } - - @Override - public void startAppearAnimation() { - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin); - super.startAppearAnimation(); - } - - @Override public boolean startDisappearAnimation(Runnable finishRunnable) { return mView.startDisappearAnimation( mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable); } + + @Override + protected int getInitialMessageResId() { + return R.string.keyguard_enter_your_pin; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index bcd1a1ee2696..81305f90e2b8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -219,13 +219,16 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard }; - private SwipeListener mSwipeListener = new SwipeListener() { + private final SwipeListener mSwipeListener = new SwipeListener() { @Override public void onSwipeUp() { if (!mUpdateMonitor.isFaceDetectionRunning()) { - mUpdateMonitor.requestFaceAuth(true, FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); + boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); mKeyguardSecurityCallback.userActivity(); - showMessage(null, null); + if (didFaceAuthRun) { + showMessage(null, null); + } } if (mUpdateMonitor.isFaceEnrolled()) { mUpdateMonitor.requestActiveUnlock( @@ -234,7 +237,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } } }; - private ConfigurationController.ConfigurationListener mConfigurationListener = + private final ConfigurationController.ConfigurationListener mConfigurationListener = new ConfigurationController.ConfigurationListener() { @Override public void onThemeChanged() { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index e9f06eddf261..784974778af5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -20,6 +20,7 @@ import android.graphics.Rect; import android.util.Slog; import com.android.keyguard.KeyguardClockSwitch.ClockSize; +import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.stack.AnimationProperties; @@ -232,4 +233,9 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV mView.setClipBounds(null); } } + + /** Gets the animations for the current clock. */ + public ClockAnimations getClockAnimations() { + return mKeyguardClockSwitchController.getClockAnimations(); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 46e187e041e4..cb1330dbd53d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -2366,11 +2366,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * @param userInitiatedRequest true if the user explicitly requested face auth * @param reason One of the reasons {@link FaceAuthApiRequestReason} on why this API is being * invoked. + * @return current face auth detection state, true if it is running. */ - public void requestFaceAuth(boolean userInitiatedRequest, + public boolean requestFaceAuth(boolean userInitiatedRequest, @FaceAuthApiRequestReason String reason) { mLogger.logFaceAuthRequested(userInitiatedRequest, reason); updateFaceListeningState(BIOMETRIC_ACTION_START, apiRequestReasonToUiEvent(reason)); + return isFaceDetectionRunning(); } /** @@ -2380,10 +2382,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab stopListeningForFace(FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER); } - public boolean isFaceScanning() { - return mFaceRunningState == BIOMETRIC_STATE_RUNNING; - } - private void updateFaceListeningState(int action, @NonNull FaceAuthUiEvent faceAuthUiEvent) { // If this message exists, we should not authenticate again until this message is // consumed by the handler @@ -2431,7 +2429,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * Attempts to trigger active unlock from trust agent. */ private void requestActiveUnlock( - ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, + @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, String reason, boolean dismissKeyguard ) { @@ -2461,7 +2459,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * Only dismisses the keyguard under certain conditions. */ public void requestActiveUnlock( - ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, + @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, String extraReason ) { final boolean canFaceBypass = isFaceEnrolled() && mKeyguardBypassController != null @@ -2728,7 +2726,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return shouldListen; } - private void maybeLogListenerModelData(KeyguardListenModel model) { + private void maybeLogListenerModelData(@NonNull KeyguardListenModel model) { mLogger.logKeyguardListenerModel(model); if (model instanceof KeyguardActiveUnlockModel) { diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index c41b7522ce93..fe7c70ae4c7e 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -23,6 +23,8 @@ import static com.android.keyguard.LockIconView.ICON_LOCK; import static com.android.keyguard.LockIconView.ICON_UNLOCK; import static com.android.systemui.classifier.Classifier.LOCK_ICON; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; +import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.content.res.Configuration; import android.content.res.Resources; @@ -44,6 +46,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.android.systemui.Dumpable; @@ -53,6 +56,10 @@ import com.android.systemui.biometrics.AuthRippleController; import com.android.systemui.biometrics.UdfpsController; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; @@ -65,6 +72,7 @@ import com.android.systemui.util.concurrency.DelayableExecutor; import java.io.PrintWriter; import java.util.Objects; +import java.util.function.Consumer; import javax.inject.Inject; @@ -101,6 +109,9 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @NonNull private CharSequence mLockedLabel; @NonNull private final VibratorHelper mVibrator; @Nullable private final AuthRippleController mAuthRippleController; + @NonNull private final FeatureFlags mFeatureFlags; + @NonNull private final KeyguardTransitionInteractor mTransitionInteractor; + @NonNull private final KeyguardInteractor mKeyguardInteractor; // Tracks the velocity of a touch to help filter out the touches that move too fast. private VelocityTracker mVelocityTracker; @@ -137,6 +148,20 @@ public class LockIconViewController extends ViewController<LockIconView> impleme private boolean mDownDetected; private final Rect mSensorTouchLocation = new Rect(); + @VisibleForTesting + final Consumer<TransitionStep> mDozeTransitionCallback = (TransitionStep step) -> { + mInterpolatedDarkAmount = step.getValue(); + mView.setDozeAmount(step.getValue()); + updateBurnInOffsets(); + }; + + @VisibleForTesting + final Consumer<Boolean> mIsDozingCallback = (Boolean isDozing) -> { + mIsDozing = isDozing; + updateBurnInOffsets(); + updateVisibility(); + }; + @Inject public LockIconViewController( @Nullable LockIconView view, @@ -152,7 +177,10 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @NonNull @Main DelayableExecutor executor, @NonNull VibratorHelper vibrator, @Nullable AuthRippleController authRippleController, - @NonNull @Main Resources resources + @NonNull @Main Resources resources, + @NonNull KeyguardTransitionInteractor transitionInteractor, + @NonNull KeyguardInteractor keyguardInteractor, + @NonNull FeatureFlags featureFlags ) { super(view); mStatusBarStateController = statusBarStateController; @@ -166,6 +194,9 @@ public class LockIconViewController extends ViewController<LockIconView> impleme mExecutor = executor; mVibrator = vibrator; mAuthRippleController = authRippleController; + mTransitionInteractor = transitionInteractor; + mKeyguardInteractor = keyguardInteractor; + mFeatureFlags = featureFlags; mMaxBurnInOffsetX = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x); mMaxBurnInOffsetY = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y); @@ -182,6 +213,12 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @Override protected void onInit() { mView.setAccessibilityDelegate(mAccessibilityDelegate); + + if (mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) { + collectFlow(mView, mTransitionInteractor.getDozeAmountTransition(), + mDozeTransitionCallback); + collectFlow(mView, mKeyguardInteractor.isDozing(), mIsDozingCallback); + } } @Override @@ -377,14 +414,17 @@ public class LockIconViewController extends ViewController<LockIconView> impleme pw.println(" mShowUnlockIcon: " + mShowUnlockIcon); pw.println(" mShowLockIcon: " + mShowLockIcon); pw.println(" mShowAodUnlockedIcon: " + mShowAodUnlockedIcon); - pw.println(" mIsDozing: " + mIsDozing); - pw.println(" mIsBouncerShowing: " + mIsBouncerShowing); - pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric); - pw.println(" mRunningFPS: " + mRunningFPS); - pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen); - pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState)); - pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount); - pw.println(" mSensorTouchLocation: " + mSensorTouchLocation); + pw.println(); + pw.println(" mIsDozing: " + mIsDozing); + pw.println(" isFlagEnabled(DOZING_MIGRATION_1): " + + mFeatureFlags.isEnabled(DOZING_MIGRATION_1)); + pw.println(" mIsBouncerShowing: " + mIsBouncerShowing); + pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric); + pw.println(" mRunningFPS: " + mRunningFPS); + pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen); + pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState)); + pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount); + pw.println(" mSensorTouchLocation: " + mSensorTouchLocation); if (mView != null) { mView.dump(pw, args); @@ -425,16 +465,20 @@ public class LockIconViewController extends ViewController<LockIconView> impleme new StatusBarStateController.StateListener() { @Override public void onDozeAmountChanged(float linear, float eased) { - mInterpolatedDarkAmount = eased; - mView.setDozeAmount(eased); - updateBurnInOffsets(); + if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) { + mInterpolatedDarkAmount = eased; + mView.setDozeAmount(eased); + updateBurnInOffsets(); + } } @Override public void onDozingChanged(boolean isDozing) { - mIsDozing = isDozing; - updateBurnInOffsets(); - updateVisibility(); + if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) { + mIsDozing = isDozing; + updateBurnInOffsets(); + updateVisibility(); + } } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java index c4be1ba53503..72a44bd198f2 100644 --- a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java +++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java @@ -21,9 +21,14 @@ import java.util.List; import dagger.Module; import dagger.Provides; -/** Dagger Module for clock package. */ +/** + * Dagger Module for clock package. + * + * @deprecated Migrate to ClockRegistry + */ @Module -public abstract class ClockModule { +@Deprecated +public abstract class ClockInfoModule { /** */ @Provides diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java new file mode 100644 index 000000000000..9767313331d3 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -0,0 +1,55 @@ +/* + * 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.keyguard.dagger; + +import android.content.Context; +import android.os.Handler; +import android.os.UserHandle; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; +import com.android.systemui.shared.clocks.ClockRegistry; +import com.android.systemui.shared.clocks.DefaultClockProvider; +import com.android.systemui.shared.plugins.PluginManager; + +import dagger.Module; +import dagger.Provides; + +/** Dagger Module for clocks. */ +@Module +public abstract class ClockRegistryModule { + /** Provide the ClockRegistry as a singleton so that it is not instantiated more than once. */ + @Provides + @SysUISingleton + public static ClockRegistry getClockRegistry( + @Application Context context, + PluginManager pluginManager, + @Main Handler handler, + DefaultClockProvider defaultClockProvider, + FeatureFlags featureFlags) { + return new ClockRegistry( + context, + pluginManager, + handler, + featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS), + UserHandle.USER_ALL, + defaultClockProvider); + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt index 2c2ab7b39161..6264ce7273f1 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt @@ -17,9 +17,9 @@ package com.android.keyguard.logging import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG import com.android.systemui.log.dagger.BiometricMessagesLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG import javax.inject.Inject /** Helper class for logging for [com.android.systemui.biometrics.FaceHelpMessageDeferral] */ diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt index 50012a589b5a..46f3d4e5f6aa 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt @@ -16,15 +16,15 @@ package com.android.keyguard.logging -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.VERBOSE -import com.android.systemui.log.LogLevel.WARNING -import com.android.systemui.log.MessageInitializer -import com.android.systemui.log.MessagePrinter import com.android.systemui.log.dagger.KeyguardLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogLevel.WARNING +import com.android.systemui.plugins.log.MessageInitializer +import com.android.systemui.plugins.log.MessagePrinter import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 2eee95738b7b..2f79e30a0b5b 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -22,13 +22,13 @@ import android.telephony.SubscriptionInfo import com.android.keyguard.ActiveUnlockConfig import com.android.keyguard.KeyguardListenModel import com.android.keyguard.KeyguardUpdateMonitorCallback -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.VERBOSE -import com.android.systemui.log.LogLevel.WARNING +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject @@ -51,7 +51,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( fun log(@CompileTimeConstant msg: String, level: LogLevel) = logBuffer.log(TAG, level, msg) - fun logActiveUnlockTriggered(reason: String) { + fun logActiveUnlockTriggered(reason: String?) { logBuffer.log("ActiveUnlock", DEBUG, { str1 = reason }, { "initiate active unlock triggerReason=$str1" }) @@ -101,14 +101,14 @@ class KeyguardUpdateMonitorLogger @Inject constructor( { "Face authenticated for wrong user: $int1" }) } - fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String) { + fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String?) { logBuffer.log(TAG, DEBUG, { int1 = msgId str1 = helpMsg }, { "Face help received, msgId: $int1 msg: $str1" }) } - fun logFaceAuthRequested(userInitiatedRequest: Boolean, reason: String) { + fun logFaceAuthRequested(userInitiatedRequest: Boolean, reason: String?) { logBuffer.log(TAG, DEBUG, { bool1 = userInitiatedRequest str1 = reason @@ -187,7 +187,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( { "No Profile Owner or Device Owner supervision app found for User $int1" }) } - fun logPhoneStateChanged(newState: String) { + fun logPhoneStateChanged(newState: String?) { logBuffer.log(TAG, DEBUG, { str1 = newState }, { "handlePhoneStateChanged($str1)" }) @@ -240,7 +240,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( }, { "handleServiceStateChange(subId=$int1, serviceState=$str1)" }) } - fun logServiceStateIntent(action: String, serviceState: ServiceState?, subId: Int) { + fun logServiceStateIntent(action: String?, serviceState: ServiceState?, subId: Int) { logBuffer.log(TAG, VERBOSE, { str1 = action str2 = "$serviceState" @@ -256,7 +256,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( }, { "handleSimStateChange(subId=$int1, slotId=$int2, state=$long1)" }) } - fun logSimStateFromIntent(action: String, extraSimState: String, slotId: Int, subId: Int) { + fun logSimStateFromIntent(action: String?, extraSimState: String?, slotId: Int, subId: Int) { logBuffer.log(TAG, VERBOSE, { str1 = action str2 = extraSimState @@ -289,7 +289,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( { "SubInfo:$str1" }) } - fun logTimeFormatChanged(newTimeFormat: String) { + fun logTimeFormatChanged(newTimeFormat: String?) { logBuffer.log(TAG, DEBUG, { str1 = newTimeFormat }, { "handleTimeFormatUpdate timeFormat=$str1" }) @@ -338,18 +338,18 @@ class KeyguardUpdateMonitorLogger @Inject constructor( fun logUserRequestedUnlock( requestOrigin: ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN, - reason: String, + reason: String?, dismissKeyguard: Boolean ) { logBuffer.log("ActiveUnlock", DEBUG, { - str1 = requestOrigin.name + str1 = requestOrigin?.name str2 = reason bool1 = dismissKeyguard }, { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" }) } fun logShowTrustGrantedMessage( - message: String + message: String? ) { logBuffer.log(TAG, DEBUG, { str1 = message diff --git a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt index a3351e1a6440..5d52056d8b17 100644 --- a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt +++ b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt @@ -86,30 +86,38 @@ open class DisplayCutoutBaseView : View, RegionInterceptableView { onUpdate() } - fun onDisplayChanged(newDisplayUniqueId: String?) { + fun updateConfiguration(newDisplayUniqueId: String?) { + val info = DisplayInfo() + context.display?.getDisplayInfo(info) val oldMode: Display.Mode? = displayMode - val display: Display? = context.display - displayMode = display?.mode + displayMode = info.mode - if (displayUniqueId != display?.uniqueId) { - displayUniqueId = display?.uniqueId - shouldDrawCutout = DisplayCutout.getFillBuiltInDisplayCutout( - context.resources, displayUniqueId - ) - } + updateDisplayUniqueId(info.uniqueId) // Skip if display mode or cutout hasn't changed. if (!displayModeChanged(oldMode, displayMode) && - display?.cutout == displayInfo.displayCutout) { + displayInfo.displayCutout == info.displayCutout && + displayRotation == info.rotation) { return } - if (newDisplayUniqueId == display?.uniqueId) { + if (newDisplayUniqueId == info.uniqueId) { + displayRotation = info.rotation updateCutout() updateProtectionBoundingPath() onUpdate() } } + open fun updateDisplayUniqueId(newDisplayUniqueId: String?) { + if (displayUniqueId != newDisplayUniqueId) { + displayUniqueId = newDisplayUniqueId + shouldDrawCutout = DisplayCutout.getFillBuiltInDisplayCutout( + context.resources, displayUniqueId + ) + invalidate() + } + } + open fun updateRotation(rotation: Int) { displayRotation = rotation updateCutout() diff --git a/packages/SystemUI/src/com/android/systemui/Dumpable.java b/packages/SystemUI/src/com/android/systemui/Dumpable.java index 652595100c0f..73fdce6c9045 100644 --- a/packages/SystemUI/src/com/android/systemui/Dumpable.java +++ b/packages/SystemUI/src/com/android/systemui/Dumpable.java @@ -30,7 +30,6 @@ public interface Dumpable { /** * Called when it's time to dump the internal state - * @param fd A file descriptor. * @param pw Where to write your dump to. * @param args Arguments. */ diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt index c5955860aebf..3e0fa455d39e 100644 --- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt @@ -19,6 +19,7 @@ package com.android.systemui import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet +import android.animation.TimeInterpolator import android.animation.ValueAnimator import android.content.Context import android.graphics.Canvas @@ -55,7 +56,7 @@ class FaceScanningOverlay( private val rimRect = RectF() private var cameraProtectionColor = Color.BLACK var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context, - com.android.systemui.R.attr.wallpaperTextColorAccent) + R.attr.wallpaperTextColorAccent) private var cameraProtectionAnimator: ValueAnimator? = null var hideOverlayRunnable: Runnable? = null var faceAuthSucceeded = false @@ -84,46 +85,19 @@ class FaceScanningOverlay( } override fun drawCutoutProtection(canvas: Canvas) { - if (rimProgress > HIDDEN_RIM_SCALE && !protectionRect.isEmpty) { - val rimPath = Path(protectionPath) - val scaleMatrix = Matrix().apply { - val rimBounds = RectF() - rimPath.computeBounds(rimBounds, true) - setScale(rimProgress, rimProgress, rimBounds.centerX(), rimBounds.centerY()) - } - rimPath.transform(scaleMatrix) - rimPaint.style = Paint.Style.FILL - val rimPaintAlpha = rimPaint.alpha - rimPaint.color = ColorUtils.blendARGB( - faceScanningAnimColor, - Color.WHITE, - statusBarStateController.dozeAmount) - rimPaint.alpha = rimPaintAlpha - canvas.drawPath(rimPath, rimPaint) + if (protectionRect.isEmpty) { + return } - - if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE && - !protectionRect.isEmpty) { - val scaledProtectionPath = Path(protectionPath) - val scaleMatrix = Matrix().apply { - val protectionPathRect = RectF() - scaledProtectionPath.computeBounds(protectionPathRect, true) - setScale(cameraProtectionProgress, cameraProtectionProgress, - protectionPathRect.centerX(), protectionPathRect.centerY()) - } - scaledProtectionPath.transform(scaleMatrix) - paint.style = Paint.Style.FILL - paint.color = cameraProtectionColor - canvas.drawPath(scaledProtectionPath, paint) + if (rimProgress > HIDDEN_RIM_SCALE) { + drawFaceScanningRim(canvas) + } + if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE) { + drawCameraProtection(canvas) } - } - - override fun updateVisOnUpdateCutout(): Boolean { - return false // instead, we always update the visibility whenever face scanning starts/ends } override fun enableShowProtection(show: Boolean) { - val showScanningAnimNow = keyguardUpdateMonitor.isFaceScanning && show + val showScanningAnimNow = keyguardUpdateMonitor.isFaceDetectionRunning && show if (showScanningAnimNow == showScanningAnim) { return } @@ -152,91 +126,26 @@ class FaceScanningOverlay( if (showScanningAnim) Interpolators.STANDARD_ACCELERATE else if (faceAuthSucceeded) Interpolators.STANDARD else Interpolators.STANDARD_DECELERATE - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - cameraProtectionProgress = animation.animatedValue as Float - invalidate() - }) + addUpdateListener(this@FaceScanningOverlay::updateCameraProtectionProgress) addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { cameraProtectionAnimator = null if (!showScanningAnim) { - visibility = View.INVISIBLE - hideOverlayRunnable?.run() - hideOverlayRunnable = null - requestLayout() + hide() } } }) } rimAnimator?.cancel() - rimAnimator = AnimatorSet().apply { - if (showScanningAnim) { - val rimAppearAnimator = ValueAnimator.ofFloat(SHOW_CAMERA_PROTECTION_SCALE, - PULSE_RADIUS_OUT).apply { - duration = PULSE_APPEAR_DURATION - interpolator = Interpolators.STANDARD_DECELERATE - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - } - - // animate in camera protection, rim, and then pulse in/out - playSequentially(cameraProtectionAnimator, rimAppearAnimator, - createPulseAnimator(), createPulseAnimator(), - createPulseAnimator(), createPulseAnimator(), - createPulseAnimator(), createPulseAnimator()) - } else { - val rimDisappearAnimator = ValueAnimator.ofFloat( - rimProgress, - if (faceAuthSucceeded) PULSE_RADIUS_SUCCESS - else SHOW_CAMERA_PROTECTION_SCALE - ).apply { - duration = - if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION - else PULSE_ERROR_DISAPPEAR_DURATION - interpolator = - if (faceAuthSucceeded) Interpolators.STANDARD_DECELERATE - else Interpolators.STANDARD - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - rimProgress = HIDDEN_RIM_SCALE - invalidate() - } - }) - } - if (faceAuthSucceeded) { - val successOpacityAnimator = ValueAnimator.ofInt(255, 0).apply { - duration = PULSE_SUCCESS_DISAPPEAR_DURATION - interpolator = Interpolators.LINEAR - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimPaint.alpha = animation.animatedValue as Int - invalidate() - }) - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - rimPaint.alpha = 255 - invalidate() - } - }) - } - val rimSuccessAnimator = AnimatorSet() - rimSuccessAnimator.playTogether(rimDisappearAnimator, successOpacityAnimator) - playTogether(rimSuccessAnimator, cameraProtectionAnimator) - } else { - playTogether(rimDisappearAnimator, cameraProtectionAnimator) - } - } - + rimAnimator = if (showScanningAnim) { + createFaceScanningRimAnimator() + } else if (faceAuthSucceeded) { + createFaceSuccessRimAnimator() + } else { + createFaceNotSuccessRimAnimator() + } + rimAnimator?.apply { addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { rimAnimator = null @@ -245,34 +154,12 @@ class FaceScanningOverlay( } } }) - start() } + rimAnimator?.start() } - fun createPulseAnimator(): AnimatorSet { - return AnimatorSet().apply { - val pulseInwards = ValueAnimator.ofFloat( - PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply { - duration = PULSE_DURATION_INWARDS - interpolator = Interpolators.STANDARD - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - } - val pulseOutwards = ValueAnimator.ofFloat( - PULSE_RADIUS_IN, PULSE_RADIUS_OUT).apply { - duration = PULSE_DURATION_OUTWARDS - interpolator = Interpolators.STANDARD - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - } - playSequentially(pulseInwards, pulseOutwards) - } + override fun updateVisOnUpdateCutout(): Boolean { + return false // instead, we always update the visibility whenever face scanning starts/ends } override fun updateProtectionBoundingPath() { @@ -290,17 +177,153 @@ class FaceScanningOverlay( // Make sure that our measured height encompasses the extra space for the animation mTotalBounds.union(mBoundingRect) mTotalBounds.union( - rimRect.left.toInt(), - rimRect.top.toInt(), - rimRect.right.toInt(), - rimRect.bottom.toInt()) + rimRect.left.toInt(), + rimRect.top.toInt(), + rimRect.right.toInt(), + rimRect.bottom.toInt()) setMeasuredDimension( - resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0), - resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)) + resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0), + resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)) } else { setMeasuredDimension( - resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0), - resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0)) + resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0), + resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0)) + } + } + + private fun drawFaceScanningRim(canvas: Canvas) { + val rimPath = Path(protectionPath) + scalePath(rimPath, rimProgress) + rimPaint.style = Paint.Style.FILL + val rimPaintAlpha = rimPaint.alpha + rimPaint.color = ColorUtils.blendARGB( + faceScanningAnimColor, + Color.WHITE, + statusBarStateController.dozeAmount + ) + rimPaint.alpha = rimPaintAlpha + canvas.drawPath(rimPath, rimPaint) + } + + private fun drawCameraProtection(canvas: Canvas) { + val scaledProtectionPath = Path(protectionPath) + scalePath(scaledProtectionPath, cameraProtectionProgress) + paint.style = Paint.Style.FILL + paint.color = cameraProtectionColor + canvas.drawPath(scaledProtectionPath, paint) + } + + private fun createFaceSuccessRimAnimator(): AnimatorSet { + val rimSuccessAnimator = AnimatorSet() + rimSuccessAnimator.playTogether( + createRimDisappearAnimator( + PULSE_RADIUS_SUCCESS, + PULSE_SUCCESS_DISAPPEAR_DURATION, + Interpolators.STANDARD_DECELERATE + ), + createSuccessOpacityAnimator(), + ) + return AnimatorSet().apply { + playTogether(rimSuccessAnimator, cameraProtectionAnimator) + } + } + + private fun createFaceNotSuccessRimAnimator(): AnimatorSet { + return AnimatorSet().apply { + playTogether( + createRimDisappearAnimator( + SHOW_CAMERA_PROTECTION_SCALE, + PULSE_ERROR_DISAPPEAR_DURATION, + Interpolators.STANDARD + ), + cameraProtectionAnimator, + ) + } + } + + private fun createRimDisappearAnimator( + endValue: Float, + animDuration: Long, + timeInterpolator: TimeInterpolator + ): ValueAnimator { + return ValueAnimator.ofFloat(rimProgress, endValue).apply { + duration = animDuration + interpolator = timeInterpolator + addUpdateListener(this@FaceScanningOverlay::updateRimProgress) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rimProgress = HIDDEN_RIM_SCALE + invalidate() + } + }) + } + } + + private fun createSuccessOpacityAnimator(): ValueAnimator { + return ValueAnimator.ofInt(255, 0).apply { + duration = PULSE_SUCCESS_DISAPPEAR_DURATION + interpolator = Interpolators.LINEAR + addUpdateListener(this@FaceScanningOverlay::updateRimAlpha) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rimPaint.alpha = 255 + invalidate() + } + }) + } + } + + private fun createFaceScanningRimAnimator(): AnimatorSet { + return AnimatorSet().apply { + playSequentially( + cameraProtectionAnimator, + createRimAppearAnimator(), + createPulseAnimator() + ) + } + } + + private fun createRimAppearAnimator(): ValueAnimator { + return ValueAnimator.ofFloat( + SHOW_CAMERA_PROTECTION_SCALE, + PULSE_RADIUS_OUT + ).apply { + duration = PULSE_APPEAR_DURATION + interpolator = Interpolators.STANDARD_DECELERATE + addUpdateListener(this@FaceScanningOverlay::updateRimProgress) + } + } + + private fun hide() { + visibility = INVISIBLE + hideOverlayRunnable?.run() + hideOverlayRunnable = null + requestLayout() + } + + private fun updateRimProgress(animator: ValueAnimator) { + rimProgress = animator.animatedValue as Float + invalidate() + } + + private fun updateCameraProtectionProgress(animator: ValueAnimator) { + cameraProtectionProgress = animator.animatedValue as Float + invalidate() + } + + private fun updateRimAlpha(animator: ValueAnimator) { + rimPaint.alpha = animator.animatedValue as Int + invalidate() + } + + private fun createPulseAnimator(): ValueAnimator { + return ValueAnimator.ofFloat( + PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply { + duration = HALF_PULSE_DURATION + interpolator = Interpolators.STANDARD + repeatCount = 11 // Pulse inwards and outwards, reversing direction, 6 times + repeatMode = ValueAnimator.REVERSE + addUpdateListener(this@FaceScanningOverlay::updateRimProgress) } } @@ -363,13 +386,24 @@ class FaceScanningOverlay( private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L private const val PULSE_APPEAR_DURATION = 250L // without start delay - private const val PULSE_DURATION_INWARDS = 500L - private const val PULSE_DURATION_OUTWARDS = 500L + private const val HALF_PULSE_DURATION = 500L private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay + + private fun scalePath(path: Path, scalingFactor: Float) { + val scaleMatrix = Matrix().apply { + val boundingRectangle = RectF() + path.computeBounds(boundingRectangle, true) + setScale( + scalingFactor, scalingFactor, + boundingRectangle.centerX(), boundingRectangle.centerY() + ) + } + path.transform(scaleMatrix) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt b/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt new file mode 100644 index 000000000000..4c3a7ff4e2eb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt @@ -0,0 +1,7 @@ +package com.android.systemui + +import com.android.systemui.dump.nano.SystemUIProtoDump + +interface ProtoDumpable : Dumpable { + fun dumpProto(systemUIProtoDump: SystemUIProtoDump, args: Array<String>) +} diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index b5f42a164495..11d579d481c1 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -456,7 +456,6 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { } } - boolean needToUpdateProviderViews = false; final String newUniqueId = mDisplayInfo.uniqueId; if (!Objects.equals(newUniqueId, mDisplayUniqueId)) { mDisplayUniqueId = newUniqueId; @@ -474,37 +473,6 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { setupDecorations(); return; } - - if (mScreenDecorHwcLayer != null) { - updateHwLayerRoundedCornerDrawable(); - updateHwLayerRoundedCornerExistAndSize(); - } - needToUpdateProviderViews = true; - } - - final float newRatio = getPhysicalPixelDisplaySizeRatio(); - if (mRoundedCornerResDelegate.getPhysicalPixelDisplaySizeRatio() != newRatio) { - mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(newRatio); - if (mScreenDecorHwcLayer != null) { - updateHwLayerRoundedCornerExistAndSize(); - } - needToUpdateProviderViews = true; - } - - if (needToUpdateProviderViews) { - updateOverlayProviderViews(null); - } else { - updateOverlayProviderViews(new Integer[] { - mFaceScanningViewId, - R.id.display_cutout, - R.id.display_cutout_left, - R.id.display_cutout_right, - R.id.display_cutout_bottom, - }); - } - - if (mScreenDecorHwcLayer != null) { - mScreenDecorHwcLayer.onDisplayChanged(newUniqueId); } } }; @@ -1070,9 +1038,11 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { && (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod))) { mRotation = newRotation; mDisplayMode = newMod; + mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio( + getPhysicalPixelDisplaySizeRatio()); if (mScreenDecorHwcLayer != null) { mScreenDecorHwcLayer.pendingConfigChange = false; - mScreenDecorHwcLayer.updateRotation(mRotation); + mScreenDecorHwcLayer.updateConfiguration(mDisplayUniqueId); updateHwLayerRoundedCornerExistAndSize(); updateHwLayerRoundedCornerDrawable(); } @@ -1111,7 +1081,8 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { context.getResources(), context.getDisplay().getUniqueId()); } - private void updateOverlayProviderViews(@Nullable Integer[] filterIds) { + @VisibleForTesting + void updateOverlayProviderViews(@Nullable Integer[] filterIds) { if (mOverlays == null) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java index 7bcba3cc1c46..50e03992df49 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java @@ -121,6 +121,6 @@ public class SystemUIService extends Service { DumpHandler.PRIORITY_ARG_CRITICAL}; } - mDumpHandler.dump(pw, massagedArgs); + mDumpHandler.dump(fd, pw, massagedArgs); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index e74d8106b2f0..80c6c48cb7ee 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -127,7 +127,7 @@ public class AuthContainerView extends LinearLayout private final ScrollView mBiometricScrollView; private final View mPanelView; private final float mTranslationY; - @ContainerState private int mContainerState = STATE_UNKNOWN; + @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN; private final Set<Integer> mFailedModalities = new HashSet<Integer>(); private final OnBackInvokedCallback mBackCallback = this::onBackInvoked; @@ -485,45 +485,18 @@ public class AuthContainerView extends LinearLayout mContainerState = STATE_SHOWING; } else { mContainerState = STATE_ANIMATING_IN; - // The background panel and content are different views since we need to be able to - // animate them separately in other places. - mPanelView.setY(mTranslationY); - mBiometricScrollView.setY(mTranslationY); - + setY(mTranslationY); setAlpha(0f); final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS; postOnAnimation(() -> { - mPanelView.animate() - .translationY(0) - .setDuration(animateDuration) - .setInterpolator(mLinearOutSlowIn) - .setListener(getJankListener(mPanelView, SHOW, animateDuration)) - .withLayer() - .withEndAction(this::onDialogAnimatedIn) - .start(); - mBiometricScrollView.animate() - .translationY(0) - .setDuration(animateDuration) - .setInterpolator(mLinearOutSlowIn) - .setListener(getJankListener(mBiometricScrollView, SHOW, animateDuration)) - .withLayer() - .start(); - if (mCredentialView != null && mCredentialView.isAttachedToWindow()) { - mCredentialView.setY(mTranslationY); - mCredentialView.animate() - .translationY(0) - .setDuration(animateDuration) - .setInterpolator(mLinearOutSlowIn) - .setListener(getJankListener(mCredentialView, SHOW, animateDuration)) - .withLayer() - .start(); - } animate() .alpha(1f) + .translationY(0) .setDuration(animateDuration) .setInterpolator(mLinearOutSlowIn) .withLayer() .setListener(getJankListener(this, SHOW, animateDuration)) + .withEndAction(this::onDialogAnimatedIn) .start(); }); } @@ -657,11 +630,25 @@ public class AuthContainerView extends LinearLayout wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle())); } + private void forceExecuteAnimatedIn() { + if (mContainerState == STATE_ANIMATING_IN) { + //clear all animators + if (mCredentialView != null && mCredentialView.isAttachedToWindow()) { + mCredentialView.animate().cancel(); + } + mPanelView.animate().cancel(); + mBiometricView.animate().cancel(); + animate().cancel(); + onDialogAnimatedIn(); + } + } + @Override public void dismissWithoutCallback(boolean animate) { if (animate) { animateAway(false /* sendReason */, 0 /* reason */); } else { + forceExecuteAnimatedIn(); removeWindowIfAttached(); } } @@ -788,32 +775,9 @@ public class AuthContainerView extends LinearLayout final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS; postOnAnimation(() -> { - mPanelView.animate() - .translationY(mTranslationY) - .setDuration(animateDuration) - .setInterpolator(mLinearOutSlowIn) - .setListener(getJankListener(mPanelView, DISMISS, animateDuration)) - .withLayer() - .withEndAction(endActionRunnable) - .start(); - mBiometricScrollView.animate() - .translationY(mTranslationY) - .setDuration(animateDuration) - .setInterpolator(mLinearOutSlowIn) - .setListener(getJankListener(mBiometricScrollView, DISMISS, animateDuration)) - .withLayer() - .start(); - if (mCredentialView != null && mCredentialView.isAttachedToWindow()) { - mCredentialView.animate() - .translationY(mTranslationY) - .setDuration(animateDuration) - .setInterpolator(mLinearOutSlowIn) - .setListener(getJankListener(mCredentialView, DISMISS, animateDuration)) - .withLayer() - .start(); - } animate() .alpha(0f) + .translationY(mTranslationY) .setDuration(animateDuration) .setInterpolator(mLinearOutSlowIn) .setListener(getJankListener(this, DISMISS, animateDuration)) @@ -828,6 +792,7 @@ public class AuthContainerView extends LinearLayout mWindowManager.updateViewLayout(this, lp); }) .withLayer() + .withEndAction(endActionRunnable) .start(); }); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index d43e5d9d09cc..c015a21c7db4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -294,6 +294,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } }); mUdfpsController.setAuthControllerUpdateUdfpsLocation(this::updateUdfpsLocation); + mUdfpsController.setUdfpsDisplayMode(new UdfpsDisplayMode(mContext, mExecution, + this)); mUdfpsBounds = mUdfpsProps.get(0).getLocation().getRect(); } @@ -626,17 +628,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mUdfpsController.onAodInterrupt(screenX, screenY, major, minor); } - /** - * Cancel a fingerprint scan manually. This will get rid of the white circle on the udfps - * sensor area even if the user hasn't explicitly lifted their finger yet. - */ - public void onCancelUdfps() { - if (mUdfpsController == null) { - return; - } - mUdfpsController.onCancelUdfps(); - } - private void sendResultAndCleanUp(@DismissedReason int reason, @Nullable byte[] credentialAttestation) { if (mReceiver == null) { @@ -964,8 +955,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } else { Log.w(TAG, "onBiometricError callback but dialog is gone"); } - - onCancelUdfps(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java index 0892612d1825..76cd3f4c4f1d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java @@ -16,12 +16,21 @@ package com.android.systemui.biometrics; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.view.WindowInsets.Type.ime; + import android.annotation.NonNull; import android.content.Context; +import android.graphics.Insets; import android.os.UserHandle; import android.text.InputType; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnApplyWindowInsetsListener; +import android.view.ViewGroup; +import android.view.WindowInsets; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.ImeAwareEditText; @@ -31,18 +40,24 @@ import com.android.internal.widget.LockPatternChecker; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockscreenCredential; import com.android.internal.widget.VerifyCredentialResponse; +import com.android.systemui.Dumpable; import com.android.systemui.R; +import java.io.PrintWriter; + /** * Pin and Password UI */ public class AuthCredentialPasswordView extends AuthCredentialView - implements TextView.OnEditorActionListener { + implements TextView.OnEditorActionListener, OnApplyWindowInsetsListener, Dumpable { private static final String TAG = "BiometricPrompt/AuthCredentialPasswordView"; private final InputMethodManager mImm; private ImeAwareEditText mPasswordField; + private ViewGroup mAuthCredentialHeader; + private ViewGroup mAuthCredentialInput; + private int mBottomInset = 0; public AuthCredentialPasswordView(Context context, AttributeSet attrs) { @@ -53,6 +68,9 @@ public class AuthCredentialPasswordView extends AuthCredentialView @Override protected void onFinishInflate() { super.onFinishInflate(); + + mAuthCredentialHeader = findViewById(R.id.auth_credential_header); + mAuthCredentialInput = findViewById(R.id.auth_credential_input); mPasswordField = findViewById(R.id.lockPassword); mPasswordField.setOnEditorActionListener(this); // TODO: De-dupe the logic with AuthContainerView @@ -66,6 +84,8 @@ public class AuthCredentialPasswordView extends AuthCredentialView } return true; }); + + setOnApplyWindowInsetsListener(this); } @Override @@ -127,4 +147,92 @@ public class AuthCredentialPasswordView extends AuthCredentialView mPasswordField.setText(""); } } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (mAuthCredentialInput == null || mAuthCredentialHeader == null || mSubtitleView == null + || mDescriptionView == null || mPasswordField == null || mErrorView == null) { + return; + } + + int inputLeftBound; + int inputTopBound; + int headerRightBound = right; + int headerTopBounds = top; + final int subTitleBottom = (mSubtitleView.getVisibility() == GONE) ? mTitleView.getBottom() + : mSubtitleView.getBottom(); + final int descBottom = (mDescriptionView.getVisibility() == GONE) ? subTitleBottom + : mDescriptionView.getBottom(); + if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { + inputTopBound = (bottom - mAuthCredentialInput.getHeight()) / 2; + inputLeftBound = (right - left) / 2; + headerRightBound = inputLeftBound; + headerTopBounds -= Math.min(mIconView.getBottom(), mBottomInset); + } else { + inputTopBound = + descBottom + (bottom - descBottom - mAuthCredentialInput.getHeight()) / 2; + inputLeftBound = (right - left - mAuthCredentialInput.getWidth()) / 2; + } + + if (mDescriptionView.getBottom() > mBottomInset) { + mAuthCredentialHeader.layout(left, headerTopBounds, headerRightBound, bottom); + } + mAuthCredentialInput.layout(inputLeftBound, inputTopBound, right, bottom); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final int newWidth = MeasureSpec.getSize(widthMeasureSpec); + final int newHeight = MeasureSpec.getSize(heightMeasureSpec) - mBottomInset; + + setMeasuredDimension(newWidth, newHeight); + + final int halfWidthSpec = MeasureSpec.makeMeasureSpec(getWidth() / 2, + MeasureSpec.AT_MOST); + final int fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED); + if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { + measureChildren(halfWidthSpec, fullHeightSpec); + } else { + measureChildren(widthMeasureSpec, fullHeightSpec); + } + } + + @NonNull + @Override + public WindowInsets onApplyWindowInsets(@NonNull View v, WindowInsets insets) { + + final Insets bottomInset = insets.getInsets(ime()); + if (v instanceof AuthCredentialPasswordView && mBottomInset != bottomInset.bottom) { + mBottomInset = bottomInset.bottom; + if (mBottomInset > 0 + && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { + mTitleView.setSingleLine(true); + mTitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE); + mTitleView.setMarqueeRepeatLimit(-1); + // select to enable marquee unless a screen reader is enabled + mTitleView.setSelected(!mAccessibilityManager.isEnabled() + || !mAccessibilityManager.isTouchExplorationEnabled()); + } else { + mTitleView.setSingleLine(false); + mTitleView.setEllipsize(null); + // select to enable marquee unless a screen reader is enabled + mTitleView.setSelected(false); + } + requestLayout(); + } + return insets; + } + + @Override + public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { + pw.println(TAG + "State:"); + pw.println(" mBottomInset=" + mBottomInset); + pw.println(" mAuthCredentialHeader size=(" + mAuthCredentialHeader.getWidth() + "," + + mAuthCredentialHeader.getHeight()); + pw.println(" mAuthCredentialInput size=(" + mAuthCredentialInput.getWidth() + "," + + mAuthCredentialInput.getHeight()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java index 11498dbc0b83..f9e44a0c1724 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java @@ -93,7 +93,9 @@ public class AuthCredentialPatternView extends AuthCredentialView { @Override protected void onErrorTimeoutFinish() { super.onErrorTimeoutFinish(); - mLockPatternView.setEnabled(true); + // select to enable marquee unless a screen reader is enabled + mLockPatternView.setEnabled(!mAccessibilityManager.isEnabled() + || !mAccessibilityManager.isTouchExplorationEnabled()); } public AuthCredentialPatternView(Context context, AttributeSet attrs) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java index 4fa835e038ec..5958e6a436f1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java @@ -30,7 +30,6 @@ import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.pm.UserInfo; import android.graphics.drawable.Drawable; -import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.PromptInfo; import android.os.AsyncTask; import android.os.CountDownTimer; @@ -77,7 +76,7 @@ public abstract class AuthCredentialView extends LinearLayout { protected final Handler mHandler; protected final LockPatternUtils mLockPatternUtils; - private final AccessibilityManager mAccessibilityManager; + protected final AccessibilityManager mAccessibilityManager; private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; @@ -86,10 +85,10 @@ public abstract class AuthCredentialView extends LinearLayout { private boolean mShouldAnimatePanel; private boolean mShouldAnimateContents; - private TextView mTitleView; - private TextView mSubtitleView; - private TextView mDescriptionView; - private ImageView mIconView; + protected TextView mTitleView; + protected TextView mSubtitleView; + protected TextView mDescriptionView; + protected ImageView mIconView; protected TextView mErrorView; protected @Utils.CredentialType int mCredentialType; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index a7648bffe503..b49d4523a765 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -123,7 +123,6 @@ public class UdfpsController implements DozeReceiver { @NonNull private final PowerManager mPowerManager; @NonNull private final AccessibilityManager mAccessibilityManager; @NonNull private final LockscreenShadeTransitionController mLockscreenShadeTransitionController; - @Nullable private final UdfpsDisplayModeProvider mUdfpsDisplayMode; @NonNull private final ConfigurationController mConfigurationController; @NonNull private final SystemClock mSystemClock; @NonNull private final UnlockedScreenOffAnimationController @@ -139,6 +138,7 @@ public class UdfpsController implements DozeReceiver { // TODO(b/229290039): UDFPS controller should manage its dimensions on its own. Remove this. @Nullable private Runnable mAuthControllerUpdateUdfpsLocation; @Nullable private final AlternateUdfpsTouchProvider mAlternateTouchProvider; + @Nullable private UdfpsDisplayMode mUdfpsDisplayMode; // Tracks the velocity of a touch to help filter out the touches that move too fast. @Nullable private VelocityTracker mVelocityTracker; @@ -319,6 +319,10 @@ public class UdfpsController implements DozeReceiver { mAuthControllerUpdateUdfpsLocation = r; } + public void setUdfpsDisplayMode(UdfpsDisplayMode udfpsDisplayMode) { + mUdfpsDisplayMode = udfpsDisplayMode; + } + /** * Calculate the pointer speed given a velocity tracker and the pointer id. * This assumes that the velocity tracker has already been passed all relevant motion events. @@ -594,7 +598,6 @@ public class UdfpsController implements DozeReceiver { @NonNull VibratorHelper vibrator, @NonNull UdfpsHapticsSimulator udfpsHapticsSimulator, @NonNull UdfpsShell udfpsShell, - @NonNull Optional<UdfpsDisplayModeProvider> udfpsDisplayMode, @NonNull KeyguardStateController keyguardStateController, @NonNull DisplayManager displayManager, @Main Handler mainHandler, @@ -626,7 +629,6 @@ public class UdfpsController implements DozeReceiver { mPowerManager = powerManager; mAccessibilityManager = accessibilityManager; mLockscreenShadeTransitionController = lockscreenShadeTransitionController; - mUdfpsDisplayMode = udfpsDisplayMode.orElse(null); screenLifecycle.addObserver(mScreenObserver); mScreenOn = screenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_ON; mConfigurationController = configurationController; @@ -786,7 +788,7 @@ public class UdfpsController implements DozeReceiver { // ACTION_UP/ACTION_CANCEL, we need to be careful about not letting the screen // accidentally remain in high brightness mode. As a mitigation, queue a call to // cancel the fingerprint scan. - mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::onCancelUdfps, + mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::cancelAodInterrupt, AOD_INTERRUPT_TIMEOUT_MILLIS); // using a hard-coded value for major and minor until it is available from the sensor onFingerDown(requestId, screenX, screenY, minor, major); @@ -813,26 +815,22 @@ public class UdfpsController implements DozeReceiver { } /** - * Cancel UDFPS affordances - ability to hide the UDFPS overlay before the user explicitly - * lifts their finger. Generally, this should be called on errors in the authentication flow. - * - * The sensor that triggers an AOD fingerprint interrupt (see onAodInterrupt) doesn't give - * ACTION_UP/ACTION_CANCEL events, so and AOD interrupt scan needs to be cancelled manually. + * The sensor that triggers {@link #onAodInterrupt} doesn't emit ACTION_UP or ACTION_CANCEL + * events, which means the fingerprint gesture created by the AOD interrupt needs to be + * cancelled manually. * This should be called when authentication either succeeds or fails. Failing to cancel the * scan will leave the display in the UDFPS mode until the user lifts their finger. On optical * sensors, this can result in illumination persisting for longer than necessary. */ - void onCancelUdfps() { + @VisibleForTesting + void cancelAodInterrupt() { if (!mIsAodInterruptActive) { return; } if (mOverlay != null && mOverlay.getOverlayView() != null) { onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView()); } - if (mCancelAodTimeoutAction != null) { - mCancelAodTimeoutAction.run(); - mCancelAodTimeoutAction = null; - } + mCancelAodTimeoutAction = null; mIsAodInterruptActive = false; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 66a521c30f47..7d0109686351 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -21,13 +21,18 @@ import android.annotation.UiThread import android.content.Context import android.graphics.PixelFormat import android.graphics.Rect -import android.hardware.biometrics.BiometricOverlayConstants +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR import android.hardware.biometrics.BiometricOverlayConstants.ShowReason import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.IUdfpsOverlayControllerCallback +import android.os.Build import android.os.RemoteException +import android.provider.Settings import android.util.Log import android.util.RotationUtils import android.view.LayoutInflater @@ -38,6 +43,7 @@ import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener import androidx.annotation.LayoutRes +import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator @@ -54,13 +60,16 @@ import com.android.systemui.util.time.SystemClock private const val TAG = "UdfpsControllerOverlay" +@VisibleForTesting +const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui" + /** * Keeps track of the overlay state and UI resources associated with a single FingerprintService * request. This state can persist across configuration changes via the [show] and [hide] * methods. */ @UiThread -class UdfpsControllerOverlay( +class UdfpsControllerOverlay @JvmOverloads constructor( private val context: Context, fingerprintManager: FingerprintManager, private val inflater: LayoutInflater, @@ -82,7 +91,8 @@ class UdfpsControllerOverlay( @ShowReason val requestReason: Int, private val controllerCallback: IUdfpsOverlayControllerCallback, private val onTouch: (View, MotionEvent, Boolean) -> Boolean, - private val activityLaunchAnimator: ActivityLaunchAnimator + private val activityLaunchAnimator: ActivityLaunchAnimator, + private val isDebuggable: Boolean = Build.IS_DEBUGGABLE ) { /** The view, when [isShowing], or null. */ var overlayView: UdfpsView? = null @@ -102,18 +112,19 @@ class UdfpsControllerOverlay( gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or - WindowManager.LayoutParams.FLAG_SPLIT_TOUCH) + WindowManager.LayoutParams.FLAG_SPLIT_TOUCH) privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY // Avoid announcing window title. accessibilityTitle = " " } /** A helper if the [requestReason] was due to enrollment. */ - val enrollHelper: UdfpsEnrollHelper? = if (requestReason.isEnrollmentReason()) { - UdfpsEnrollHelper(context, fingerprintManager, requestReason) - } else { - null - } + val enrollHelper: UdfpsEnrollHelper? = + if (requestReason.isEnrollmentReason() && !shouldRemoveEnrollmentUi()) { + UdfpsEnrollHelper(context, fingerprintManager, requestReason) + } else { + null + } /** If the overlay is currently showing. */ val isShowing: Boolean @@ -129,6 +140,17 @@ class UdfpsControllerOverlay( private var touchExplorationEnabled = false + private fun shouldRemoveEnrollmentUi(): Boolean { + if (isDebuggable) { + return Settings.Global.getInt( + context.contentResolver, + SETTING_REMOVE_ENROLLMENT_UI, + 0 /* def */ + ) != 0 + } + return false + } + /** Show the overlay or return false and do nothing if it is already showing. */ @SuppressLint("ClickableViewAccessibility") fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean { @@ -183,7 +205,18 @@ class UdfpsControllerOverlay( view: UdfpsView, controller: UdfpsController ): UdfpsAnimationViewController<*>? { - return when (requestReason) { + val isEnrollment = when (requestReason) { + REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true + else -> false + } + + val filteredRequestReason = if (isEnrollment && shouldRemoveEnrollmentUi()) { + REASON_AUTH_OTHER + } else { + requestReason + } + + return when (filteredRequestReason) { REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> { UdfpsEnrollViewController( @@ -198,7 +231,7 @@ class UdfpsControllerOverlay( overlayParams.scaleFactor ) } - BiometricOverlayConstants.REASON_AUTH_KEYGUARD -> { + REASON_AUTH_KEYGUARD -> { UdfpsKeyguardViewController( view.addUdfpsView(R.layout.udfps_keyguard_view), statusBarStateController, @@ -216,7 +249,7 @@ class UdfpsControllerOverlay( activityLaunchAnimator ) } - BiometricOverlayConstants.REASON_AUTH_BP -> { + REASON_AUTH_BP -> { // note: empty controller, currently shows no visual affordance UdfpsBpViewController( view.addUdfpsView(R.layout.udfps_bp_view), @@ -226,8 +259,8 @@ class UdfpsControllerOverlay( dumpManager ) } - BiometricOverlayConstants.REASON_AUTH_OTHER, - BiometricOverlayConstants.REASON_AUTH_SETTINGS -> { + REASON_AUTH_OTHER, + REASON_AUTH_SETTINGS -> { UdfpsFpmOtherViewController( view.addUdfpsView(R.layout.udfps_fpm_other_view), statusBarStateController, @@ -440,4 +473,4 @@ private fun Int.isEnrollmentReason() = private fun Int.isImportantForAccessibility() = this == REASON_ENROLL_FIND_SENSOR || this == REASON_ENROLL_ENROLLING || - this == BiometricOverlayConstants.REASON_AUTH_BP + this == REASON_AUTH_BP diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt new file mode 100644 index 000000000000..e9de7ccc7b4f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt @@ -0,0 +1,88 @@ +package com.android.systemui.biometrics + +import android.content.Context +import android.os.RemoteException +import android.os.Trace +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.util.concurrency.Execution +import javax.inject.Inject + +private const val TAG = "UdfpsDisplayMode" + +/** + * UdfpsDisplayMode that encapsulates pixel-specific code, such as enabling the high-brightness mode + * (HBM) in a display-specific way and freezing the display's refresh rate. + */ +@SysUISingleton +class UdfpsDisplayMode +@Inject +constructor( + private val context: Context, + private val execution: Execution, + private val authController: AuthController +) : UdfpsDisplayModeProvider { + + // The request is reset to null after it's processed. + private var currentRequest: Request? = null + + override fun enable(onEnabled: Runnable?) { + execution.isMainThread() + Log.v(TAG, "enable") + + if (currentRequest != null) { + Log.e(TAG, "enable | already requested") + return + } + if (authController.udfpsHbmListener == null) { + Log.e(TAG, "enable | mDisplayManagerCallback is null") + return + } + + Trace.beginSection("UdfpsDisplayMode.enable") + + // Track this request in one object. + val request = Request(context.displayId) + currentRequest = request + + try { + // This method is a misnomer. It has nothing to do with HBM, its purpose is to set + // the appropriate display refresh rate. + authController.udfpsHbmListener!!.onHbmEnabled(request.displayId) + Log.v(TAG, "enable | requested optimal refresh rate for UDFPS") + } catch (e: RemoteException) { + Log.e(TAG, "enable", e) + } + + onEnabled?.run() ?: Log.w(TAG, "enable | onEnabled is null") + Trace.endSection() + } + + override fun disable(onDisabled: Runnable?) { + execution.isMainThread() + Log.v(TAG, "disable") + + val request = currentRequest + if (request == null) { + Log.w(TAG, "disable | already disabled") + return + } + + Trace.beginSection("UdfpsDisplayMode.disable") + + try { + // Allow DisplayManager to unset the UDFPS refresh rate. + authController.udfpsHbmListener!!.onHbmDisabled(request.displayId) + Log.v(TAG, "disable | removed the UDFPS refresh rate request") + } catch (e: RemoteException) { + Log.e(TAG, "disable", e) + } + + currentRequest = null + onDisabled?.run() ?: Log.w(TAG, "disable | onDisabled is null") + Trace.endSection() + } +} + +/** Tracks a request to enable the UDFPS mode. */ +private data class Request(val displayId: Int) diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt index 96af42bfda22..d99625a9fbf2 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt @@ -17,9 +17,9 @@ package com.android.systemui.bluetooth import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.BluetoothLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** Helper class for logging bluetooth events. */ diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java index 9b7d49883222..8e062bd69d63 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java @@ -17,15 +17,11 @@ package com.android.systemui.bluetooth; import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import android.os.Bundle; -import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.Window; -import android.view.WindowManager; import android.widget.Button; import android.widget.TextView; @@ -33,7 +29,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; -import com.android.systemui.media.MediaDataUtils; +import com.android.systemui.media.controls.util.MediaDataUtils; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.statusbar.phone.SystemUIDialog; diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt index 5b3a982ab5e2..d27708fc04d7 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt @@ -20,11 +20,11 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogMessage +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogMessage import com.android.systemui.log.dagger.BroadcastDispatcherLog import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java index 3871248eccd5..858bac30880b 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java @@ -44,9 +44,6 @@ public interface FalsingCollector { void onQsDown(); /** */ - void setQsExpanded(boolean expanded); - - /** */ boolean shouldEnforceBouncer(); /** */ diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java index 28aac051c66d..0b7d6ab5acf7 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java @@ -49,10 +49,6 @@ public class FalsingCollectorFake implements FalsingCollector { } @Override - public void setQsExpanded(boolean expanded) { - } - - @Override public boolean shouldEnforceBouncer() { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java index f5f9655ef24b..da3d293d543b 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java @@ -23,6 +23,8 @@ import android.hardware.biometrics.BiometricSourceType; import android.util.Log; import android.view.MotionEvent; +import androidx.annotation.VisibleForTesting; + import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.dagger.SysUISingleton; @@ -30,6 +32,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; @@ -133,6 +136,7 @@ class FalsingCollectorImpl implements FalsingCollector { ProximitySensor proximitySensor, StatusBarStateController statusBarStateController, KeyguardStateController keyguardStateController, + ShadeExpansionStateManager shadeExpansionStateManager, BatteryController batteryController, DockManager dockManager, @Main DelayableExecutor mainExecutor, @@ -157,6 +161,8 @@ class FalsingCollectorImpl implements FalsingCollector { mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback); + shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged); + mBatteryController.addCallback(mBatteryListener); mDockManager.addListener(mDockEventListener); } @@ -193,8 +199,8 @@ class FalsingCollectorImpl implements FalsingCollector { public void onQsDown() { } - @Override - public void setQsExpanded(boolean expanded) { + @VisibleForTesting + void onQsExpansionChanged(Boolean expanded) { if (expanded) { unregisterSensors(); } else if (mSessionStarted) { diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java index 05e3f1ce87a6..82e570438dab 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java @@ -31,9 +31,12 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.systemui.CoreStartable; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.util.DeviceConfigProxy; import javax.inject.Inject; +import javax.inject.Provider; /** * ClipboardListener brings up a clipboard overlay when something is copied to the clipboard. @@ -51,20 +54,30 @@ public class ClipboardListener implements private final Context mContext; private final DeviceConfigProxy mDeviceConfig; - private final ClipboardOverlayControllerFactory mOverlayFactory; + private final Provider<ClipboardOverlayController> mOverlayProvider; + private final ClipboardOverlayControllerLegacyFactory mOverlayFactory; private final ClipboardManager mClipboardManager; private final UiEventLogger mUiEventLogger; - private ClipboardOverlayController mClipboardOverlayController; + private final FeatureFlags mFeatureFlags; + private boolean mUsingNewOverlay; + private ClipboardOverlay mClipboardOverlay; @Inject public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy, - ClipboardOverlayControllerFactory overlayFactory, ClipboardManager clipboardManager, - UiEventLogger uiEventLogger) { + Provider<ClipboardOverlayController> clipboardOverlayControllerProvider, + ClipboardOverlayControllerLegacyFactory overlayFactory, + ClipboardManager clipboardManager, + UiEventLogger uiEventLogger, + FeatureFlags featureFlags) { mContext = context; mDeviceConfig = deviceConfigProxy; + mOverlayProvider = clipboardOverlayControllerProvider; mOverlayFactory = overlayFactory; mClipboardManager = clipboardManager; mUiEventLogger = uiEventLogger; + mFeatureFlags = featureFlags; + + mUsingNewOverlay = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR); } @Override @@ -89,16 +102,22 @@ public class ClipboardListener implements return; } - if (mClipboardOverlayController == null) { - mClipboardOverlayController = mOverlayFactory.create(mContext); + boolean enabled = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR); + if (mClipboardOverlay == null || enabled != mUsingNewOverlay) { + mUsingNewOverlay = enabled; + if (enabled) { + mClipboardOverlay = mOverlayProvider.get(); + } else { + mClipboardOverlay = mOverlayFactory.create(mContext); + } mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource); } else { mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource); } - mClipboardOverlayController.setClipData(clipData, clipSource); - mClipboardOverlayController.setOnSessionCompleteListener(() -> { + mClipboardOverlay.setClipData(clipData, clipSource); + mClipboardOverlay.setOnSessionCompleteListener(() -> { // Session is complete, free memory until it's needed again. - mClipboardOverlayController = null; + mClipboardOverlay = null; }); } @@ -120,4 +139,10 @@ public class ClipboardListener implements private static boolean isEmulator() { return SystemProperties.getBoolean("ro.boot.qemu", false); } + + interface ClipboardOverlay { + void setClipData(ClipData clipData, String clipSource); + + void setOnSessionCompleteListener(Runnable runnable); + } } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java index 7e499ebdf691..9f338d1c6e1d 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java @@ -17,7 +17,6 @@ package com.android.systemui.clipboardoverlay; import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS; -import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; @@ -37,11 +36,6 @@ import static java.util.Objects.requireNonNull; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.annotation.MainThread; -import android.app.ICompatCameraControlCallback; import android.app.RemoteAction; import android.content.BroadcastReceiver; import android.content.ClipData; @@ -52,14 +46,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Insets; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.Region; -import android.graphics.drawable.Icon; import android.hardware.display.DisplayManager; import android.hardware.input.InputManager; import android.net.Uri; @@ -67,57 +54,37 @@ import android.os.AsyncTask; import android.os.Looper; import android.provider.DeviceConfig; import android.text.TextUtils; -import android.util.DisplayMetrics; import android.util.Log; -import android.util.MathUtils; import android.util.Size; -import android.util.TypedValue; import android.view.Display; -import android.view.DisplayCutout; -import android.view.Gravity; import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.InputMonitor; -import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.View; -import android.view.ViewRootImpl; -import android.view.ViewTreeObserver; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityManager; -import android.view.animation.LinearInterpolator; -import android.view.animation.PathInterpolator; import android.view.textclassifier.TextClassification; import android.view.textclassifier.TextClassificationManager; import android.view.textclassifier.TextClassifier; import android.view.textclassifier.TextLinks; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.core.view.ViewCompat; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.android.internal.logging.UiEventLogger; -import com.android.internal.policy.PhoneWindow; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastSender; -import com.android.systemui.screenshot.DraggableConstraintLayout; -import com.android.systemui.screenshot.FloatingWindowUtil; -import com.android.systemui.screenshot.OverlayActionChip; +import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext; import com.android.systemui.screenshot.TimeoutHandler; import java.io.IOException; import java.util.ArrayList; +import java.util.Optional; + +import javax.inject.Inject; /** * Controls state and UI for the overlay that appears when something is added to the clipboard */ -public class ClipboardOverlayController { +public class ClipboardOverlayController implements ClipboardListener.ClipboardOverlay { private static final String TAG = "ClipboardOverlayCtrlr"; /** Constants for screenshot/copy deconflicting */ @@ -126,36 +93,22 @@ public class ClipboardOverlayController { public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY"; private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000; - private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe - private static final int FONT_SEARCH_STEP_PX = 4; private final Context mContext; private final ClipboardLogger mClipboardLogger; private final BroadcastDispatcher mBroadcastDispatcher; private final DisplayManager mDisplayManager; - private final DisplayMetrics mDisplayMetrics; - private final WindowManager mWindowManager; - private final WindowManager.LayoutParams mWindowLayoutParams; - private final PhoneWindow mWindow; + private final ClipboardOverlayWindow mWindow; private final TimeoutHandler mTimeoutHandler; - private final AccessibilityManager mAccessibilityManager; private final TextClassifier mTextClassifier; - private final DraggableConstraintLayout mView; - private final View mClipboardPreview; - private final ImageView mImagePreview; - private final TextView mTextPreview; - private final TextView mHiddenPreview; - private final View mPreviewBorder; - private final OverlayActionChip mEditChip; - private final OverlayActionChip mShareChip; - private final OverlayActionChip mRemoteCopyChip; - private final View mActionContainerBackground; - private final View mDismissButton; - private final LinearLayout mActionContainer; - private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>(); + private final ClipboardOverlayView mView; private Runnable mOnSessionCompleteListener; + private Runnable mOnRemoteCopyTapped; + private Runnable mOnShareTapped; + private Runnable mOnEditTapped; + private Runnable mOnPreviewTapped; private InputMonitor mInputMonitor; private InputEventReceiver mInputEventReceiver; @@ -163,14 +116,66 @@ public class ClipboardOverlayController { private BroadcastReceiver mCloseDialogsReceiver; private BroadcastReceiver mScreenshotReceiver; - private boolean mBlockAttach = false; private Animator mExitAnimator; private Animator mEnterAnimator; - private final int mOrientation; - private boolean mKeyboardVisible; + private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks = + new ClipboardOverlayView.ClipboardOverlayCallbacks() { + @Override + public void onInteraction() { + mTimeoutHandler.resetTimeout(); + } + + @Override + public void onSwipeDismissInitiated(Animator animator) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED); + mExitAnimator = animator; + } + + @Override + public void onDismissComplete() { + hideImmediate(); + } + + @Override + public void onPreviewTapped() { + if (mOnPreviewTapped != null) { + mOnPreviewTapped.run(); + } + } + + @Override + public void onShareButtonTapped() { + if (mOnShareTapped != null) { + mOnShareTapped.run(); + } + } + + @Override + public void onEditButtonTapped() { + if (mOnEditTapped != null) { + mOnEditTapped.run(); + } + } + + @Override + public void onRemoteCopyButtonTapped() { + if (mOnRemoteCopyTapped != null) { + mOnRemoteCopyTapped.run(); + } + } + + @Override + public void onDismissButtonTapped() { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED); + animateOut(); + } + }; - public ClipboardOverlayController(Context context, + @Inject + public ClipboardOverlayController(@OverlayWindowContext Context context, + ClipboardOverlayView clipboardOverlayView, + ClipboardOverlayWindow clipboardOverlayWindow, BroadcastDispatcher broadcastDispatcher, BroadcastSender broadcastSender, TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) { @@ -181,121 +186,26 @@ public class ClipboardOverlayController { mClipboardLogger = new ClipboardLogger(uiEventLogger); - mAccessibilityManager = AccessibilityManager.getInstance(mContext); + mView = clipboardOverlayView; + mWindow = clipboardOverlayWindow; + mWindow.init(mView::setInsets, () -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER); + hideImmediate(); + }); + mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class)) .getTextClassifier(); - mWindowManager = mContext.getSystemService(WindowManager.class); - - mDisplayMetrics = new DisplayMetrics(); - mContext.getDisplay().getRealMetrics(mDisplayMetrics); - mTimeoutHandler = timeoutHandler; mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS); - // Setup the window that we are going to use - mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); - mWindowLayoutParams.setTitle("ClipboardOverlay"); - - mWindow = FloatingWindowUtil.getFloatingWindow(mContext); - mWindow.setWindowManager(mWindowManager, null, null); - - setWindowFocusable(false); - - mView = (DraggableConstraintLayout) - LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay, null); - mActionContainerBackground = - requireNonNull(mView.findViewById(R.id.actions_container_background)); - mActionContainer = requireNonNull(mView.findViewById(R.id.actions)); - mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview)); - mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview)); - mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview)); - mHiddenPreview = requireNonNull(mView.findViewById(R.id.hidden_preview)); - mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border)); - mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip)); - mShareChip = requireNonNull(mView.findViewById(R.id.share_chip)); - mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip)); - mEditChip.setAlpha(1); - mShareChip.setAlpha(1); - mRemoteCopyChip.setAlpha(1); - mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button)); - - mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share)); - mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() { - @Override - public void onInteraction() { - mTimeoutHandler.resetTimeout(); - } - - @Override - public void onSwipeDismissInitiated(Animator animator) { - mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED); - mExitAnimator = animator; - } + mView.setCallbacks(mClipboardCallbacks); - @Override - public void onDismissComplete() { - hideImmediate(); - } - }); - mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> { - int availableHeight = mTextPreview.getHeight() - - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom()); - mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight()); - return true; - }); - - mDismissButton.setOnClickListener(view -> { - mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED); - animateOut(); - }); - - mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true); - mRemoteCopyChip.setIcon( - Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true); - mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); - mOrientation = mContext.getResources().getConfiguration().orientation; - - attachWindow(); - withWindowAttached(() -> { + mWindow.withWindowAttached(() -> { mWindow.setContentView(mView); - WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); - mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime()); - updateInsets(insets); - mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - WindowInsets insets = - mWindowManager.getCurrentWindowMetrics().getWindowInsets(); - boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime()); - if (keyboardVisible != mKeyboardVisible) { - mKeyboardVisible = keyboardVisible; - updateInsets(insets); - } - } - }); - mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( - new ViewRootImpl.ActivityConfigCallback() { - @Override - public void onConfigurationChanged(Configuration overrideConfig, - int newDisplayId) { - if (mContext.getResources().getConfiguration().orientation - != mOrientation) { - mClipboardLogger.logSessionComplete( - CLIPBOARD_OVERLAY_DISMISSED_OTHER); - hideImmediate(); - } - } - - @Override - public void requestCompatCameraControl( - boolean showControl, boolean transformationApplied, - ICompatCameraControlCallback callback) { - Log.w(TAG, "unexpected requestCompatCameraControl call"); - } - }); + mView.setInsets(mWindow.getWindowInsets(), + mContext.getResources().getConfiguration().orientation); }); mTimeoutHandler.setOnTimeoutRunnable(() -> { @@ -336,21 +246,19 @@ public class ClipboardOverlayController { broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION); } - void setClipData(ClipData clipData, String clipSource) { + @Override // ClipboardListener.ClipboardOverlay + public void setClipData(ClipData clipData, String clipSource) { if (mExitAnimator != null && mExitAnimator.isRunning()) { mExitAnimator.cancel(); } reset(); - String accessibilityAnnouncement; + String accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null && clipData.getDescription().getExtras() .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE); if (clipData == null || clipData.getItemCount() == 0) { - showTextPreview( - mContext.getResources().getString(R.string.clipboard_overlay_text_copied), - mTextPreview); - accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + mView.showDefaultTextPreview(); } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) { ClipData.Item item = clipData.getItemAt(0); if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, @@ -360,53 +268,47 @@ public class ClipboardOverlayController { } } if (isSensitive) { - showEditableText( - mContext.getResources().getString(R.string.clipboard_asterisks), true); + showEditableText(mContext.getString(R.string.clipboard_asterisks), true); } else { showEditableText(item.getText(), false); } - showShareChip(clipData); + mOnShareTapped = () -> shareContent(clipData); + mView.showShareChip(); accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied); } else if (clipData.getItemAt(0).getUri() != null) { if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) { - showShareChip(clipData); + mOnShareTapped = () -> shareContent(clipData); + mView.showShareChip(); accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied); - } else { - accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); } } else { - showTextPreview( - mContext.getResources().getString(R.string.clipboard_overlay_text_copied), - mTextPreview); - accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + mView.showDefaultTextPreview(); } + maybeShowRemoteCopy(clipData); + animateIn(); + mView.announceForAccessibility(accessibilityAnnouncement); + mTimeoutHandler.resetTimeout(); + } + + private void maybeShowRemoteCopy(ClipData clipData) { Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext); // Only show remote copy if it's available. PackageManager packageManager = mContext.getPackageManager(); if (packageManager.resolveActivity( remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) { - mRemoteCopyChip.setContentDescription( - mContext.getString(R.string.clipboard_send_nearby_description)); - mRemoteCopyChip.setVisibility(View.VISIBLE); - mRemoteCopyChip.setOnClickListener((v) -> { + mView.setRemoteCopyVisibility(true); + mOnRemoteCopyTapped = () -> { mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED); mContext.startActivity(remoteCopyIntent); animateOut(); - }); - mActionContainerBackground.setVisibility(View.VISIBLE); + }; } else { - mRemoteCopyChip.setVisibility(View.GONE); + mView.setRemoteCopyVisibility(false); } - withWindowAttached(() -> { - if (mEnterAnimator == null || !mEnterAnimator.isRunning()) { - mView.post(this::animateIn); - } - mView.announceForAccessibility(accessibilityAnnouncement); - }); - mTimeoutHandler.resetTimeout(); } - void setOnSessionCompleteListener(Runnable runnable) { + @Override // ClipboardListener.ClipboardOverlay + public void setOnSessionCompleteListener(Runnable runnable) { mOnSessionCompleteListener = runnable; } @@ -418,72 +320,29 @@ public class ClipboardOverlayController { actions.addAll(classification.getActions()); } mView.post(() -> { - resetActionChips(); - if (actions.size() > 0) { - mActionContainerBackground.setVisibility(View.VISIBLE); - for (RemoteAction action : actions) { - Intent targetIntent = action.getActionIntent().getIntent(); - ComponentName component = targetIntent.getComponent(); - if (component != null && !TextUtils.equals(source, - component.getPackageName())) { - OverlayActionChip chip = constructActionChip(action); - mActionContainer.addView(chip); - mActionChips.add(chip); - break; // only show at most one action chip - } - } - } - }); - } - - private void showShareChip(ClipData clip) { - mShareChip.setVisibility(View.VISIBLE); - mActionContainerBackground.setVisibility(View.VISIBLE); - mShareChip.setOnClickListener((v) -> shareContent(clip)); - } - - private OverlayActionChip constructActionChip(RemoteAction action) { - OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate( - R.layout.overlay_action_chip, mActionContainer, false); - chip.setText(action.getTitle()); - chip.setContentDescription(action.getTitle()); - chip.setIcon(action.getIcon(), false); - chip.setPendingIntent(action.getActionIntent(), () -> { - mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED); - animateOut(); + Optional<RemoteAction> action = actions.stream().filter(remoteAction -> { + ComponentName component = remoteAction.getActionIntent().getIntent().getComponent(); + return component != null && !TextUtils.equals(source, component.getPackageName()); + }).findFirst(); + mView.resetActionChips(); + action.ifPresent(remoteAction -> mView.setActionChip(remoteAction, () -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED); + animateOut(); + })); }); - chip.setAlpha(1); - return chip; } private void monitorOutsideTouches() { InputManager inputManager = mContext.getSystemService(InputManager.class); mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0); - mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(), - Looper.getMainLooper()) { + mInputEventReceiver = new InputEventReceiver( + mInputMonitor.getInputChannel(), Looper.getMainLooper()) { @Override public void onInputEvent(InputEvent event) { if (event instanceof MotionEvent) { MotionEvent motionEvent = (MotionEvent) event; if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - Region touchRegion = new Region(); - - final Rect tmpRect = new Rect(); - mPreviewBorder.getBoundsOnScreen(tmpRect); - tmpRect.inset( - (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), - (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, - -SWIPE_PADDING_DP)); - touchRegion.op(tmpRect, Region.Op.UNION); - mActionContainerBackground.getBoundsOnScreen(tmpRect); - tmpRect.inset( - (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), - (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, - -SWIPE_PADDING_DP)); - touchRegion.op(tmpRect, Region.Op.UNION); - mDismissButton.getBoundsOnScreen(tmpRect); - touchRegion.op(tmpRect, Region.Op.UNION); - if (!touchRegion.contains( + if (!mView.isInTouchRegion( (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) { mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE); animateOut(); @@ -513,95 +372,27 @@ public class ClipboardOverlayController { animateOut(); } - private void showSinglePreview(View v) { - mTextPreview.setVisibility(View.GONE); - mImagePreview.setVisibility(View.GONE); - mHiddenPreview.setVisibility(View.GONE); - v.setVisibility(View.VISIBLE); - } - - private void showTextPreview(CharSequence text, TextView textView) { - showSinglePreview(textView); - final CharSequence truncatedText = text.subSequence(0, Math.min(500, text.length())); - textView.setText(truncatedText); - updateTextSize(truncatedText, textView); - - textView.addOnLayoutChangeListener( - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - if (right - left != oldRight - oldLeft) { - updateTextSize(truncatedText, textView); - } - }); - mEditChip.setVisibility(View.GONE); - } - - private void updateTextSize(CharSequence text, TextView textView) { - Paint paint = new Paint(textView.getPaint()); - Resources res = textView.getResources(); - float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font); - float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font); - if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) { - // If the text is a single word and would fit within the TextView at the min font size, - // find the biggest font size that will fit. - float fontSizePx = minFontSize; - while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize - && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) { - fontSizePx += FONT_SEARCH_STEP_PX; - } - // Need to turn off autosizing, otherwise setTextSize is a no-op. - textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE); - // It's possible to hit the max font size and not fill the width, so centering - // horizontally looks better in this case. - textView.setGravity(Gravity.CENTER); - textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx); - } else { - // Otherwise just stick with autosize. - textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize, - (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX); - textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); - } - } - - private static boolean fitsInView(CharSequence text, TextView textView, Paint paint, - float fontSizePx) { - paint.setTextSize(fontSizePx); - float size = paint.measureText(text.toString()); - float availableWidth = textView.getWidth() - textView.getPaddingLeft() - - textView.getPaddingRight(); - return size < availableWidth; - } - - private static boolean isOneWord(CharSequence text) { - return text.toString().split("\\s+", 2).length == 1; - } - private void showEditableText(CharSequence text, boolean hidden) { - TextView textView = hidden ? mHiddenPreview : mTextPreview; - showTextPreview(text, textView); - View.OnClickListener listener = v -> editText(); - setAccessibilityActionToEdit(textView); + mView.showTextPreview(text, hidden); + mView.setEditAccessibilityAction(true); + mOnPreviewTapped = this::editText; if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) { - mEditChip.setVisibility(View.VISIBLE); - mActionContainerBackground.setVisibility(View.VISIBLE); - mEditChip.setContentDescription( - mContext.getString(R.string.clipboard_edit_text_description)); - mEditChip.setOnClickListener(listener); + mOnEditTapped = this::editText; + mView.showEditChip(mContext.getString(R.string.clipboard_edit_text_description)); } - textView.setOnClickListener(listener); } private boolean tryShowEditableImage(Uri uri, boolean isSensitive) { - View.OnClickListener listener = v -> editImage(uri); + Runnable listener = () -> editImage(uri); ContentResolver resolver = mContext.getContentResolver(); String mimeType = resolver.getType(uri); boolean isEditableImage = mimeType != null && mimeType.startsWith("image"); if (isSensitive) { - mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden)); - showSinglePreview(mHiddenPreview); + mView.showImagePreview(null); if (isEditableImage) { - mHiddenPreview.setOnClickListener(listener); - setAccessibilityActionToEdit(mHiddenPreview); + mOnPreviewTapped = listener; + mView.setEditAccessibilityAction(true); } } else if (isEditableImage) { // if the MIMEtype is image, try to load try { @@ -609,44 +400,36 @@ public class ClipboardOverlayController { // The width of the view is capped, height maintains aspect ratio, so allow it to be // taller if needed. Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null); - showSinglePreview(mImagePreview); - mImagePreview.setImageBitmap(thumbnail); - mImagePreview.setOnClickListener(listener); - setAccessibilityActionToEdit(mImagePreview); + mView.showImagePreview(thumbnail); + mView.setEditAccessibilityAction(true); + mOnPreviewTapped = listener; } catch (IOException e) { Log.e(TAG, "Thumbnail loading failed", e); - showTextPreview( - mContext.getResources().getString(R.string.clipboard_overlay_text_copied), - mTextPreview); + mView.showDefaultTextPreview(); isEditableImage = false; } } else { - showTextPreview( - mContext.getResources().getString(R.string.clipboard_overlay_text_copied), - mTextPreview); + mView.showDefaultTextPreview(); } if (isEditableImage && DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) { - mEditChip.setVisibility(View.VISIBLE); - mActionContainerBackground.setVisibility(View.VISIBLE); - mEditChip.setOnClickListener(listener); - mEditChip.setContentDescription( - mContext.getString(R.string.clipboard_edit_image_description)); + mView.showEditChip(mContext.getString(R.string.clipboard_edit_image_description)); } return isEditableImage; } - private void setAccessibilityActionToEdit(View view) { - ViewCompat.replaceAccessibilityAction(view, - AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, - mContext.getString(R.string.clipboard_edit), null); - } - private void animateIn() { - if (mAccessibilityManager.isEnabled()) { - mDismissButton.setVisibility(View.VISIBLE); + if (mEnterAnimator != null && mEnterAnimator.isRunning()) { + return; } - mEnterAnimator = getEnterAnimation(); + mEnterAnimator = mView.getEnterAnimation(); + mEnterAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mTimeoutHandler.resetTimeout(); + } + }); mEnterAnimator.start(); } @@ -654,7 +437,7 @@ public class ClipboardOverlayController { if (mExitAnimator != null && mExitAnimator.isRunning()) { return; } - Animator anim = getExitAnimation(); + Animator anim = mView.getExitAnimation(); anim.addListener(new AnimatorListenerAdapter() { private boolean mCancelled; @@ -676,122 +459,11 @@ public class ClipboardOverlayController { anim.start(); } - private Animator getEnterAnimation() { - TimeInterpolator linearInterpolator = new LinearInterpolator(); - TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f); - AnimatorSet enterAnim = new AnimatorSet(); - - ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); - rootAnim.setInterpolator(linearInterpolator); - rootAnim.setDuration(66); - rootAnim.addUpdateListener(animation -> { - mView.setAlpha(animation.getAnimatedFraction()); - }); - - ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); - scaleAnim.setInterpolator(scaleInterpolator); - scaleAnim.setDuration(333); - scaleAnim.addUpdateListener(animation -> { - float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); - mClipboardPreview.setScaleX(previewScale); - mClipboardPreview.setScaleY(previewScale); - mPreviewBorder.setScaleX(previewScale); - mPreviewBorder.setScaleY(previewScale); - - float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); - mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); - mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); - float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction()); - float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); - mActionContainer.setScaleX(actionsScaleX); - mActionContainer.setScaleY(actionsScaleY); - mActionContainerBackground.setScaleX(actionsScaleX); - mActionContainerBackground.setScaleY(actionsScaleY); - }); - - ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); - alphaAnim.setInterpolator(linearInterpolator); - alphaAnim.setDuration(283); - alphaAnim.addUpdateListener(animation -> { - float alpha = animation.getAnimatedFraction(); - mClipboardPreview.setAlpha(alpha); - mPreviewBorder.setAlpha(alpha); - mDismissButton.setAlpha(alpha); - mActionContainer.setAlpha(alpha); - }); - - mActionContainer.setAlpha(0); - mPreviewBorder.setAlpha(0); - mClipboardPreview.setAlpha(0); - enterAnim.play(rootAnim).with(scaleAnim); - enterAnim.play(alphaAnim).after(50).after(rootAnim); - - enterAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - mView.setAlpha(1); - mTimeoutHandler.resetTimeout(); - } - }); - return enterAnim; - } - - private Animator getExitAnimation() { - TimeInterpolator linearInterpolator = new LinearInterpolator(); - TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f); - AnimatorSet exitAnim = new AnimatorSet(); - - ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); - rootAnim.setInterpolator(linearInterpolator); - rootAnim.setDuration(100); - rootAnim.addUpdateListener(anim -> mView.setAlpha(1 - anim.getAnimatedFraction())); - - ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); - scaleAnim.setInterpolator(scaleInterpolator); - scaleAnim.setDuration(250); - scaleAnim.addUpdateListener(animation -> { - float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); - mClipboardPreview.setScaleX(previewScale); - mClipboardPreview.setScaleY(previewScale); - mPreviewBorder.setScaleX(previewScale); - mPreviewBorder.setScaleY(previewScale); - - float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); - mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); - mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); - float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction()); - float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); - mActionContainer.setScaleX(actionScaleX); - mActionContainer.setScaleY(actionScaleY); - mActionContainerBackground.setScaleX(actionScaleX); - mActionContainerBackground.setScaleY(actionScaleY); - }); - - ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); - alphaAnim.setInterpolator(linearInterpolator); - alphaAnim.setDuration(166); - alphaAnim.addUpdateListener(animation -> { - float alpha = 1 - animation.getAnimatedFraction(); - mClipboardPreview.setAlpha(alpha); - mPreviewBorder.setAlpha(alpha); - mDismissButton.setAlpha(alpha); - mActionContainer.setAlpha(alpha); - }); - - exitAnim.play(alphaAnim).with(scaleAnim); - exitAnim.play(rootAnim).after(150).after(alphaAnim); - return exitAnim; - } - - private void hideImmediate() { + void hideImmediate() { // Note this may be called multiple times if multiple dismissal events happen at the same // time. mTimeoutHandler.cancelTimeout(); - final View decorView = mWindow.peekDecorView(); - if (decorView != null && decorView.isAttachedToWindow()) { - mWindowManager.removeViewImmediate(decorView); - } + mWindow.remove(); if (mCloseDialogsReceiver != null) { mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver); mCloseDialogsReceiver = null; @@ -813,129 +485,20 @@ public class ClipboardOverlayController { } } - private void resetActionChips() { - for (OverlayActionChip chip : mActionChips) { - mActionContainer.removeView(chip); - } - mActionChips.clear(); - } - private void reset() { - mView.setTranslationX(0); - mView.setAlpha(0); - mActionContainerBackground.setVisibility(View.GONE); - mShareChip.setVisibility(View.GONE); - mEditChip.setVisibility(View.GONE); - mRemoteCopyChip.setVisibility(View.GONE); - resetActionChips(); + mOnRemoteCopyTapped = null; + mOnShareTapped = null; + mOnEditTapped = null; + mOnPreviewTapped = null; + mView.reset(); mTimeoutHandler.cancelTimeout(); mClipboardLogger.reset(); } - @MainThread - private void attachWindow() { - View decorView = mWindow.getDecorView(); - if (decorView.isAttachedToWindow() || mBlockAttach) { - return; - } - mBlockAttach = true; - mWindowManager.addView(decorView, mWindowLayoutParams); - decorView.requestApplyInsets(); - mView.requestApplyInsets(); - decorView.getViewTreeObserver().addOnWindowAttachListener( - new ViewTreeObserver.OnWindowAttachListener() { - @Override - public void onWindowAttached() { - mBlockAttach = false; - } - - @Override - public void onWindowDetached() { - } - } - ); - } - - private void withWindowAttached(Runnable action) { - View decorView = mWindow.getDecorView(); - if (decorView.isAttachedToWindow()) { - action.run(); - } else { - decorView.getViewTreeObserver().addOnWindowAttachListener( - new ViewTreeObserver.OnWindowAttachListener() { - @Override - public void onWindowAttached() { - mBlockAttach = false; - decorView.getViewTreeObserver().removeOnWindowAttachListener(this); - action.run(); - } - - @Override - public void onWindowDetached() { - } - }); - } - } - - private void updateInsets(WindowInsets insets) { - int orientation = mContext.getResources().getConfiguration().orientation; - FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams(); - if (p == null) { - return; - } - DisplayCutout cutout = insets.getDisplayCutout(); - Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); - Insets imeInsets = insets.getInsets(WindowInsets.Type.ime()); - if (cutout == null) { - p.setMargins(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom)); - } else { - Insets waterfall = cutout.getWaterfallInsets(); - if (orientation == ORIENTATION_PORTRAIT) { - p.setMargins( - waterfall.left, - Math.max(cutout.getSafeInsetTop(), waterfall.top), - waterfall.right, - Math.max(imeInsets.bottom, - Math.max(cutout.getSafeInsetBottom(), - Math.max(navBarInsets.bottom, waterfall.bottom)))); - } else { - p.setMargins( - waterfall.left, - waterfall.top, - waterfall.right, - Math.max(imeInsets.bottom, - Math.max(navBarInsets.bottom, waterfall.bottom))); - } - } - mView.setLayoutParams(p); - mView.requestLayout(); - } - private Display getDefaultDisplay() { return mDisplayManager.getDisplay(DEFAULT_DISPLAY); } - /** - * Updates the window focusability. If the window is already showing, then it updates the - * window immediately, otherwise the layout params will be applied when the window is next - * shown. - */ - private void setWindowFocusable(boolean focusable) { - int flags = mWindowLayoutParams.flags; - if (focusable) { - mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } else { - mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } - if (mWindowLayoutParams.flags == flags) { - return; - } - final View decorView = mWindow.peekDecorView(); - if (decorView != null && decorView.isAttachedToWindow()) { - mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); - } - } - static class ClipboardLogger { private final UiEventLogger mUiEventLogger; private boolean mGuarded = false; diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java new file mode 100644 index 000000000000..3a040829ba0c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java @@ -0,0 +1,963 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.clipboardoverlay; + +import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; + +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS; +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISSED_OTHER; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EDIT_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT; + +import static java.util.Objects.requireNonNull; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.MainThread; +import android.app.ICompatCameraControlCallback; +import android.app.RemoteAction; +import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Insets; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Icon; +import android.hardware.display.DisplayManager; +import android.hardware.input.InputManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Looper; +import android.provider.DeviceConfig; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.MathUtils; +import android.util.Size; +import android.util.TypedValue; +import android.view.Display; +import android.view.DisplayCutout; +import android.view.Gravity; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.InputMonitor; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.LinearInterpolator; +import android.view.animation.PathInterpolator; +import android.view.textclassifier.TextClassification; +import android.view.textclassifier.TextClassificationManager; +import android.view.textclassifier.TextClassifier; +import android.view.textclassifier.TextLinks; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import com.android.internal.logging.UiEventLogger; +import com.android.internal.policy.PhoneWindow; +import com.android.systemui.R; +import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.screenshot.DraggableConstraintLayout; +import com.android.systemui.screenshot.FloatingWindowUtil; +import com.android.systemui.screenshot.OverlayActionChip; +import com.android.systemui.screenshot.TimeoutHandler; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * Controls state and UI for the overlay that appears when something is added to the clipboard + */ +public class ClipboardOverlayControllerLegacy implements ClipboardListener.ClipboardOverlay { + private static final String TAG = "ClipboardOverlayCtrlr"; + private static final String REMOTE_COPY_ACTION = "android.intent.action.REMOTE_COPY"; + + /** Constants for screenshot/copy deconflicting */ + public static final String SCREENSHOT_ACTION = "com.android.systemui.SCREENSHOT"; + public static final String SELF_PERMISSION = "com.android.systemui.permission.SELF"; + public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY"; + + private static final String EXTRA_EDIT_SOURCE_CLIPBOARD = "edit_source_clipboard"; + + private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000; + private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe + private static final int FONT_SEARCH_STEP_PX = 4; + + private final Context mContext; + private final ClipboardLogger mClipboardLogger; + private final BroadcastDispatcher mBroadcastDispatcher; + private final DisplayManager mDisplayManager; + private final DisplayMetrics mDisplayMetrics; + private final WindowManager mWindowManager; + private final WindowManager.LayoutParams mWindowLayoutParams; + private final PhoneWindow mWindow; + private final TimeoutHandler mTimeoutHandler; + private final AccessibilityManager mAccessibilityManager; + private final TextClassifier mTextClassifier; + + private final DraggableConstraintLayout mView; + private final View mClipboardPreview; + private final ImageView mImagePreview; + private final TextView mTextPreview; + private final TextView mHiddenPreview; + private final View mPreviewBorder; + private final OverlayActionChip mEditChip; + private final OverlayActionChip mShareChip; + private final OverlayActionChip mRemoteCopyChip; + private final View mActionContainerBackground; + private final View mDismissButton; + private final LinearLayout mActionContainer; + private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>(); + + private Runnable mOnSessionCompleteListener; + + private InputMonitor mInputMonitor; + private InputEventReceiver mInputEventReceiver; + + private BroadcastReceiver mCloseDialogsReceiver; + private BroadcastReceiver mScreenshotReceiver; + + private boolean mBlockAttach = false; + private Animator mExitAnimator; + private Animator mEnterAnimator; + private final int mOrientation; + private boolean mKeyboardVisible; + + + public ClipboardOverlayControllerLegacy(Context context, + BroadcastDispatcher broadcastDispatcher, + BroadcastSender broadcastSender, + TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) { + mBroadcastDispatcher = broadcastDispatcher; + mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class)); + final Context displayContext = context.createDisplayContext(getDefaultDisplay()); + mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null); + + mClipboardLogger = new ClipboardLogger(uiEventLogger); + + mAccessibilityManager = AccessibilityManager.getInstance(mContext); + mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class)) + .getTextClassifier(); + + mWindowManager = mContext.getSystemService(WindowManager.class); + + mDisplayMetrics = new DisplayMetrics(); + mContext.getDisplay().getRealMetrics(mDisplayMetrics); + + mTimeoutHandler = timeoutHandler; + mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS); + + // Setup the window that we are going to use + mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); + mWindowLayoutParams.setTitle("ClipboardOverlay"); + + mWindow = FloatingWindowUtil.getFloatingWindow(mContext); + mWindow.setWindowManager(mWindowManager, null, null); + + setWindowFocusable(false); + + mView = (DraggableConstraintLayout) + LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay_legacy, null); + mActionContainerBackground = + requireNonNull(mView.findViewById(R.id.actions_container_background)); + mActionContainer = requireNonNull(mView.findViewById(R.id.actions)); + mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview)); + mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview)); + mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview)); + mHiddenPreview = requireNonNull(mView.findViewById(R.id.hidden_preview)); + mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border)); + mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip)); + mShareChip = requireNonNull(mView.findViewById(R.id.share_chip)); + mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip)); + mEditChip.setAlpha(1); + mShareChip.setAlpha(1); + mRemoteCopyChip.setAlpha(1); + mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button)); + + mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share)); + mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() { + @Override + public void onInteraction() { + mTimeoutHandler.resetTimeout(); + } + + @Override + public void onSwipeDismissInitiated(Animator animator) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED); + mExitAnimator = animator; + } + + @Override + public void onDismissComplete() { + hideImmediate(); + } + }); + + mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> { + int availableHeight = mTextPreview.getHeight() + - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom()); + mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight()); + return true; + }); + + mDismissButton.setOnClickListener(view -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED); + animateOut(); + }); + + mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true); + mRemoteCopyChip.setIcon( + Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true); + mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); + mOrientation = mContext.getResources().getConfiguration().orientation; + + attachWindow(); + withWindowAttached(() -> { + mWindow.setContentView(mView); + WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime()); + updateInsets(insets); + mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + WindowInsets insets = + mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime()); + if (keyboardVisible != mKeyboardVisible) { + mKeyboardVisible = keyboardVisible; + updateInsets(insets); + } + } + }); + mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( + new ViewRootImpl.ActivityConfigCallback() { + @Override + public void onConfigurationChanged(Configuration overrideConfig, + int newDisplayId) { + if (mContext.getResources().getConfiguration().orientation + != mOrientation) { + mClipboardLogger.logSessionComplete( + CLIPBOARD_OVERLAY_DISMISSED_OTHER); + hideImmediate(); + } + } + + @Override + public void requestCompatCameraControl( + boolean showControl, boolean transformationApplied, + ICompatCameraControlCallback callback) { + Log.w(TAG, "unexpected requestCompatCameraControl call"); + } + }); + }); + + mTimeoutHandler.setOnTimeoutRunnable(() -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TIMED_OUT); + animateOut(); + }); + + mCloseDialogsReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER); + animateOut(); + } + } + }; + + mBroadcastDispatcher.registerReceiver(mCloseDialogsReceiver, + new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS)); + mScreenshotReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (SCREENSHOT_ACTION.equals(intent.getAction())) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER); + animateOut(); + } + } + }; + + mBroadcastDispatcher.registerReceiver(mScreenshotReceiver, + new IntentFilter(SCREENSHOT_ACTION), null, null, Context.RECEIVER_EXPORTED, + SELF_PERMISSION); + monitorOutsideTouches(); + + Intent copyIntent = new Intent(COPY_OVERLAY_ACTION); + // Set package name so the system knows it's safe + copyIntent.setPackage(mContext.getPackageName()); + broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION); + } + + @Override // ClipboardListener.ClipboardOverlay + public void setClipData(ClipData clipData, String clipSource) { + if (mExitAnimator != null && mExitAnimator.isRunning()) { + mExitAnimator.cancel(); + } + reset(); + String accessibilityAnnouncement; + + boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null + && clipData.getDescription().getExtras() + .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE); + if (clipData == null || clipData.getItemCount() == 0) { + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) { + ClipData.Item item = clipData.getItemAt(0); + if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) { + if (item.getTextLinks() != null) { + AsyncTask.execute(() -> classifyText(clipData.getItemAt(0), clipSource)); + } + } + if (isSensitive) { + showEditableText( + mContext.getResources().getString(R.string.clipboard_asterisks), true); + } else { + showEditableText(item.getText(), false); + } + showShareChip(clipData); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied); + } else if (clipData.getItemAt(0).getUri() != null) { + if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) { + showShareChip(clipData); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied); + } else { + accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + } + } else { + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + } + Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext); + // Only show remote copy if it's available. + PackageManager packageManager = mContext.getPackageManager(); + if (packageManager.resolveActivity( + remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) { + mRemoteCopyChip.setContentDescription( + mContext.getString(R.string.clipboard_send_nearby_description)); + mRemoteCopyChip.setVisibility(View.VISIBLE); + mRemoteCopyChip.setOnClickListener((v) -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED); + mContext.startActivity(remoteCopyIntent); + animateOut(); + }); + mActionContainerBackground.setVisibility(View.VISIBLE); + } else { + mRemoteCopyChip.setVisibility(View.GONE); + } + withWindowAttached(() -> { + if (mEnterAnimator == null || !mEnterAnimator.isRunning()) { + mView.post(this::animateIn); + } + mView.announceForAccessibility(accessibilityAnnouncement); + }); + mTimeoutHandler.resetTimeout(); + } + + @Override // ClipboardListener.ClipboardOverlay + public void setOnSessionCompleteListener(Runnable runnable) { + mOnSessionCompleteListener = runnable; + } + + private void classifyText(ClipData.Item item, String source) { + ArrayList<RemoteAction> actions = new ArrayList<>(); + for (TextLinks.TextLink link : item.getTextLinks().getLinks()) { + TextClassification classification = mTextClassifier.classifyText( + item.getText(), link.getStart(), link.getEnd(), null); + actions.addAll(classification.getActions()); + } + mView.post(() -> { + resetActionChips(); + if (actions.size() > 0) { + mActionContainerBackground.setVisibility(View.VISIBLE); + for (RemoteAction action : actions) { + Intent targetIntent = action.getActionIntent().getIntent(); + ComponentName component = targetIntent.getComponent(); + if (component != null && !TextUtils.equals(source, + component.getPackageName())) { + OverlayActionChip chip = constructActionChip(action); + mActionContainer.addView(chip); + mActionChips.add(chip); + break; // only show at most one action chip + } + } + } + }); + } + + private void showShareChip(ClipData clip) { + mShareChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + mShareChip.setOnClickListener((v) -> shareContent(clip)); + } + + private OverlayActionChip constructActionChip(RemoteAction action) { + OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate( + R.layout.overlay_action_chip, mActionContainer, false); + chip.setText(action.getTitle()); + chip.setContentDescription(action.getTitle()); + chip.setIcon(action.getIcon(), false); + chip.setPendingIntent(action.getActionIntent(), () -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED); + animateOut(); + }); + chip.setAlpha(1); + return chip; + } + + private void monitorOutsideTouches() { + InputManager inputManager = mContext.getSystemService(InputManager.class); + mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0); + mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(), + Looper.getMainLooper()) { + @Override + public void onInputEvent(InputEvent event) { + if (event instanceof MotionEvent) { + MotionEvent motionEvent = (MotionEvent) event; + if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + Region touchRegion = new Region(); + + final Rect tmpRect = new Rect(); + mPreviewBorder.getBoundsOnScreen(tmpRect); + tmpRect.inset( + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, + -SWIPE_PADDING_DP)); + touchRegion.op(tmpRect, Region.Op.UNION); + mActionContainerBackground.getBoundsOnScreen(tmpRect); + tmpRect.inset( + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, + -SWIPE_PADDING_DP)); + touchRegion.op(tmpRect, Region.Op.UNION); + mDismissButton.getBoundsOnScreen(tmpRect); + touchRegion.op(tmpRect, Region.Op.UNION); + if (!touchRegion.contains( + (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE); + animateOut(); + } + } + } + finishInputEvent(event, true /* handled */); + } + }; + } + + private void editImage(Uri uri) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED); + mContext.startActivity(IntentCreator.getImageEditIntent(uri, mContext)); + animateOut(); + } + + private void editText() { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED); + mContext.startActivity(IntentCreator.getTextEditorIntent(mContext)); + animateOut(); + } + + private void shareContent(ClipData clip) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SHARE_TAPPED); + mContext.startActivity(IntentCreator.getShareIntent(clip, mContext)); + animateOut(); + } + + private void showSinglePreview(View v) { + mTextPreview.setVisibility(View.GONE); + mImagePreview.setVisibility(View.GONE); + mHiddenPreview.setVisibility(View.GONE); + v.setVisibility(View.VISIBLE); + } + + private void showTextPreview(CharSequence text, TextView textView) { + showSinglePreview(textView); + final CharSequence truncatedText = text.subSequence(0, Math.min(500, text.length())); + textView.setText(truncatedText); + updateTextSize(truncatedText, textView); + + textView.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (right - left != oldRight - oldLeft) { + updateTextSize(truncatedText, textView); + } + }); + mEditChip.setVisibility(View.GONE); + } + + private void updateTextSize(CharSequence text, TextView textView) { + Paint paint = new Paint(textView.getPaint()); + Resources res = textView.getResources(); + float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font); + float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font); + if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) { + // If the text is a single word and would fit within the TextView at the min font size, + // find the biggest font size that will fit. + float fontSizePx = minFontSize; + while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize + && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) { + fontSizePx += FONT_SEARCH_STEP_PX; + } + // Need to turn off autosizing, otherwise setTextSize is a no-op. + textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE); + // It's possible to hit the max font size and not fill the width, so centering + // horizontally looks better in this case. + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx); + } else { + // Otherwise just stick with autosize. + textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize, + (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX); + textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); + } + } + + private static boolean fitsInView(CharSequence text, TextView textView, Paint paint, + float fontSizePx) { + paint.setTextSize(fontSizePx); + float size = paint.measureText(text.toString()); + float availableWidth = textView.getWidth() - textView.getPaddingLeft() + - textView.getPaddingRight(); + return size < availableWidth; + } + + private static boolean isOneWord(CharSequence text) { + return text.toString().split("\\s+", 2).length == 1; + } + + private void showEditableText(CharSequence text, boolean hidden) { + TextView textView = hidden ? mHiddenPreview : mTextPreview; + showTextPreview(text, textView); + View.OnClickListener listener = v -> editText(); + setAccessibilityActionToEdit(textView); + if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) { + mEditChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + mEditChip.setContentDescription( + mContext.getString(R.string.clipboard_edit_text_description)); + mEditChip.setOnClickListener(listener); + } + textView.setOnClickListener(listener); + } + + private boolean tryShowEditableImage(Uri uri, boolean isSensitive) { + View.OnClickListener listener = v -> editImage(uri); + ContentResolver resolver = mContext.getContentResolver(); + String mimeType = resolver.getType(uri); + boolean isEditableImage = mimeType != null && mimeType.startsWith("image"); + if (isSensitive) { + mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden)); + showSinglePreview(mHiddenPreview); + if (isEditableImage) { + mHiddenPreview.setOnClickListener(listener); + setAccessibilityActionToEdit(mHiddenPreview); + } + } else if (isEditableImage) { // if the MIMEtype is image, try to load + try { + int size = mContext.getResources().getDimensionPixelSize(R.dimen.overlay_x_scale); + // The width of the view is capped, height maintains aspect ratio, so allow it to be + // taller if needed. + Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null); + showSinglePreview(mImagePreview); + mImagePreview.setImageBitmap(thumbnail); + mImagePreview.setOnClickListener(listener); + setAccessibilityActionToEdit(mImagePreview); + } catch (IOException e) { + Log.e(TAG, "Thumbnail loading failed", e); + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); + isEditableImage = false; + } + } else { + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); + } + if (isEditableImage && DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) { + mEditChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + mEditChip.setOnClickListener(listener); + mEditChip.setContentDescription( + mContext.getString(R.string.clipboard_edit_image_description)); + } + return isEditableImage; + } + + private void setAccessibilityActionToEdit(View view) { + ViewCompat.replaceAccessibilityAction(view, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + mContext.getString(R.string.clipboard_edit), null); + } + + private void animateIn() { + if (mAccessibilityManager.isEnabled()) { + mDismissButton.setVisibility(View.VISIBLE); + } + mEnterAnimator = getEnterAnimation(); + mEnterAnimator.start(); + } + + private void animateOut() { + if (mExitAnimator != null && mExitAnimator.isRunning()) { + return; + } + Animator anim = getExitAnimation(); + anim.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (!mCancelled) { + hideImmediate(); + } + } + }); + mExitAnimator = anim; + anim.start(); + } + + private Animator getEnterAnimation() { + TimeInterpolator linearInterpolator = new LinearInterpolator(); + TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f); + AnimatorSet enterAnim = new AnimatorSet(); + + ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); + rootAnim.setInterpolator(linearInterpolator); + rootAnim.setDuration(66); + rootAnim.addUpdateListener(animation -> { + mView.setAlpha(animation.getAnimatedFraction()); + }); + + ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); + scaleAnim.setInterpolator(scaleInterpolator); + scaleAnim.setDuration(333); + scaleAnim.addUpdateListener(animation -> { + float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); + mClipboardPreview.setScaleX(previewScale); + mClipboardPreview.setScaleY(previewScale); + mPreviewBorder.setScaleX(previewScale); + mPreviewBorder.setScaleY(previewScale); + + float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); + mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); + mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); + float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction()); + float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); + mActionContainer.setScaleX(actionsScaleX); + mActionContainer.setScaleY(actionsScaleY); + mActionContainerBackground.setScaleX(actionsScaleX); + mActionContainerBackground.setScaleY(actionsScaleY); + }); + + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setInterpolator(linearInterpolator); + alphaAnim.setDuration(283); + alphaAnim.addUpdateListener(animation -> { + float alpha = animation.getAnimatedFraction(); + mClipboardPreview.setAlpha(alpha); + mPreviewBorder.setAlpha(alpha); + mDismissButton.setAlpha(alpha); + mActionContainer.setAlpha(alpha); + }); + + mActionContainer.setAlpha(0); + mPreviewBorder.setAlpha(0); + mClipboardPreview.setAlpha(0); + enterAnim.play(rootAnim).with(scaleAnim); + enterAnim.play(alphaAnim).after(50).after(rootAnim); + + enterAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mView.setAlpha(1); + mTimeoutHandler.resetTimeout(); + } + }); + return enterAnim; + } + + private Animator getExitAnimation() { + TimeInterpolator linearInterpolator = new LinearInterpolator(); + TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f); + AnimatorSet exitAnim = new AnimatorSet(); + + ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); + rootAnim.setInterpolator(linearInterpolator); + rootAnim.setDuration(100); + rootAnim.addUpdateListener(anim -> mView.setAlpha(1 - anim.getAnimatedFraction())); + + ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); + scaleAnim.setInterpolator(scaleInterpolator); + scaleAnim.setDuration(250); + scaleAnim.addUpdateListener(animation -> { + float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); + mClipboardPreview.setScaleX(previewScale); + mClipboardPreview.setScaleY(previewScale); + mPreviewBorder.setScaleX(previewScale); + mPreviewBorder.setScaleY(previewScale); + + float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); + mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); + mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); + float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction()); + float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); + mActionContainer.setScaleX(actionScaleX); + mActionContainer.setScaleY(actionScaleY); + mActionContainerBackground.setScaleX(actionScaleX); + mActionContainerBackground.setScaleY(actionScaleY); + }); + + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setInterpolator(linearInterpolator); + alphaAnim.setDuration(166); + alphaAnim.addUpdateListener(animation -> { + float alpha = 1 - animation.getAnimatedFraction(); + mClipboardPreview.setAlpha(alpha); + mPreviewBorder.setAlpha(alpha); + mDismissButton.setAlpha(alpha); + mActionContainer.setAlpha(alpha); + }); + + exitAnim.play(alphaAnim).with(scaleAnim); + exitAnim.play(rootAnim).after(150).after(alphaAnim); + return exitAnim; + } + + private void hideImmediate() { + // Note this may be called multiple times if multiple dismissal events happen at the same + // time. + mTimeoutHandler.cancelTimeout(); + final View decorView = mWindow.peekDecorView(); + if (decorView != null && decorView.isAttachedToWindow()) { + mWindowManager.removeViewImmediate(decorView); + } + if (mCloseDialogsReceiver != null) { + mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver); + mCloseDialogsReceiver = null; + } + if (mScreenshotReceiver != null) { + mBroadcastDispatcher.unregisterReceiver(mScreenshotReceiver); + mScreenshotReceiver = null; + } + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + } + if (mInputMonitor != null) { + mInputMonitor.dispose(); + mInputMonitor = null; + } + if (mOnSessionCompleteListener != null) { + mOnSessionCompleteListener.run(); + } + } + + private void resetActionChips() { + for (OverlayActionChip chip : mActionChips) { + mActionContainer.removeView(chip); + } + mActionChips.clear(); + } + + private void reset() { + mView.setTranslationX(0); + mView.setAlpha(0); + mActionContainerBackground.setVisibility(View.GONE); + mShareChip.setVisibility(View.GONE); + mEditChip.setVisibility(View.GONE); + mRemoteCopyChip.setVisibility(View.GONE); + resetActionChips(); + mTimeoutHandler.cancelTimeout(); + mClipboardLogger.reset(); + } + + @MainThread + private void attachWindow() { + View decorView = mWindow.getDecorView(); + if (decorView.isAttachedToWindow() || mBlockAttach) { + return; + } + mBlockAttach = true; + mWindowManager.addView(decorView, mWindowLayoutParams); + decorView.requestApplyInsets(); + mView.requestApplyInsets(); + decorView.getViewTreeObserver().addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + mBlockAttach = false; + } + + @Override + public void onWindowDetached() { + } + } + ); + } + + private void withWindowAttached(Runnable action) { + View decorView = mWindow.getDecorView(); + if (decorView.isAttachedToWindow()) { + action.run(); + } else { + decorView.getViewTreeObserver().addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + mBlockAttach = false; + decorView.getViewTreeObserver().removeOnWindowAttachListener(this); + action.run(); + } + + @Override + public void onWindowDetached() { + } + }); + } + } + + private void updateInsets(WindowInsets insets) { + int orientation = mContext.getResources().getConfiguration().orientation; + FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams(); + if (p == null) { + return; + } + DisplayCutout cutout = insets.getDisplayCutout(); + Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); + Insets imeInsets = insets.getInsets(WindowInsets.Type.ime()); + if (cutout == null) { + p.setMargins(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom)); + } else { + Insets waterfall = cutout.getWaterfallInsets(); + if (orientation == ORIENTATION_PORTRAIT) { + p.setMargins( + waterfall.left, + Math.max(cutout.getSafeInsetTop(), waterfall.top), + waterfall.right, + Math.max(imeInsets.bottom, + Math.max(cutout.getSafeInsetBottom(), + Math.max(navBarInsets.bottom, waterfall.bottom)))); + } else { + p.setMargins( + waterfall.left, + waterfall.top, + waterfall.right, + Math.max(imeInsets.bottom, + Math.max(navBarInsets.bottom, waterfall.bottom))); + } + } + mView.setLayoutParams(p); + mView.requestLayout(); + } + + private Display getDefaultDisplay() { + return mDisplayManager.getDisplay(DEFAULT_DISPLAY); + } + + /** + * Updates the window focusability. If the window is already showing, then it updates the + * window immediately, otherwise the layout params will be applied when the window is next + * shown. + */ + private void setWindowFocusable(boolean focusable) { + int flags = mWindowLayoutParams.flags; + if (focusable) { + mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } else { + mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } + if (mWindowLayoutParams.flags == flags) { + return; + } + final View decorView = mWindow.peekDecorView(); + if (decorView != null && decorView.isAttachedToWindow()) { + mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); + } + } + + static class ClipboardLogger { + private final UiEventLogger mUiEventLogger; + private boolean mGuarded = false; + + ClipboardLogger(UiEventLogger uiEventLogger) { + mUiEventLogger = uiEventLogger; + } + + void logSessionComplete(@NonNull UiEventLogger.UiEventEnum event) { + if (!mGuarded) { + mGuarded = true; + mUiEventLogger.log(event); + } + } + + void reset() { + mGuarded = false; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerFactory.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java index 8b0b2a59dd92..0d989a78947d 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerFactory.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java @@ -27,17 +27,17 @@ import com.android.systemui.screenshot.TimeoutHandler; import javax.inject.Inject; /** - * A factory that churns out ClipboardOverlayControllers on demand. + * A factory that churns out ClipboardOverlayControllerLegacys on demand. */ @SysUISingleton -public class ClipboardOverlayControllerFactory { +public class ClipboardOverlayControllerLegacyFactory { private final UiEventLogger mUiEventLogger; private final BroadcastDispatcher mBroadcastDispatcher; private final BroadcastSender mBroadcastSender; @Inject - public ClipboardOverlayControllerFactory(BroadcastDispatcher broadcastDispatcher, + public ClipboardOverlayControllerLegacyFactory(BroadcastDispatcher broadcastDispatcher, BroadcastSender broadcastSender, UiEventLogger uiEventLogger) { this.mBroadcastDispatcher = broadcastDispatcher; this.mBroadcastSender = broadcastSender; @@ -45,10 +45,10 @@ public class ClipboardOverlayControllerFactory { } /** - * One new ClipboardOverlayController, coming right up! + * One new ClipboardOverlayControllerLegacy, coming right up! */ - public ClipboardOverlayController create(Context context) { - return new ClipboardOverlayController(context, mBroadcastDispatcher, mBroadcastSender, + public ClipboardOverlayControllerLegacy create(Context context) { + return new ClipboardOverlayControllerLegacy(context, mBroadcastDispatcher, mBroadcastSender, new TimeoutHandler(context), mUiEventLogger); } } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java new file mode 100644 index 000000000000..2d3315759371 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java @@ -0,0 +1,482 @@ +/* + * 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.clipboardoverlay; + +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + +import static java.util.Objects.requireNonNull; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.app.RemoteAction; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Insets; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Icon; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.MathUtils; +import android.util.TypedValue; +import android.view.DisplayCutout; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowInsets; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.LinearInterpolator; +import android.view.animation.PathInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import com.android.systemui.R; +import com.android.systemui.screenshot.DraggableConstraintLayout; +import com.android.systemui.screenshot.FloatingWindowUtil; +import com.android.systemui.screenshot.OverlayActionChip; + +import java.util.ArrayList; + +/** + * Handles the visual elements and animations for the clipboard overlay. + */ +public class ClipboardOverlayView extends DraggableConstraintLayout { + + interface ClipboardOverlayCallbacks extends SwipeDismissCallbacks { + void onDismissButtonTapped(); + + void onRemoteCopyButtonTapped(); + + void onEditButtonTapped(); + + void onShareButtonTapped(); + + void onPreviewTapped(); + } + + private static final String TAG = "ClipboardView"; + + private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe + private static final int FONT_SEARCH_STEP_PX = 4; + + private final DisplayMetrics mDisplayMetrics; + private final AccessibilityManager mAccessibilityManager; + private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>(); + + private View mClipboardPreview; + private ImageView mImagePreview; + private TextView mTextPreview; + private TextView mHiddenPreview; + private View mPreviewBorder; + private OverlayActionChip mEditChip; + private OverlayActionChip mShareChip; + private OverlayActionChip mRemoteCopyChip; + private View mActionContainerBackground; + private View mDismissButton; + private LinearLayout mActionContainer; + + public ClipboardOverlayView(Context context) { + this(context, null); + } + + public ClipboardOverlayView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ClipboardOverlayView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mDisplayMetrics = new DisplayMetrics(); + mContext.getDisplay().getRealMetrics(mDisplayMetrics); + mAccessibilityManager = AccessibilityManager.getInstance(mContext); + } + + @Override + protected void onFinishInflate() { + mActionContainerBackground = + requireNonNull(findViewById(R.id.actions_container_background)); + mActionContainer = requireNonNull(findViewById(R.id.actions)); + mClipboardPreview = requireNonNull(findViewById(R.id.clipboard_preview)); + mImagePreview = requireNonNull(findViewById(R.id.image_preview)); + mTextPreview = requireNonNull(findViewById(R.id.text_preview)); + mHiddenPreview = requireNonNull(findViewById(R.id.hidden_preview)); + mPreviewBorder = requireNonNull(findViewById(R.id.preview_border)); + mEditChip = requireNonNull(findViewById(R.id.edit_chip)); + mShareChip = requireNonNull(findViewById(R.id.share_chip)); + mRemoteCopyChip = requireNonNull(findViewById(R.id.remote_copy_chip)); + mDismissButton = requireNonNull(findViewById(R.id.dismiss_button)); + + mEditChip.setAlpha(1); + mShareChip.setAlpha(1); + mRemoteCopyChip.setAlpha(1); + mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share)); + + mEditChip.setIcon( + Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true); + mRemoteCopyChip.setIcon( + Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true); + mShareChip.setIcon( + Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); + + mRemoteCopyChip.setContentDescription( + mContext.getString(R.string.clipboard_send_nearby_description)); + + mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> { + int availableHeight = mTextPreview.getHeight() + - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom()); + mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight()); + return true; + }); + super.onFinishInflate(); + } + + @Override + public void setCallbacks(SwipeDismissCallbacks callbacks) { + super.setCallbacks(callbacks); + ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks; + mEditChip.setOnClickListener(v -> clipboardCallbacks.onEditButtonTapped()); + mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped()); + mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped()); + mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped()); + mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped()); + } + + void setEditAccessibilityAction(boolean editable) { + if (editable) { + ViewCompat.replaceAccessibilityAction(mClipboardPreview, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + mContext.getString(R.string.clipboard_edit), null); + } else { + ViewCompat.replaceAccessibilityAction(mClipboardPreview, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + null, null); + } + } + + void setInsets(WindowInsets insets, int orientation) { + FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) getLayoutParams(); + if (p == null) { + return; + } + Rect margins = computeMargins(insets, orientation); + p.setMargins(margins.left, margins.top, margins.right, margins.bottom); + setLayoutParams(p); + requestLayout(); + } + + boolean isInTouchRegion(int x, int y) { + Region touchRegion = new Region(); + final Rect tmpRect = new Rect(); + + mPreviewBorder.getBoundsOnScreen(tmpRect); + tmpRect.inset( + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP)); + touchRegion.op(tmpRect, Region.Op.UNION); + + mActionContainerBackground.getBoundsOnScreen(tmpRect); + tmpRect.inset( + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP)); + touchRegion.op(tmpRect, Region.Op.UNION); + + mDismissButton.getBoundsOnScreen(tmpRect); + touchRegion.op(tmpRect, Region.Op.UNION); + + return touchRegion.contains(x, y); + } + + void setRemoteCopyVisibility(boolean visible) { + if (visible) { + mRemoteCopyChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + } else { + mRemoteCopyChip.setVisibility(View.GONE); + } + } + + void showDefaultTextPreview() { + String copied = mContext.getString(R.string.clipboard_overlay_text_copied); + showTextPreview(copied, false); + } + + void showTextPreview(CharSequence text, boolean hidden) { + TextView textView = hidden ? mHiddenPreview : mTextPreview; + showSinglePreview(textView); + textView.setText(text.subSequence(0, Math.min(500, text.length()))); + updateTextSize(text, textView); + textView.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (right - left != oldRight - oldLeft) { + updateTextSize(text, textView); + } + }); + mEditChip.setVisibility(View.GONE); + } + + void showImagePreview(@Nullable Bitmap thumbnail) { + if (thumbnail == null) { + mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden)); + showSinglePreview(mHiddenPreview); + } else { + mImagePreview.setImageBitmap(thumbnail); + showSinglePreview(mImagePreview); + } + } + + void showEditChip(String contentDescription) { + mEditChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + mEditChip.setContentDescription(contentDescription); + } + + void showShareChip() { + mShareChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + } + + void reset() { + setTranslationX(0); + setAlpha(0); + mActionContainerBackground.setVisibility(View.GONE); + mDismissButton.setVisibility(View.GONE); + mShareChip.setVisibility(View.GONE); + mEditChip.setVisibility(View.GONE); + mRemoteCopyChip.setVisibility(View.GONE); + setEditAccessibilityAction(false); + resetActionChips(); + } + + void resetActionChips() { + for (OverlayActionChip chip : mActionChips) { + mActionContainer.removeView(chip); + } + mActionChips.clear(); + } + + Animator getEnterAnimation() { + if (mAccessibilityManager.isEnabled()) { + mDismissButton.setVisibility(View.VISIBLE); + } + TimeInterpolator linearInterpolator = new LinearInterpolator(); + TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f); + AnimatorSet enterAnim = new AnimatorSet(); + + ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); + rootAnim.setInterpolator(linearInterpolator); + rootAnim.setDuration(66); + rootAnim.addUpdateListener(animation -> { + setAlpha(animation.getAnimatedFraction()); + }); + + ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); + scaleAnim.setInterpolator(scaleInterpolator); + scaleAnim.setDuration(333); + scaleAnim.addUpdateListener(animation -> { + float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); + mClipboardPreview.setScaleX(previewScale); + mClipboardPreview.setScaleY(previewScale); + mPreviewBorder.setScaleX(previewScale); + mPreviewBorder.setScaleY(previewScale); + + float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); + mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); + mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); + float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction()); + float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); + mActionContainer.setScaleX(actionsScaleX); + mActionContainer.setScaleY(actionsScaleY); + mActionContainerBackground.setScaleX(actionsScaleX); + mActionContainerBackground.setScaleY(actionsScaleY); + }); + + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setInterpolator(linearInterpolator); + alphaAnim.setDuration(283); + alphaAnim.addUpdateListener(animation -> { + float alpha = animation.getAnimatedFraction(); + mClipboardPreview.setAlpha(alpha); + mPreviewBorder.setAlpha(alpha); + mDismissButton.setAlpha(alpha); + mActionContainer.setAlpha(alpha); + }); + + mActionContainer.setAlpha(0); + mPreviewBorder.setAlpha(0); + mClipboardPreview.setAlpha(0); + enterAnim.play(rootAnim).with(scaleAnim); + enterAnim.play(alphaAnim).after(50).after(rootAnim); + + enterAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + setAlpha(1); + } + }); + return enterAnim; + } + + Animator getExitAnimation() { + TimeInterpolator linearInterpolator = new LinearInterpolator(); + TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f); + AnimatorSet exitAnim = new AnimatorSet(); + + ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); + rootAnim.setInterpolator(linearInterpolator); + rootAnim.setDuration(100); + rootAnim.addUpdateListener(anim -> setAlpha(1 - anim.getAnimatedFraction())); + + ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); + scaleAnim.setInterpolator(scaleInterpolator); + scaleAnim.setDuration(250); + scaleAnim.addUpdateListener(animation -> { + float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); + mClipboardPreview.setScaleX(previewScale); + mClipboardPreview.setScaleY(previewScale); + mPreviewBorder.setScaleX(previewScale); + mPreviewBorder.setScaleY(previewScale); + + float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); + mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); + mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); + float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction()); + float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); + mActionContainer.setScaleX(actionScaleX); + mActionContainer.setScaleY(actionScaleY); + mActionContainerBackground.setScaleX(actionScaleX); + mActionContainerBackground.setScaleY(actionScaleY); + }); + + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setInterpolator(linearInterpolator); + alphaAnim.setDuration(166); + alphaAnim.addUpdateListener(animation -> { + float alpha = 1 - animation.getAnimatedFraction(); + mClipboardPreview.setAlpha(alpha); + mPreviewBorder.setAlpha(alpha); + mDismissButton.setAlpha(alpha); + mActionContainer.setAlpha(alpha); + }); + + exitAnim.play(alphaAnim).with(scaleAnim); + exitAnim.play(rootAnim).after(150).after(alphaAnim); + return exitAnim; + } + + void setActionChip(RemoteAction action, Runnable onFinish) { + mActionContainerBackground.setVisibility(View.VISIBLE); + OverlayActionChip chip = constructActionChip(action, onFinish); + mActionContainer.addView(chip); + mActionChips.add(chip); + } + + private void showSinglePreview(View v) { + mTextPreview.setVisibility(View.GONE); + mImagePreview.setVisibility(View.GONE); + mHiddenPreview.setVisibility(View.GONE); + v.setVisibility(View.VISIBLE); + } + + private OverlayActionChip constructActionChip(RemoteAction action, Runnable onFinish) { + OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate( + R.layout.overlay_action_chip, mActionContainer, false); + chip.setText(action.getTitle()); + chip.setContentDescription(action.getTitle()); + chip.setIcon(action.getIcon(), false); + chip.setPendingIntent(action.getActionIntent(), onFinish); + chip.setAlpha(1); + return chip; + } + + private static void updateTextSize(CharSequence text, TextView textView) { + Paint paint = new Paint(textView.getPaint()); + Resources res = textView.getResources(); + float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font); + float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font); + if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) { + // If the text is a single word and would fit within the TextView at the min font size, + // find the biggest font size that will fit. + float fontSizePx = minFontSize; + while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize + && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) { + fontSizePx += FONT_SEARCH_STEP_PX; + } + // Need to turn off autosizing, otherwise setTextSize is a no-op. + textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE); + // It's possible to hit the max font size and not fill the width, so centering + // horizontally looks better in this case. + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx); + } else { + // Otherwise just stick with autosize. + textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize, + (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX); + textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); + } + } + + private static boolean fitsInView(CharSequence text, TextView textView, Paint paint, + float fontSizePx) { + paint.setTextSize(fontSizePx); + float size = paint.measureText(text.toString()); + float availableWidth = textView.getWidth() - textView.getPaddingLeft() + - textView.getPaddingRight(); + return size < availableWidth; + } + + private static boolean isOneWord(CharSequence text) { + return text.toString().split("\\s+", 2).length == 1; + } + + private static Rect computeMargins(WindowInsets insets, int orientation) { + DisplayCutout cutout = insets.getDisplayCutout(); + Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); + Insets imeInsets = insets.getInsets(WindowInsets.Type.ime()); + if (cutout == null) { + return new Rect(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom)); + } else { + Insets waterfall = cutout.getWaterfallInsets(); + if (orientation == ORIENTATION_PORTRAIT) { + return new Rect( + waterfall.left, + Math.max(cutout.getSafeInsetTop(), waterfall.top), + waterfall.right, + Math.max(imeInsets.bottom, + Math.max(cutout.getSafeInsetBottom(), + Math.max(navBarInsets.bottom, waterfall.bottom)))); + } else { + return new Rect( + waterfall.left, + waterfall.top, + waterfall.right, + Math.max(imeInsets.bottom, + Math.max(navBarInsets.bottom, waterfall.bottom))); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java new file mode 100644 index 000000000000..9dac9b393d60 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java @@ -0,0 +1,174 @@ +/* + * 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.clipboardoverlay; + +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.app.ICompatCameraControlCallback; +import android.content.Context; +import android.content.res.Configuration; +import android.util.Log; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; + +import com.android.internal.policy.PhoneWindow; +import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext; +import com.android.systemui.screenshot.FloatingWindowUtil; + +import java.util.function.BiConsumer; + +import javax.inject.Inject; + +/** + * Handles attaching the window and the window insets for the clipboard overlay. + */ +public class ClipboardOverlayWindow extends PhoneWindow + implements ViewRootImpl.ActivityConfigCallback { + private static final String TAG = "ClipboardOverlayWindow"; + + private final Context mContext; + private final WindowManager mWindowManager; + private final WindowManager.LayoutParams mWindowLayoutParams; + + private boolean mKeyboardVisible; + private final int mOrientation; + private BiConsumer<WindowInsets, Integer> mOnKeyboardChangeListener; + private Runnable mOnOrientationChangeListener; + + @Inject + ClipboardOverlayWindow(@OverlayWindowContext Context context) { + super(context); + mContext = context; + mOrientation = mContext.getResources().getConfiguration().orientation; + + // Setup the window that we are going to use + requestFeature(Window.FEATURE_NO_TITLE); + requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS); + setBackgroundDrawableResource(android.R.color.transparent); + mWindowManager = mContext.getSystemService(WindowManager.class); + mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); + mWindowLayoutParams.setTitle("ClipboardOverlay"); + setWindowManager(mWindowManager, null, null); + setWindowFocusable(false); + } + + /** + * Set callbacks for keyboard state change and orientation change and attach the window + * + * @param onKeyboardChangeListener callback for IME visibility changes + * @param onOrientationChangeListener callback for device orientation changes + */ + public void init(@NonNull BiConsumer<WindowInsets, Integer> onKeyboardChangeListener, + @NonNull Runnable onOrientationChangeListener) { + mOnKeyboardChangeListener = onKeyboardChangeListener; + mOnOrientationChangeListener = onOrientationChangeListener; + + attach(); + withWindowAttached(() -> { + WindowInsets currentInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + mKeyboardVisible = currentInsets.isVisible(WindowInsets.Type.ime()); + peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(() -> { + WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime()); + if (keyboardVisible != mKeyboardVisible) { + mKeyboardVisible = keyboardVisible; + mOnKeyboardChangeListener.accept(insets, mOrientation); + } + }); + peekDecorView().getViewRootImpl().setActivityConfigCallback(this); + }); + } + + @Override // ViewRootImpl.ActivityConfigCallback + public void onConfigurationChanged(Configuration overrideConfig, int newDisplayId) { + if (mContext.getResources().getConfiguration().orientation != mOrientation) { + mOnOrientationChangeListener.run(); + } + } + + @Override // ViewRootImpl.ActivityConfigCallback + public void requestCompatCameraControl(boolean showControl, boolean transformationApplied, + ICompatCameraControlCallback callback) { + Log.w(TAG, "unexpected requestCompatCameraControl call"); + } + + void remove() { + final View decorView = peekDecorView(); + if (decorView != null && decorView.isAttachedToWindow()) { + mWindowManager.removeViewImmediate(decorView); + } + } + + WindowInsets getWindowInsets() { + return mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + } + + void withWindowAttached(Runnable action) { + View decorView = getDecorView(); + if (decorView.isAttachedToWindow()) { + action.run(); + } else { + decorView.getViewTreeObserver().addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + decorView.getViewTreeObserver().removeOnWindowAttachListener(this); + action.run(); + } + + @Override + public void onWindowDetached() { + } + }); + } + } + + @MainThread + private void attach() { + View decorView = getDecorView(); + if (decorView.isAttachedToWindow()) { + return; + } + mWindowManager.addView(decorView, mWindowLayoutParams); + decorView.requestApplyInsets(); + } + + /** + * Updates the window focusability. If the window is already showing, then it updates the + * window immediately, otherwise the layout params will be applied when the window is next + * shown. + */ + private void setWindowFocusable(boolean focusable) { + int flags = mWindowLayoutParams.flags; + if (focusable) { + mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } else { + mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } + if (mWindowLayoutParams.flags == flags) { + return; + } + final View decorView = peekDecorView(); + if (decorView != null && decorView.isAttachedToWindow()) { + mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java new file mode 100644 index 000000000000..22448130f7e5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java @@ -0,0 +1,68 @@ +/* + * 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.clipboardoverlay.dagger; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.view.Display; +import android.view.LayoutInflater; + +import com.android.systemui.R; +import com.android.systemui.clipboardoverlay.ClipboardOverlayView; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +import dagger.Module; +import dagger.Provides; + +/** Module for {@link com.android.systemui.clipboardoverlay}. */ +@Module +public interface ClipboardOverlayModule { + + /** + * + */ + @Provides + @OverlayWindowContext + static Context provideWindowContext(DisplayManager displayManager, Context context) { + Display display = displayManager.getDisplay(DEFAULT_DISPLAY); + return context.createWindowContext(display, TYPE_SCREENSHOT, null); + } + + /** + * + */ + @Provides + static ClipboardOverlayView provideClipboardOverlayView(@OverlayWindowContext Context context) { + return (ClipboardOverlayView) LayoutInflater.from(context).inflate( + R.layout.clipboard_overlay, null); + } + + @Qualifier + @Documented + @Retention(RUNTIME) + @interface OverlayWindowContext { + } +} 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 index bebade0cc484..08e8293cbe9c 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt @@ -17,6 +17,7 @@ package com.android.systemui.common.shared.model import android.annotation.StringRes +import android.content.Context /** * Models a content description, that can either be already [loaded][ContentDescription.Loaded] or @@ -30,4 +31,20 @@ sealed class ContentDescription { data class Resource( @StringRes val res: Int, ) : ContentDescription() + + companion object { + /** + * Returns the loaded content description string, or null if we don't have one. + * + * Prefer [com.android.systemui.common.ui.binder.ContentDescriptionViewBinder.bind] over + * this method. This should only be used for testing or concatenation purposes. + */ + fun ContentDescription?.loadContentDescription(context: Context): String? { + return when (this) { + null -> null + is Loaded -> this.description + is Resource -> context.getString(this.res) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt index 5d0e08ffc307..4a5693202dba 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt @@ -18,6 +18,7 @@ package com.android.systemui.common.shared.model import android.annotation.StringRes +import android.content.Context /** * Models a text, that can either be already [loaded][Text.Loaded] or be a [reference] @@ -31,4 +32,20 @@ sealed class Text { data class Resource( @StringRes val res: Int, ) : Text() + + companion object { + /** + * Returns the loaded test string, or null if we don't have one. + * + * Prefer [com.android.systemui.common.ui.binder.TextViewBinder.bind] over this method. This + * should only be used for testing or concatenation purposes. + */ + fun Text?.loadText(context: Context): String? { + return when (this) { + null -> null + is Loaded -> this.text + is Resource -> context.getString(this.res) + } + } + } } 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 77b65233c112..d3b5d0edd222 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt @@ -21,6 +21,8 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Bundle +import android.os.RemoteException +import android.service.dreams.IDreamManager import android.view.View import android.view.ViewGroup import android.view.WindowInsets @@ -40,11 +42,13 @@ import javax.inject.Inject */ class ControlsActivity @Inject constructor( private val uiController: ControlsUiController, - private val broadcastDispatcher: BroadcastDispatcher + private val broadcastDispatcher: BroadcastDispatcher, + private val dreamManager: IDreamManager, ) : ComponentActivity() { private lateinit var parent: ViewGroup private lateinit var broadcastReceiver: BroadcastReceiver + private var mExitToDream: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -81,17 +85,36 @@ class ControlsActivity @Inject constructor( parent = requireViewById<ViewGroup>(R.id.global_actions_controls) parent.alpha = 0f - uiController.show(parent, { finish() }, this) + uiController.show(parent, { finishOrReturnToDream() }, this) ControlsAnimations.enterAnimation(parent).start() } - override fun onBackPressed() { + override fun onResume() { + super.onResume() + mExitToDream = intent.getBooleanExtra(ControlsUiController.EXIT_TO_DREAM, false) + } + + fun finishOrReturnToDream() { + if (mExitToDream) { + try { + mExitToDream = false + dreamManager.dream() + return + } catch (e: RemoteException) { + // Fall through + } + } finish() } + override fun onBackPressed() { + finishOrReturnToDream() + } + override fun onStop() { super.onStop() + mExitToDream = false uiController.hide() } @@ -106,7 +129,8 @@ class ControlsActivity @Inject constructor( broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action = intent.getAction() - if (Intent.ACTION_SCREEN_OFF.equals(action)) { + if (action == Intent.ACTION_SCREEN_OFF || + action == Intent.ACTION_DREAMING_STARTED) { finish() } } @@ -114,6 +138,7 @@ class ControlsActivity @Inject constructor( val filter = IntentFilter() filter.addAction(Intent.ACTION_SCREEN_OFF) + filter.addAction(Intent.ACTION_DREAMING_STARTED) broadcastDispatcher.registerReceiver(broadcastReceiver, filter) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index 822f8f2e6191..c1cfbcb0c211 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -27,6 +27,7 @@ interface ControlsUiController { companion object { public const val TAG = "ControlsUiController" public const val EXTRA_ANIMATE = "extra_animate" + public const val EXIT_TO_DREAM = "extra_exit_to_dream" } fun show(parent: ViewGroup, onDismiss: Runnable, activityContext: Context) diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index dc3dadb32669..7e31626983e7 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -23,7 +23,8 @@ import android.service.dreams.IDreamManager; import androidx.annotation.Nullable; import com.android.internal.statusbar.IStatusBarService; -import com.android.keyguard.clock.ClockModule; +import com.android.keyguard.clock.ClockInfoModule; +import com.android.keyguard.dagger.ClockRegistryModule; import com.android.keyguard.dagger.KeyguardBouncerComponent; import com.android.systemui.BootCompleteCache; import com.android.systemui.BootCompleteCacheImpl; @@ -33,6 +34,7 @@ import com.android.systemui.biometrics.AlternateUdfpsTouchProvider; import com.android.systemui.biometrics.UdfpsDisplayModeProvider; import com.android.systemui.biometrics.dagger.BiometricsModule; import com.android.systemui.classifier.FalsingModule; +import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule; import com.android.systemui.controls.dagger.ControlsModule; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.dagger.DemoModeModule; @@ -118,7 +120,9 @@ import dagger.Provides; AssistModule.class, BiometricsModule.class, BouncerViewModule.class, - ClockModule.class, + ClipboardOverlayModule.class, + ClockInfoModule.class, + ClockRegistryModule.class, CoroutinesModule.class, DreamModule.class, ControlsModule.class, @@ -165,12 +169,16 @@ public abstract class SystemUIModule { @Binds abstract BootCompleteCache bindBootCompleteCache(BootCompleteCacheImpl bootCompleteCache); - /** */ + /** + * + */ @Binds public abstract ContextComponentHelper bindComponentHelper( ContextComponentResolver componentHelper); - /** */ + /** + * + */ @Binds public abstract NotificationRowBinder bindNotificationRowBinder( NotificationRowBinderImpl notificationRowBinder); @@ -209,6 +217,7 @@ public abstract class SystemUIModule { abstract SystemClock bindSystemClock(SystemClockImpl systemClock); // TODO: This should provided by the WM component + /** Provides Optional of BubbleManager */ @SysUISingleton @Provides diff --git a/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt index 991b54e8035e..ded0fb79cd6f 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt @@ -59,7 +59,7 @@ class CutoutDecorProviderImpl( (view as? DisplayCutoutView)?.let { cutoutView -> cutoutView.setColor(tintColor) cutoutView.updateRotation(rotation) - cutoutView.onDisplayChanged(displayUniqueId) + cutoutView.updateConfiguration(displayUniqueId) } } } diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt index ec0013bb5000..976afd457f79 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt @@ -34,8 +34,6 @@ import com.android.systemui.FaceScanningOverlay import com.android.systemui.biometrics.AuthController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.plugins.statusbar.StatusBarStateController import java.util.concurrent.Executor import javax.inject.Inject @@ -47,15 +45,13 @@ class FaceScanningProviderFactory @Inject constructor( private val statusBarStateController: StatusBarStateController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, @Main private val mainExecutor: Executor, - private val featureFlags: FeatureFlags ) : DecorProviderFactory() { private val display = context.display private val displayInfo = DisplayInfo() override val hasProviders: Boolean get() { - if (!featureFlags.isEnabled(Flags.FACE_SCANNING_ANIM) || - authController.faceSensorLocation == null) { + if (authController.faceSensorLocation == null) { return false } @@ -99,7 +95,7 @@ class FaceScanningProviderFactory @Inject constructor( } fun shouldShowFaceScanningAnim(): Boolean { - return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceScanning + return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceDetectionRunning } } @@ -124,7 +120,7 @@ class FaceScanningOverlayProviderImpl( view.layoutParams = it (view as? FaceScanningOverlay)?.let { overlay -> overlay.setColor(tintColor) - overlay.onDisplayChanged(displayUniqueId) + overlay.updateConfiguration(displayUniqueId) } } } diff --git a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt index a25286438387..8b4aeefb6ed4 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt @@ -78,23 +78,18 @@ class RoundedCornerResDelegate( reloadMeasures() } - private fun reloadAll(newReloadToken: Int) { - if (reloadToken == newReloadToken) { - return - } - reloadToken = newReloadToken - reloadRes() - reloadMeasures() - } - fun updateDisplayUniqueId(newDisplayUniqueId: String?, newReloadToken: Int?) { if (displayUniqueId != newDisplayUniqueId) { displayUniqueId = newDisplayUniqueId newReloadToken ?.let { reloadToken = it } reloadRes() reloadMeasures() - } else { - newReloadToken?.let { reloadAll(it) } + } else if (newReloadToken != null) { + if (reloadToken == newReloadToken) { + return + } + reloadToken = newReloadToken + reloadMeasures() } } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java index 2e51b51d2836..b69afeb37371 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java @@ -287,8 +287,8 @@ public class DozeLog implements Dumpable { /** * Appends sensor event dropped event to logs */ - public void traceSensorEventDropped(int sensorEvent, String reason) { - mLogger.logSensorEventDropped(sensorEvent, reason); + public void traceSensorEventDropped(@Reason int pulseReason, String reason) { + mLogger.logSensorEventDropped(pulseReason, reason); } /** @@ -386,6 +386,47 @@ public class DozeLog implements Dumpable { mLogger.logSetAodDimmingScrim((long) scrimOpacity); } + /** + * Appends sensor attempted to register and whether it was a successful registration. + */ + public void traceSensorRegisterAttempt(String sensorName, boolean successfulRegistration) { + mLogger.logSensorRegisterAttempt(sensorName, successfulRegistration); + } + + /** + * Appends sensor attempted to unregister and whether it was successfully unregistered. + */ + public void traceSensorUnregisterAttempt(String sensorInfo, boolean successfullyUnregistered) { + mLogger.logSensorUnregisterAttempt(sensorInfo, successfullyUnregistered); + } + + /** + * Appends sensor attempted to unregister and whether it was successfully unregistered + * with a reason the sensor is being unregistered. + */ + public void traceSensorUnregisterAttempt(String sensorInfo, boolean successfullyUnregistered, + String reason) { + mLogger.logSensorUnregisterAttempt(sensorInfo, successfullyUnregistered, reason); + } + + /** + * Appends the event of skipping a sensor registration since it's already registered. + */ + public void traceSkipRegisterSensor(String sensorInfo) { + mLogger.logSkipSensorRegistration(sensorInfo); + } + + /** + * Appends a plugin sensor was registered or unregistered event. + */ + public void tracePluginSensorUpdate(boolean registered) { + if (registered) { + mLogger.log("register plugin sensor"); + } else { + mLogger.log("unregister plugin sensor"); + } + } + private class SummaryStats { private int mCount; diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt index cc5766210406..18c8e01cbf76 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt @@ -19,12 +19,13 @@ package com.android.systemui.doze import android.view.Display import com.android.systemui.doze.DozeLog.Reason import com.android.systemui.doze.DozeLog.reasonToString -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.log.dagger.DozeLog import com.android.systemui.statusbar.policy.DevicePostureController +import com.google.errorprone.annotations.CompileTimeConstant import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -224,10 +225,14 @@ class DozeLogger @Inject constructor( }) } - fun logPulseDropped(from: String, state: DozeMachine.State) { + /** + * Log why a pulse was dropped and the current doze machine state. The state can be null + * if the DozeMachine is the middle of transitioning between states. + */ + fun logPulseDropped(from: String, state: DozeMachine.State?) { buffer.log(TAG, INFO, { str1 = from - str2 = state.name + str2 = state?.name }, { "Pulse dropped, cannot pulse from=$str1 state=$str2" }) @@ -320,6 +325,50 @@ class DozeLogger @Inject constructor( "Doze car mode started" }) } + + fun logSensorRegisterAttempt(sensorInfo: String, successfulRegistration: Boolean) { + buffer.log(TAG, INFO, { + str1 = sensorInfo + bool1 = successfulRegistration + }, { + "Register sensor. Success=$bool1 sensor=$str1" + }) + } + + fun logSensorUnregisterAttempt(sensorInfo: String, successfulUnregister: Boolean) { + buffer.log(TAG, INFO, { + str1 = sensorInfo + bool1 = successfulUnregister + }, { + "Unregister sensor. Success=$bool1 sensor=$str1" + }) + } + + fun logSensorUnregisterAttempt( + sensorInfo: String, + successfulUnregister: Boolean, + reason: String + ) { + buffer.log(TAG, INFO, { + str1 = sensorInfo + bool1 = successfulUnregister + str2 = reason + }, { + "Unregister sensor. reason=$str2. Success=$bool1 sensor=$str1" + }) + } + + fun logSkipSensorRegistration(sensor: String) { + buffer.log(TAG, DEBUG, { + str1 = sensor + }, { + "Skipping sensor registration because its already registered. sensor=$str1" + }) + } + + fun log(@CompileTimeConstant msg: String) { + buffer.log(TAG, DEBUG, msg) + } } private const val TAG = "DozeLog" diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java index ae412152b10e..96c35d43052e 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java @@ -20,7 +20,6 @@ import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWA import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING; import android.annotation.MainThread; -import android.app.UiModeManager; import android.content.res.Configuration; import android.hardware.display.AmbientDisplayConfiguration; import android.os.Trace; @@ -145,10 +144,9 @@ public class DozeMachine { private final Service mDozeService; private final WakeLock mWakeLock; - private final AmbientDisplayConfiguration mConfig; + private final AmbientDisplayConfiguration mAmbientDisplayConfig; private final WakefulnessLifecycle mWakefulnessLifecycle; private final DozeHost mDozeHost; - private final UiModeManager mUiModeManager; private final DockManager mDockManager; private final Part[] mParts; @@ -156,18 +154,18 @@ public class DozeMachine { private State mState = State.UNINITIALIZED; private int mPulseReason; private boolean mWakeLockHeldForCurrentState = false; + private int mUiModeType = Configuration.UI_MODE_TYPE_NORMAL; @Inject - public DozeMachine(@WrappedService Service service, AmbientDisplayConfiguration config, + public DozeMachine(@WrappedService Service service, + AmbientDisplayConfiguration ambientDisplayConfig, WakeLock wakeLock, WakefulnessLifecycle wakefulnessLifecycle, - UiModeManager uiModeManager, DozeLog dozeLog, DockManager dockManager, DozeHost dozeHost, Part[] parts) { mDozeService = service; - mConfig = config; + mAmbientDisplayConfig = ambientDisplayConfig; mWakefulnessLifecycle = wakefulnessLifecycle; mWakeLock = wakeLock; - mUiModeManager = uiModeManager; mDozeLog = dozeLog; mDockManager = dockManager; mDozeHost = dozeHost; @@ -187,6 +185,18 @@ public class DozeMachine { } /** + * Notifies the {@link DozeMachine} that {@link Configuration} has changed. + */ + public void onConfigurationChanged(Configuration newConfiguration) { + int newUiModeType = newConfiguration.uiMode & Configuration.UI_MODE_TYPE_MASK; + if (mUiModeType == newUiModeType) return; + mUiModeType = newUiModeType; + for (Part part : mParts) { + part.onUiModeTypeChanged(mUiModeType); + } + } + + /** * Requests transitioning to {@code requestedState}. * * This can be called during a state transition, in which case it will be queued until all @@ -211,6 +221,14 @@ public class DozeMachine { requestState(State.DOZE_REQUEST_PULSE, pulseReason); } + /** + * @return true if {@link DozeMachine} is currently in either {@link State#UNINITIALIZED} + * or {@link State#FINISH} + */ + public boolean isUninitializedOrFinished() { + return mState == State.UNINITIALIZED || mState == State.FINISH; + } + void onScreenState(int state) { mDozeLog.traceDisplayState(state); for (Part part : mParts) { @@ -360,7 +378,7 @@ public class DozeMachine { if (mState == State.FINISH) { return State.FINISH; } - if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR + if (mUiModeType == Configuration.UI_MODE_TYPE_CAR && (requestedState.canPulse() || requestedState.staysAwake())) { Log.i(TAG, "Doze is suppressed with all triggers disabled as car mode is active"); mDozeLog.traceCarModeStarted(); @@ -411,7 +429,7 @@ public class DozeMachine { nextState = State.FINISH; } else if (mDockManager.isDocked()) { nextState = mDockManager.isHidden() ? State.DOZE : State.DOZE_AOD_DOCKED; - } else if (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) { + } else if (mAmbientDisplayConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) { nextState = State.DOZE_AOD; } else { nextState = State.DOZE; @@ -427,6 +445,7 @@ public class DozeMachine { /** Dumps the current state */ public void dump(PrintWriter pw) { pw.print(" state="); pw.println(mState); + pw.print(" mUiModeType="); pw.println(mUiModeType); pw.print(" wakeLockHeldForCurrentState="); pw.println(mWakeLockHeldForCurrentState); pw.print(" wakeLock="); pw.println(mWakeLock); pw.println("Parts:"); @@ -459,6 +478,19 @@ public class DozeMachine { /** Sets the {@link DozeMachine} when this Part is associated with one. */ default void setDozeMachine(DozeMachine dozeMachine) {} + + /** + * Notifies the Part about a change in {@link Configuration#uiMode}. + * + * @param newUiModeType {@link Configuration#UI_MODE_TYPE_NORMAL}, + * {@link Configuration#UI_MODE_TYPE_DESK}, + * {@link Configuration#UI_MODE_TYPE_CAR}, + * {@link Configuration#UI_MODE_TYPE_TELEVISION}, + * {@link Configuration#UI_MODE_TYPE_APPLIANCE}, + * {@link Configuration#UI_MODE_TYPE_WATCH}, + * or {@link Configuration#UI_MODE_TYPE_VR_HEADSET} + */ + default void onUiModeTypeChanged(int newUiModeType) {} } /** A wrapper interface for {@link android.service.dreams.DreamService} */ diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index da6c163b1eea..9a091e725818 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -23,7 +23,6 @@ import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_ import android.annotation.AnyThread; import android.app.ActivityManager; -import android.content.Context; import android.database.ContentObserver; import android.hardware.Sensor; import android.hardware.SensorManager; @@ -37,7 +36,6 @@ import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.IndentingPrintWriter; -import android.util.Log; import android.view.Display; import androidx.annotation.NonNull; @@ -88,12 +86,9 @@ import java.util.function.Consumer; * trigger callbacks on the provided {@link mProxCallback}. */ public class DozeSensors { - - private static final boolean DEBUG = DozeService.DEBUG; private static final String TAG = "DozeSensors"; private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); - private final Context mContext; private final AsyncSensorManager mSensorManager; private final AmbientDisplayConfiguration mConfig; private final WakeLock mWakeLock; @@ -144,7 +139,6 @@ public class DozeSensors { } DozeSensors( - Context context, AsyncSensorManager sensorManager, DozeParameters dozeParameters, AmbientDisplayConfiguration config, @@ -157,7 +151,6 @@ public class DozeSensors { AuthController authController, DevicePostureController devicePostureController ) { - mContext = context; mSensorManager = sensorManager; mConfig = config; mWakeLock = wakeLock; @@ -605,10 +598,7 @@ public class DozeSensors { // cancel the previous sensor: if (mRegistered) { final boolean rt = mSensorManager.cancelTriggerSensor(this, oldSensor); - if (DEBUG) { - Log.d(TAG, "posture changed, cancelTriggerSensor[" + oldSensor + "] " - + rt); - } + mDozeLog.traceSensorUnregisterAttempt(oldSensor.toString(), rt, "posture changed"); mRegistered = false; } @@ -654,19 +644,13 @@ public class DozeSensors { if (mRequested && !mDisabled && (enabledBySetting() || mIgnoresSetting)) { if (!mRegistered) { mRegistered = mSensorManager.requestTriggerSensor(this, sensor); - if (DEBUG) { - Log.d(TAG, "requestTriggerSensor[" + sensor + "] " + mRegistered); - } + mDozeLog.traceSensorRegisterAttempt(sensor.toString(), mRegistered); } else { - if (DEBUG) { - Log.d(TAG, "requestTriggerSensor[" + sensor + "] already registered"); - } + mDozeLog.traceSkipRegisterSensor(sensor.toString()); } } else if (mRegistered) { final boolean rt = mSensorManager.cancelTriggerSensor(this, sensor); - if (DEBUG) { - Log.d(TAG, "cancelTriggerSensor[" + sensor + "] " + rt); - } + mDozeLog.traceSensorUnregisterAttempt(sensor.toString(), rt); mRegistered = false; } } @@ -704,7 +688,6 @@ public class DozeSensors { final Sensor sensor = mSensors[mPosture]; mDozeLog.traceSensor(mPulseReason); mHandler.post(mWakeLock.wrap(() -> { - if (DEBUG) Log.d(TAG, "onTrigger: " + triggerEventToString(event)); if (sensor != null && sensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) { UI_EVENT_LOGGER.log(DozeSensorsUiEvent.ACTION_AMBIENT_GESTURE_PICKUP); } @@ -776,11 +759,11 @@ public class DozeSensors { && !mRegistered) { asyncSensorManager.registerPluginListener(mPluginSensor, this); mRegistered = true; - if (DEBUG) Log.d(TAG, "registerPluginListener"); + mDozeLog.tracePluginSensorUpdate(true /* registered */); } else if (mRegistered) { asyncSensorManager.unregisterPluginListener(mPluginSensor, this); mRegistered = false; - if (DEBUG) Log.d(TAG, "unregisterPluginListener"); + mDozeLog.tracePluginSensorUpdate(false /* registered */); } } @@ -813,10 +796,9 @@ public class DozeSensors { mHandler.post(mWakeLock.wrap(() -> { final long now = SystemClock.uptimeMillis(); if (now < mDebounceFrom + mDebounce) { - Log.d(TAG, "onSensorEvent dropped: " + triggerEventToString(event)); + mDozeLog.traceSensorEventDropped(mPulseReason, "debounce"); return; } - if (DEBUG) Log.d(TAG, "onSensorEvent: " + triggerEventToString(event)); mSensorCallback.onSensorPulse(mPulseReason, -1, -1, event.getValues()); })); } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java index a2eb4e3bb640..e8d7e4642e3e 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java @@ -17,6 +17,7 @@ package com.android.systemui.doze; import android.content.Context; +import android.content.res.Configuration; import android.os.PowerManager; import android.os.SystemClock; import android.service.dreams.DreamService; @@ -59,6 +60,7 @@ public class DozeService extends DreamService mPluginManager.addPluginListener(this, DozeServicePlugin.class, false /* allowMultiple */); DozeComponent dozeComponent = mDozeComponentBuilder.build(this); mDozeMachine = dozeComponent.getDozeMachine(); + mDozeMachine.onConfigurationChanged(getResources().getConfiguration()); } @Override @@ -127,6 +129,12 @@ public class DozeService extends DreamService } @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDozeMachine.onConfigurationChanged(newConfig); + } + + @Override public void onRequestHideDoze() { if (mDozeMachine != null) { mDozeMachine.requestState(DozeMachine.State.DOZE); diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java index 7ed4b35e1ee7..e6d98655b119 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java @@ -16,21 +16,13 @@ package com.android.systemui.doze; -import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE; -import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE; - -import android.app.UiModeManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; +import static android.content.res.Configuration.UI_MODE_TYPE_CAR; + import android.hardware.display.AmbientDisplayConfiguration; import android.os.PowerManager; import android.os.UserHandle; import android.text.TextUtils; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.doze.dagger.DozeScope; import com.android.systemui.statusbar.phone.BiometricUnlockController; @@ -43,7 +35,9 @@ import dagger.Lazy; /** * Handles suppressing doze on: * 1. INITIALIZED, don't allow dozing at all when: - * - in CAR_MODE + * - in CAR_MODE, in this scenario the device is asleep and won't listen for any triggers + * to wake up. In this state, no UI shows. Unlike other conditions, this suppression is only + * temporary and stops when the device exits CAR_MODE * - device is NOT provisioned * - there's a pending authentication * 2. PowerSaveMode active @@ -57,35 +51,47 @@ import dagger.Lazy; */ @DozeScope public class DozeSuppressor implements DozeMachine.Part { - private static final String TAG = "DozeSuppressor"; private DozeMachine mMachine; private final DozeHost mDozeHost; private final AmbientDisplayConfiguration mConfig; private final DozeLog mDozeLog; - private final BroadcastDispatcher mBroadcastDispatcher; - private final UiModeManager mUiModeManager; private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy; - private boolean mBroadcastReceiverRegistered; + private boolean mIsCarModeEnabled = false; @Inject public DozeSuppressor( DozeHost dozeHost, AmbientDisplayConfiguration config, DozeLog dozeLog, - BroadcastDispatcher broadcastDispatcher, - UiModeManager uiModeManager, Lazy<BiometricUnlockController> biometricUnlockControllerLazy) { mDozeHost = dozeHost; mConfig = config; mDozeLog = dozeLog; - mBroadcastDispatcher = broadcastDispatcher; - mUiModeManager = uiModeManager; mBiometricUnlockControllerLazy = biometricUnlockControllerLazy; } @Override + public void onUiModeTypeChanged(int newUiModeType) { + boolean isCarModeEnabled = newUiModeType == UI_MODE_TYPE_CAR; + if (mIsCarModeEnabled == isCarModeEnabled) { + return; + } + mIsCarModeEnabled = isCarModeEnabled; + // Do not handle the event if doze machine is not initialized yet. + // It will be handled upon initialization. + if (mMachine.isUninitializedOrFinished()) { + return; + } + if (mIsCarModeEnabled) { + handleCarModeStarted(); + } else { + handleCarModeExited(); + } + } + + @Override public void setDozeMachine(DozeMachine dozeMachine) { mMachine = dozeMachine; } @@ -94,7 +100,6 @@ public class DozeSuppressor implements DozeMachine.Part { public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) { switch (newState) { case INITIALIZED: - registerBroadcastReceiver(); mDozeHost.addCallback(mHostCallback); checkShouldImmediatelyEndDoze(); checkShouldImmediatelySuspendDoze(); @@ -108,14 +113,12 @@ public class DozeSuppressor implements DozeMachine.Part { @Override public void destroy() { - unregisterBroadcastReceiver(); mDozeHost.removeCallback(mHostCallback); } private void checkShouldImmediatelySuspendDoze() { - if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) { - mDozeLog.traceCarModeStarted(); - mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS); + if (mIsCarModeEnabled) { + handleCarModeStarted(); } } @@ -135,7 +138,7 @@ public class DozeSuppressor implements DozeMachine.Part { @Override public void dump(PrintWriter pw) { - pw.println(" uiMode=" + mUiModeManager.getCurrentModeType()); + pw.println(" isCarModeEnabled=" + mIsCarModeEnabled); pw.println(" hasPendingAuth=" + mBiometricUnlockControllerLazy.get().hasPendingAuthentication()); pw.println(" isProvisioned=" + mDozeHost.isProvisioned()); @@ -143,40 +146,18 @@ public class DozeSuppressor implements DozeMachine.Part { pw.println(" aodPowerSaveActive=" + mDozeHost.isPowerSaveActive()); } - private void registerBroadcastReceiver() { - if (mBroadcastReceiverRegistered) { - return; - } - IntentFilter filter = new IntentFilter(ACTION_ENTER_CAR_MODE); - filter.addAction(ACTION_EXIT_CAR_MODE); - mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter); - mBroadcastReceiverRegistered = true; + private void handleCarModeExited() { + mDozeLog.traceCarModeEnded(); + mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) + ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE); } - private void unregisterBroadcastReceiver() { - if (!mBroadcastReceiverRegistered) { - return; - } - mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver); - mBroadcastReceiverRegistered = false; + private void handleCarModeStarted() { + mDozeLog.traceCarModeStarted(); + mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS); } - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (ACTION_ENTER_CAR_MODE.equals(action)) { - mDozeLog.traceCarModeStarted(); - mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS); - } else if (ACTION_EXIT_CAR_MODE.equals(action)) { - mDozeLog.traceCarModeEnded(); - mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) - ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE); - } - } - }; - - private DozeHost.Callback mHostCallback = new DozeHost.Callback() { + private final DozeHost.Callback mHostCallback = new DozeHost.Callback() { @Override public void onPowerSaveChanged(boolean active) { // handles suppression changes, while DozeMachine#transitionPolicy handles gating diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index ef454ffbdeb1..32cb1c01b776 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -198,7 +198,7 @@ public class DozeTriggers implements DozeMachine.Part { mAllowPulseTriggers = true; mSessionTracker = sessionTracker; - mDozeSensors = new DozeSensors(context, mSensorManager, dozeParameters, + mDozeSensors = new DozeSensors(mSensorManager, dozeParameters, config, wakeLock, this::onSensor, this::onProximityFar, dozeLog, proximitySensor, secureSettings, authController, devicePostureController); mDockManager = dockManager; @@ -536,13 +536,13 @@ public class DozeTriggers implements DozeMachine.Part { return; } - if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse()) { + if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse(dozeState)) { if (!mAllowPulseTriggers) { mDozeLog.tracePulseDropped("requestPulse - !mAllowPulseTriggers"); } else if (mDozeHost.isPulsePending()) { mDozeLog.tracePulseDropped("requestPulse - pulsePending"); - } else if (!canPulse()) { - mDozeLog.tracePulseDropped("requestPulse", dozeState); + } else if (!canPulse(dozeState)) { + mDozeLog.tracePulseDropped("requestPulse - dozeState cannot pulse", dozeState); } runIfNotNull(onPulseSuppressedListener); return; @@ -559,15 +559,16 @@ public class DozeTriggers implements DozeMachine.Part { // not in pocket, continue pulsing final boolean isPulsePending = mDozeHost.isPulsePending(); mDozeHost.setPulsePending(false); - if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse()) { + if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse(dozeState)) { if (!isPulsePending) { mDozeLog.tracePulseDropped("continuePulseRequest - pulse no longer" + " pending, pulse was cancelled before it could start" + " transitioning to pulsing state."); } else if (mDozeHost.isPulsingBlocked()) { mDozeLog.tracePulseDropped("continuePulseRequest - pulsingBlocked"); - } else if (!canPulse()) { - mDozeLog.tracePulseDropped("continuePulseRequest", mMachine.getState()); + } else if (!canPulse(dozeState)) { + mDozeLog.tracePulseDropped("continuePulseRequest" + + " - doze state cannot pulse", dozeState); } runIfNotNull(onPulseSuppressedListener); return; @@ -582,10 +583,10 @@ public class DozeTriggers implements DozeMachine.Part { .ifPresent(uiEventEnum -> mUiEventLogger.log(uiEventEnum, getKeyguardSessionId())); } - private boolean canPulse() { - return mMachine.getState() == DozeMachine.State.DOZE - || mMachine.getState() == DozeMachine.State.DOZE_AOD - || mMachine.getState() == DozeMachine.State.DOZE_AOD_DOCKED; + private boolean canPulse(DozeMachine.State dozeState) { + return dozeState == DozeMachine.State.DOZE + || dozeState == DozeMachine.State.DOZE_AOD + || dozeState == DozeMachine.State.DOZE_AOD_DOCKED; } @Nullable diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java index 0ccb222c8acc..cedd850ac2ef 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java @@ -210,7 +210,8 @@ public class DreamHomeControlsComplication implements Complication { final Intent intent = new Intent(mContext, ControlsActivity.class) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(ControlsUiController.EXTRA_ANIMATE, true); + .putExtra(ControlsUiController.EXTRA_ANIMATE, true) + .putExtra(ControlsUiController.EXIT_TO_DREAM, true); final ActivityLaunchAnimator.Controller controller = v != null ? ActivityLaunchAnimator.Controller.fromView(v, null /* cujType */) diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java index c07d4022df76..1166c2fc1120 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java @@ -28,7 +28,7 @@ import com.android.systemui.ActivityIntentHelper; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.media.MediaCarouselController; +import com.android.systemui.media.controls.ui.MediaCarouselController; import com.android.systemui.media.dream.MediaDreamComplication; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.NotificationLockscreenUserManager; diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt index 08ef8f3d025f..609bd76cf210 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt @@ -24,8 +24,13 @@ import com.android.systemui.R import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL -import com.android.systemui.log.LogBuffer +import com.android.systemui.dump.nano.SystemUIProtoDump +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager +import com.google.protobuf.nano.MessageNano +import java.io.BufferedOutputStream +import java.io.FileDescriptor +import java.io.FileOutputStream import java.io.PrintWriter import javax.inject.Inject import javax.inject.Provider @@ -100,7 +105,7 @@ class DumpHandler @Inject constructor( /** * Dump the diagnostics! Behavior can be controlled via [args]. */ - fun dump(pw: PrintWriter, args: Array<String>) { + fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { Trace.beginSection("DumpManager#dump()") val start = SystemClock.uptimeMillis() @@ -111,10 +116,12 @@ class DumpHandler @Inject constructor( return } - when (parsedArgs.dumpPriority) { - PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs) - PRIORITY_ARG_NORMAL -> dumpNormal(pw, parsedArgs) - else -> dumpParameterized(pw, parsedArgs) + when { + parsedArgs.dumpPriority == PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs) + parsedArgs.dumpPriority == PRIORITY_ARG_NORMAL && !parsedArgs.proto -> { + dumpNormal(pw, parsedArgs) + } + else -> dumpParameterized(fd, pw, parsedArgs) } pw.println() @@ -122,7 +129,7 @@ class DumpHandler @Inject constructor( Trace.endSection() } - private fun dumpParameterized(pw: PrintWriter, args: ParsedArgs) { + private fun dumpParameterized(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) { when (args.command) { "bugreport-critical" -> dumpCritical(pw, args) "bugreport-normal" -> dumpNormal(pw, args) @@ -130,7 +137,13 @@ class DumpHandler @Inject constructor( "buffers" -> dumpBuffers(pw, args) "config" -> dumpConfig(pw) "help" -> dumpHelp(pw) - else -> dumpTargets(args.nonFlagArgs, pw, args) + else -> { + if (args.proto) { + dumpProtoTargets(args.nonFlagArgs, fd, args) + } else { + dumpTargets(args.nonFlagArgs, pw, args) + } + } } } @@ -160,6 +173,26 @@ class DumpHandler @Inject constructor( } } + private fun dumpProtoTargets( + targets: List<String>, + fd: FileDescriptor, + args: ParsedArgs + ) { + val systemUIProto = SystemUIProtoDump() + if (targets.isNotEmpty()) { + for (target in targets) { + dumpManager.dumpProtoTarget(target, systemUIProto, args.rawArgs) + } + } else { + dumpManager.dumpProtoDumpables(systemUIProto, args.rawArgs) + } + val buffer = BufferedOutputStream(FileOutputStream(fd)) + buffer.use { + it.write(MessageNano.toByteArray(systemUIProto)) + it.flush() + } + } + private fun dumpTargets( targets: List<String>, pw: PrintWriter, @@ -235,6 +268,7 @@ class DumpHandler @Inject constructor( pw.println("$ <invocation> buffers") pw.println("$ <invocation> bugreport-critical") pw.println("$ <invocation> bugreport-normal") + pw.println("$ <invocation> config") pw.println() pw.println("Targets can be listed:") @@ -266,6 +300,7 @@ class DumpHandler @Inject constructor( } } } + PROTO -> pArgs.proto = true "-t", "--tail" -> { pArgs.tailLength = readArgument(iterator, arg) { it.toInt() @@ -277,6 +312,9 @@ class DumpHandler @Inject constructor( "-h", "--help" -> { pArgs.command = "help" } + // This flag is passed as part of the proto dump in Bug reports, we can ignore + // it because this is our default behavior. + "-a" -> {} else -> { throw ArgParseException("Unknown flag: $arg") } @@ -313,13 +351,21 @@ class DumpHandler @Inject constructor( const val PRIORITY_ARG_CRITICAL = "CRITICAL" const val PRIORITY_ARG_HIGH = "HIGH" const val PRIORITY_ARG_NORMAL = "NORMAL" + const val PROTO = "--proto" } } private val PRIORITY_OPTIONS = arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_HIGH, PRIORITY_ARG_NORMAL) -private val COMMANDS = arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables") +private val COMMANDS = arrayOf( + "bugreport-critical", + "bugreport-normal", + "buffers", + "dumpables", + "config", + "help" +) private class ParsedArgs( val rawArgs: Array<String>, @@ -329,6 +375,7 @@ private class ParsedArgs( var tailLength: Int = 0 var command: String? = null var listOnly = false + var proto = false } class ArgParseException(message: String) : Exception(message) diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt index cca04da8f426..ae780896a7e2 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt @@ -18,7 +18,9 @@ package com.android.systemui.dump import android.util.ArrayMap import com.android.systemui.Dumpable -import com.android.systemui.log.LogBuffer +import com.android.systemui.ProtoDumpable +import com.android.systemui.dump.nano.SystemUIProtoDump +import com.android.systemui.plugins.log.LogBuffer import java.io.PrintWriter import javax.inject.Inject import javax.inject.Singleton @@ -90,7 +92,7 @@ open class DumpManager @Inject constructor() { target: String, pw: PrintWriter, args: Array<String>, - tailLength: Int + tailLength: Int, ) { for (dumpable in dumpables.values) { if (dumpable.name.endsWith(target)) { @@ -107,6 +109,36 @@ open class DumpManager @Inject constructor() { } } + @Synchronized + fun dumpProtoTarget( + target: String, + protoDump: SystemUIProtoDump, + args: Array<String> + ) { + for (dumpable in dumpables.values) { + if (dumpable.dumpable is ProtoDumpable && dumpable.name.endsWith(target)) { + dumpProtoDumpable(dumpable.dumpable, protoDump, args) + return + } + } + } + + @Synchronized + fun dumpProtoDumpables( + systemUIProtoDump: SystemUIProtoDump, + args: Array<String> + ) { + for (dumpable in dumpables.values) { + if (dumpable.dumpable is ProtoDumpable) { + dumpProtoDumpable( + dumpable.dumpable, + systemUIProtoDump, + args + ) + } + } + } + /** * Dumps all registered dumpables to [pw] */ @@ -184,6 +216,14 @@ open class DumpManager @Inject constructor() { buffer.dumpable.dump(pw, tailLength) } + private fun dumpProtoDumpable( + protoDumpable: ProtoDumpable, + systemUIProtoDump: SystemUIProtoDump, + args: Array<String> + ) { + protoDumpable.dumpProto(systemUIProtoDump, args) + } + private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean { val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable return existingDumpable == null || newDumpable == existingDumpable @@ -195,4 +235,4 @@ private data class RegisteredDumpable<T>( val dumpable: T ) -private const val TAG = "DumpManager"
\ No newline at end of file +private const val TAG = "DumpManager" diff --git a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt index 0eab1afc4119..8299b13d305f 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt @@ -19,7 +19,7 @@ package com.android.systemui.dump import android.content.Context import android.util.Log import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.util.io.Files import com.android.systemui.util.time.SystemClock import java.io.IOException diff --git a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java index 0a41a56b5ecb..da983ab03a1d 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java +++ b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java @@ -51,6 +51,7 @@ public class SystemUIAuxiliaryDumpService extends Service { protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { // Simulate the NORMAL priority arg being passed to us mDumpHandler.dump( + fd, pw, new String[] { DumpHandler.PRIORITY_ARG, DumpHandler.PRIORITY_ARG_NORMAL }); } diff --git a/packages/SystemUI/src/com/android/systemui/dump/sysui.proto b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto new file mode 100644 index 000000000000..cd8c08aeb2dc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto @@ -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. + */ +syntax = "proto3"; + +package com.android.systemui.dump; + +import "frameworks/base/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto"; + +option java_multiple_files = true; + +message SystemUIProtoDump { + repeated com.android.systemui.qs.QsTileState tiles = 1; +} + diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java deleted file mode 100644 index d89451992351..000000000000 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.flags; - -import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER; - -import com.android.internal.annotations.Keep; -import com.android.systemui.R; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * List of {@link Flag} objects for use in SystemUI. - * - * Flag Ids are integers. - * Ids must be unique. This is enforced in a unit test. - * Ids need not be sequential. Flags can "claim" a chunk of ids for flags in related features with - * a comment. This is purely for organizational purposes. - * - * On public release builds, flags will always return their default value. There is no way to - * change their value on release builds. - * - * See {@link FeatureFlagsDebug} for instructions on flipping the flags via adb. - */ -public class Flags { - public static final UnreleasedFlag TEAMFOOD = new UnreleasedFlag(1); - - /***************************************/ - // 100 - notification - public static final UnreleasedFlag NOTIFICATION_PIPELINE_DEVELOPER_LOGGING = - new UnreleasedFlag(103); - - public static final UnreleasedFlag NSSL_DEBUG_LINES = - new UnreleasedFlag(105); - - public static final UnreleasedFlag NSSL_DEBUG_REMOVE_ANIMATION = - new UnreleasedFlag(106); - - public static final UnreleasedFlag NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE = - new UnreleasedFlag(107); - - public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS = - new ResourceBooleanFlag(108, R.bool.config_notificationToContents); - - public static final ReleasedFlag REMOVE_UNRANKED_NOTIFICATIONS = - new ReleasedFlag(109); - - public static final UnreleasedFlag FSI_REQUIRES_KEYGUARD = - new UnreleasedFlag(110, true); - - public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true); - - public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112, - false); - - public static final UnreleasedFlag NOTIFICATION_DISMISSAL_FADE = new UnreleasedFlag(113, true); - - // next id: 114 - - /***************************************/ - // 200 - keyguard/lockscreen - - // ** Flag retired ** - // public static final BooleanFlag KEYGUARD_LAYOUT = - // new BooleanFlag(200, true); - - public static final ReleasedFlag LOCKSCREEN_ANIMATIONS = - new ReleasedFlag(201); - - public static final ReleasedFlag NEW_UNLOCK_SWIPE_ANIMATION = - new ReleasedFlag(202); - - public static final ResourceBooleanFlag CHARGING_RIPPLE = - new ResourceBooleanFlag(203, R.bool.flag_charging_ripple); - - public static final ResourceBooleanFlag BOUNCER_USER_SWITCHER = - new ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher); - - public static final ResourceBooleanFlag FACE_SCANNING_ANIM = - new ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation); - - public static final UnreleasedFlag LOCKSCREEN_CUSTOM_CLOCKS = new UnreleasedFlag(207); - - /** - * Flag to enable the usage of the new bouncer data source. This is a refactor of and - * eventual replacement of KeyguardBouncer.java. - */ - public static final UnreleasedFlag MODERN_BOUNCER = new UnreleasedFlag(208); - - /** - * Whether the user interactor and repository should use `UserSwitcherController`. - * - * <p>If this is {@code false}, the interactor and repo skip the controller and directly access - * the framework APIs. - */ - public static final UnreleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER = - new UnreleasedFlag(210); - - /** - * Whether `UserSwitcherController` should use the user interactor. - * - * <p>When this is {@code true}, the controller does not directly access framework APIs. - * Instead, it goes through the interactor. - * - * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is - * {@code true} as it would created a cycle between controller -> interactor -> controller. - */ - public static final ReleasedFlag USER_CONTROLLER_USES_INTERACTOR = new ReleasedFlag(211); - - /***************************************/ - // 300 - power menu - public static final ReleasedFlag POWER_MENU_LITE = - new ReleasedFlag(300); - - /***************************************/ - // 400 - smartspace - public static final ReleasedFlag SMARTSPACE_DEDUPING = - new ReleasedFlag(400); - - public static final ReleasedFlag SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED = - new ReleasedFlag(401); - - public static final ResourceBooleanFlag SMARTSPACE = - new ResourceBooleanFlag(402, R.bool.flag_smartspace); - - /***************************************/ - // 500 - quick settings - /** - * @deprecated Not needed anymore - */ - @Deprecated - public static final ReleasedFlag NEW_USER_SWITCHER = - new ReleasedFlag(500); - - public static final UnreleasedFlag COMBINED_QS_HEADERS = - new UnreleasedFlag(501, true); - - public static final ResourceBooleanFlag PEOPLE_TILE = - new ResourceBooleanFlag(502, R.bool.flag_conversations); - - public static final ResourceBooleanFlag QS_USER_DETAIL_SHORTCUT = - new ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut); - - /** - * @deprecated Not needed anymore - */ - @Deprecated - public static final ReleasedFlag NEW_FOOTER = new ReleasedFlag(504); - - public static final UnreleasedFlag NEW_HEADER = new UnreleasedFlag(505, true); - public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER = - new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher); - - public static final ReleasedFlag NEW_FOOTER_ACTIONS = new ReleasedFlag(507); - - /***************************************/ - // 600- status bar - public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER = - new ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip); - - public static final ReleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE = - new ReleasedFlag(603, false); - - public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_BACKEND = - new UnreleasedFlag(604, false); - - public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_FRONTEND = - new UnreleasedFlag(605, false); - - /***************************************/ - // 700 - dialer/calls - public static final ReleasedFlag ONGOING_CALL_STATUS_BAR_CHIP = - new ReleasedFlag(700); - - public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE = - new ReleasedFlag(701); - - public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP = - new ReleasedFlag(702); - - /***************************************/ - // 800 - general visual/theme - public static final ResourceBooleanFlag MONET = - new ResourceBooleanFlag(800, R.bool.flag_monet); - - /***************************************/ - // 801 - region sampling - public static final UnreleasedFlag REGION_SAMPLING = new UnreleasedFlag(801); - - // 802 - wallpaper rendering - public static final UnreleasedFlag USE_CANVAS_RENDERER = new UnreleasedFlag(802, true); - - // 803 - screen contents translation - public static final UnreleasedFlag SCREEN_CONTENTS_TRANSLATION = new UnreleasedFlag(803); - - /***************************************/ - // 900 - media - public static final ReleasedFlag MEDIA_TAP_TO_TRANSFER = new ReleasedFlag(900); - public static final UnreleasedFlag MEDIA_SESSION_ACTIONS = new UnreleasedFlag(901); - public static final ReleasedFlag MEDIA_NEARBY_DEVICES = new ReleasedFlag(903); - public static final ReleasedFlag MEDIA_MUTE_AWAIT = new ReleasedFlag(904); - public static final UnreleasedFlag DREAM_MEDIA_COMPLICATION = new UnreleasedFlag(905); - public static final UnreleasedFlag DREAM_MEDIA_TAP_TO_OPEN = new UnreleasedFlag(906); - public static final UnreleasedFlag UMO_SURFACE_RIPPLE = new UnreleasedFlag(907); - - // 1000 - dock - public static final ReleasedFlag SIMULATE_DOCK_THROUGH_CHARGING = - new ReleasedFlag(1000); - public static final ReleasedFlag DOCK_SETUP_ENABLED = new ReleasedFlag(1001); - - public static final UnreleasedFlag ROUNDED_BOX_RIPPLE = - new UnreleasedFlag(1002, /* teamfood= */ true); - - public static final UnreleasedFlag REFACTORED_DOCK_SETUP = new UnreleasedFlag(1003, true); - - // 1100 - windowing - @Keep - public static final SysPropBooleanFlag WM_ENABLE_SHELL_TRANSITIONS = - new SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false); - - /** - * b/170163464: animate bubbles expanded view collapse with home gesture - */ - @Keep - public static final SysPropBooleanFlag BUBBLES_HOME_GESTURE = - new SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true); - - @Keep - public static final DeviceConfigBooleanFlag WM_ENABLE_PARTIAL_SCREEN_SHARING = - new DeviceConfigBooleanFlag(1102, "record_task_content", - NAMESPACE_WINDOW_MANAGER, false, true); - - @Keep - public static final SysPropBooleanFlag HIDE_NAVBAR_WINDOW = - new SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false); - - @Keep - public static final SysPropBooleanFlag WM_DESKTOP_WINDOWING = - new SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false); - - @Keep - public static final SysPropBooleanFlag WM_CAPTION_ON_SHELL = - new SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false); - - @Keep - public static final SysPropBooleanFlag FLOATING_TASKS_ENABLED = - new SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false); - - @Keep - public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES = - new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false); - - @Keep - public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_BUBBLE = - new SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true); - @Keep - public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_PIP = - new SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true); - - @Keep - public static final SysPropBooleanFlag ENABLE_PIP_KEEP_CLEAR_ALGORITHM = - new SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false); - - // 1200 - predictive back - @Keep - public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag( - 1200, "persist.wm.debug.predictive_back", true); - @Keep - public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK_ANIM = new SysPropBooleanFlag( - 1201, "persist.wm.debug.predictive_back_anim", false); - @Keep - public static final SysPropBooleanFlag WM_ALWAYS_ENFORCE_PREDICTIVE_BACK = - new SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false); - - public static final UnreleasedFlag NEW_BACK_AFFORDANCE = - new UnreleasedFlag(1203, false /* teamfood */); - - // 1300 - screenshots - - public static final UnreleasedFlag SCREENSHOT_REQUEST_PROCESSOR = new UnreleasedFlag(1300); - public static final UnreleasedFlag SCREENSHOT_WORK_PROFILE_POLICY = new UnreleasedFlag(1301); - - // 1400 - columbus - public static final ReleasedFlag QUICK_TAP_IN_PCC = new ReleasedFlag(1400); - - // 1500 - chooser - public static final UnreleasedFlag CHOOSER_UNBUNDLED = new UnreleasedFlag(1500); - - // Pay no attention to the reflection behind the curtain. - // ========================== Curtain ========================== - // | | - // | . . . . . . . . . . . . . . . . . . . | - private static Map<Integer, Flag<?>> sFlagMap; - static Map<Integer, Flag<?>> collectFlags() { - if (sFlagMap != null) { - return sFlagMap; - } - - Map<Integer, Flag<?>> flags = new HashMap<>(); - List<Field> flagFields = getFlagFields(); - - for (Field field : flagFields) { - try { - Flag<?> flag = (Flag<?>) field.get(null); - flags.put(flag.getId(), flag); - } catch (IllegalAccessException e) { - // no-op - } - } - - sFlagMap = flags; - - return sFlagMap; - } - - static List<Field> getFlagFields() { - Field[] fields = Flags.class.getFields(); - List<Field> result = new ArrayList<>(); - - for (Field field : fields) { - Class<?> t = field.getType(); - if (Flag.class.isAssignableFrom(t)) { - result.add(field); - } - } - - return result; - } - // | . . . . . . . . . . . . . . . . . . . | - // | | - // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/ - -} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt new file mode 100644 index 000000000000..06b7614a469e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.flags + +import android.provider.DeviceConfig +import com.android.internal.annotations.Keep +import com.android.systemui.R +import java.lang.reflect.Field + +/** + * List of [Flag] objects for use in SystemUI. + * + * Flag Ids are integers. Ids must be unique. This is enforced in a unit test. Ids need not be + * sequential. Flags can "claim" a chunk of ids for flags in related features with a comment. This + * is purely for organizational purposes. + * + * On public release builds, flags will always return their default value. There is no way to change + * their value on release builds. + * + * See [FeatureFlagsDebug] for instructions on flipping the flags via adb. + */ +object Flags { + @JvmField val TEAMFOOD = UnreleasedFlag(1) + + // 100 - notification + // TODO(b/254512751): Tracking Bug + val NOTIFICATION_PIPELINE_DEVELOPER_LOGGING = UnreleasedFlag(103) + + // TODO(b/254512732): Tracking Bug + @JvmField val NSSL_DEBUG_LINES = UnreleasedFlag(105) + + // TODO(b/254512505): Tracking Bug + @JvmField val NSSL_DEBUG_REMOVE_ANIMATION = UnreleasedFlag(106) + + // TODO(b/254512624): Tracking Bug + @JvmField + val NOTIFICATION_DRAG_TO_CONTENTS = + ResourceBooleanFlag(108, R.bool.config_notificationToContents) + + // TODO(b/254512517): Tracking Bug + val FSI_REQUIRES_KEYGUARD = UnreleasedFlag(110, teamfood = true) + + // TODO(b/254512538): Tracking Bug + val INSTANT_VOICE_REPLY = UnreleasedFlag(111, teamfood = true) + + // TODO(b/254512425): Tracking Bug + val NOTIFICATION_MEMORY_MONITOR_ENABLED = UnreleasedFlag(112, teamfood = false) + + // TODO(b/254512731): Tracking Bug + @JvmField val NOTIFICATION_DISMISSAL_FADE = UnreleasedFlag(113, teamfood = true) + val STABILITY_INDEX_FIX = UnreleasedFlag(114, teamfood = true) + val SEMI_STABLE_SORT = UnreleasedFlag(115, teamfood = true) + @JvmField val NOTIFICATION_GROUP_CORNER = UnreleasedFlag(116, true) + // next id: 117 + + // 200 - keyguard/lockscreen + // ** Flag retired ** + // public static final BooleanFlag KEYGUARD_LAYOUT = + // new BooleanFlag(200, true); + // TODO(b/254512713): Tracking Bug + @JvmField val LOCKSCREEN_ANIMATIONS = ReleasedFlag(201) + + // TODO(b/254512750): Tracking Bug + val NEW_UNLOCK_SWIPE_ANIMATION = ReleasedFlag(202) + val CHARGING_RIPPLE = ResourceBooleanFlag(203, R.bool.flag_charging_ripple) + + // TODO(b/254512281): Tracking Bug + @JvmField + val BOUNCER_USER_SWITCHER = ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher) + + // TODO(b/254512676): Tracking Bug + @JvmField val LOCKSCREEN_CUSTOM_CLOCKS = UnreleasedFlag(207, teamfood = true) + + /** + * Flag to enable the usage of the new bouncer data source. This is a refactor of and eventual + * replacement of KeyguardBouncer.java. + */ + // TODO(b/254512385): Tracking Bug + @JvmField val MODERN_BOUNCER = UnreleasedFlag(208) + + /** + * Whether the user interactor and repository should use `UserSwitcherController`. + * + * If this is `false`, the interactor and repo skip the controller and directly access the + * framework APIs. + */ + // TODO(b/254513286): Tracking Bug + val USER_INTERACTOR_AND_REPO_USE_CONTROLLER = UnreleasedFlag(210) + + /** + * Whether `UserSwitcherController` should use the user interactor. + * + * When this is `true`, the controller does not directly access framework APIs. Instead, it goes + * through the interactor. + * + * Note: do not set this to true if [.USER_INTERACTOR_AND_REPO_USE_CONTROLLER] is `true` as it + * would created a cycle between controller -> interactor -> controller. + */ + // TODO(b/254513102): Tracking Bug + val USER_CONTROLLER_USES_INTERACTOR = ReleasedFlag(211) + + /** + * Whether the clock on a wide lock screen should use the new "stepping" animation for moving + * the digits when the clock moves. + */ + @JvmField val STEP_CLOCK_ANIMATION = UnreleasedFlag(212) + + /** + * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository + * will occur in stages. This is one stage of many to come. + */ + @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213, teamfood = true) + + // 300 - power menu + // TODO(b/254512600): Tracking Bug + @JvmField val POWER_MENU_LITE = ReleasedFlag(300) + + // 400 - smartspace + + // TODO(b/254513100): Tracking Bug + val SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED = ReleasedFlag(401) + val SMARTSPACE = ResourceBooleanFlag(402, R.bool.flag_smartspace) + + // 500 - quick settings + @Deprecated("Not needed anymore") val NEW_USER_SWITCHER = ReleasedFlag(500) + + // TODO(b/254512321): Tracking Bug + @JvmField val COMBINED_QS_HEADERS = ReleasedFlag(501, teamfood = true) + val PEOPLE_TILE = ResourceBooleanFlag(502, R.bool.flag_conversations) + @JvmField + val QS_USER_DETAIL_SHORTCUT = + ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut) + + // TODO(b/254512699): Tracking Bug + @Deprecated("Not needed anymore") val NEW_FOOTER = ReleasedFlag(504) + + // TODO(b/254512747): Tracking Bug + val NEW_HEADER = ReleasedFlag(505, teamfood = true) + + // TODO(b/254512383): Tracking Bug + @JvmField + val FULL_SCREEN_USER_SWITCHER = + ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher) + + // TODO(b/254512678): Tracking Bug + @JvmField val NEW_FOOTER_ACTIONS = ReleasedFlag(507) + + // 600- status bar + // TODO(b/254513246): Tracking Bug + val STATUS_BAR_USER_SWITCHER = ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip) + + // TODO(b/254513025): Tracking Bug + val STATUS_BAR_LETTERBOX_APPEARANCE = ReleasedFlag(603, teamfood = false) + + // TODO(b/254512623): Tracking Bug + @Deprecated("Replaced by mobile and wifi specific flags.") + val NEW_STATUS_BAR_PIPELINE_BACKEND = UnreleasedFlag(604, teamfood = false) + + // TODO(b/254512660): Tracking Bug + @Deprecated("Replaced by mobile and wifi specific flags.") + val NEW_STATUS_BAR_PIPELINE_FRONTEND = UnreleasedFlag(605, teamfood = false) + + val NEW_STATUS_BAR_MOBILE_ICONS = UnreleasedFlag(606, false) + + val NEW_STATUS_BAR_WIFI_ICON = UnreleasedFlag(607, false) + + // 700 - dialer/calls + // TODO(b/254512734): Tracking Bug + val ONGOING_CALL_STATUS_BAR_CHIP = ReleasedFlag(700) + + // TODO(b/254512681): Tracking Bug + val ONGOING_CALL_IN_IMMERSIVE = ReleasedFlag(701) + + // TODO(b/254512753): Tracking Bug + val ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP = ReleasedFlag(702) + + // 800 - general visual/theme + @JvmField val MONET = ResourceBooleanFlag(800, R.bool.flag_monet) + + // 801 - region sampling + // TODO(b/254512848): Tracking Bug + val REGION_SAMPLING = UnreleasedFlag(801) + + // 802 - wallpaper rendering + // TODO(b/254512923): Tracking Bug + @JvmField val USE_CANVAS_RENDERER = ReleasedFlag(802) + + // 803 - screen contents translation + // TODO(b/254513187): Tracking Bug + val SCREEN_CONTENTS_TRANSLATION = UnreleasedFlag(803) + + // 804 - monochromatic themes + @JvmField val MONOCHROMATIC_THEMES = UnreleasedFlag(804) + + // 900 - media + // TODO(b/254512697): Tracking Bug + val MEDIA_TAP_TO_TRANSFER = ReleasedFlag(900) + + // TODO(b/254512502): Tracking Bug + val MEDIA_SESSION_ACTIONS = UnreleasedFlag(901) + + // TODO(b/254512726): Tracking Bug + val MEDIA_NEARBY_DEVICES = ReleasedFlag(903) + + // TODO(b/254512695): Tracking Bug + val MEDIA_MUTE_AWAIT = ReleasedFlag(904) + + // TODO(b/254512654): Tracking Bug + @JvmField val DREAM_MEDIA_COMPLICATION = UnreleasedFlag(905) + + // TODO(b/254512673): Tracking Bug + @JvmField val DREAM_MEDIA_TAP_TO_OPEN = UnreleasedFlag(906) + + // TODO(b/254513168): Tracking Bug + val UMO_SURFACE_RIPPLE = UnreleasedFlag(907) + + // 1000 - dock + val SIMULATE_DOCK_THROUGH_CHARGING = ReleasedFlag(1000) + + // TODO(b/254512444): Tracking Bug + @JvmField val DOCK_SETUP_ENABLED = ReleasedFlag(1001) + + // TODO(b/254512758): Tracking Bug + @JvmField val ROUNDED_BOX_RIPPLE = ReleasedFlag(1002) + + // TODO(b/254512525): Tracking Bug + @JvmField val REFACTORED_DOCK_SETUP = ReleasedFlag(1003, teamfood = true) + + // 1100 - windowing + @Keep + val WM_ENABLE_SHELL_TRANSITIONS = + SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false) + + /** b/170163464: animate bubbles expanded view collapse with home gesture */ + @Keep + val BUBBLES_HOME_GESTURE = + SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true) + + // TODO(b/254513207): Tracking Bug + @JvmField + @Keep + val WM_ENABLE_PARTIAL_SCREEN_SHARING = + DeviceConfigBooleanFlag( + 1102, + "record_task_content", + DeviceConfig.NAMESPACE_WINDOW_MANAGER, + false, + teamfood = true + ) + + // TODO(b/254512674): Tracking Bug + @JvmField + @Keep + val HIDE_NAVBAR_WINDOW = SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false) + + @Keep + val WM_DESKTOP_WINDOWING = SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false) + + @Keep + val WM_CAPTION_ON_SHELL = SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false) + + @Keep + val FLOATING_TASKS_ENABLED = SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false) + + @Keep + val SHOW_FLOATING_TASKS_AS_BUBBLES = + SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false) + + @Keep + val ENABLE_FLING_TO_DISMISS_BUBBLE = + SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true) + + @Keep + val ENABLE_FLING_TO_DISMISS_PIP = + SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true) + + @Keep + val ENABLE_PIP_KEEP_CLEAR_ALGORITHM = + SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false) + + // 1200 - predictive back + @Keep + val WM_ENABLE_PREDICTIVE_BACK = + SysPropBooleanFlag(1200, "persist.wm.debug.predictive_back", true) + + @Keep + val WM_ENABLE_PREDICTIVE_BACK_ANIM = + SysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", false) + + @Keep + val WM_ALWAYS_ENFORCE_PREDICTIVE_BACK = + SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false) + + // TODO(b/254512728): Tracking Bug + @JvmField val NEW_BACK_AFFORDANCE = UnreleasedFlag(1203, teamfood = false) + + // 1300 - screenshots + // TODO(b/254512719): Tracking Bug + @JvmField val SCREENSHOT_REQUEST_PROCESSOR = UnreleasedFlag(1300) + + // TODO(b/254513155): Tracking Bug + @JvmField val SCREENSHOT_WORK_PROFILE_POLICY = UnreleasedFlag(1301) + + // 1400 - columbus + // TODO(b/254512756): Tracking Bug + val QUICK_TAP_IN_PCC = ReleasedFlag(1400) + + // 1500 - chooser + // TODO(b/254512507): Tracking Bug + val CHOOSER_UNBUNDLED = UnreleasedFlag(1500) + + // 1700 - clipboard + @JvmField val CLIPBOARD_OVERLAY_REFACTOR = UnreleasedFlag(1700) + + // 1800 - shade container + @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = UnreleasedFlag(1800, true) + + // Pay no attention to the reflection behind the curtain. + // ========================== Curtain ========================== + // | | + // | . . . . . . . . . . . . . . . . . . . | + @JvmStatic + fun collectFlags(): Map<Int, Flag<*>> { + return flagFields + .map { field -> + // field[null] returns the current value of the field. + // See java.lang.Field#get + val flag = field[null] as Flag<*> + flag.id to flag + } + .toMap() + } + + // | . . . . . . . . . . . . . . . . . . . | + @JvmStatic + val flagFields: List<Field> + get() { + return Flags::class.java.fields.filter { f -> + Flag::class.java.isAssignableFrom(f.type) + } + } + // | | + // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/ +} diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index da5819a7f3bc..3ef5499237f1 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -116,6 +116,7 @@ import com.android.systemui.MultiListLayout; import com.android.systemui.MultiListLayout.MultiListAdapter; import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.animation.Expandable; import com.android.systemui.animation.Interpolators; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; @@ -448,10 +449,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene * * @param keyguardShowing True if keyguard is showing * @param isDeviceProvisioned True if device is provisioned - * @param view The view from which we should animate the dialog when showing it + * @param expandable The expandable from which we should animate the dialog when + * showing it */ public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned, - @Nullable View view) { + @Nullable Expandable expandable) { mKeyguardShowing = keyguardShowing; mDeviceProvisioned = isDeviceProvisioned; if (mDialog != null && mDialog.isShowing()) { @@ -463,7 +465,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mDialog.dismiss(); mDialog = null; } else { - handleShow(view); + handleShow(expandable); } } @@ -495,7 +497,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } } - protected void handleShow(@Nullable View view) { + protected void handleShow(@Nullable Expandable expandable) { awakenIfNecessary(); mDialog = createDialog(); prepareDialog(); @@ -507,10 +509,12 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene // Don't acquire soft keyboard focus, to avoid destroying state when capturing bugreports mDialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM); - if (view != null) { - mDialogLaunchAnimator.showFromView(mDialog, view, - new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG)); + DialogLaunchAnimator.Controller controller = + expandable != null ? expandable.dialogLaunchController( + new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG)) : null; + if (controller != null) { + mDialogLaunchAnimator.show(mDialog, controller); } else { mDialog.show(); } @@ -1016,8 +1020,9 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene Log.w(TAG, "Bugreport handler could not be launched"); mIActivityManager.requestInteractiveBugReport(); } - // Close shade so user sees the activity - mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade); + // Maybe close shade (depends on a flag) so user sees the activity + mCentralSurfacesOptional.ifPresent( + CentralSurfaces::collapseShadeForBugreport); } catch (RemoteException e) { } } @@ -1036,8 +1041,8 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mMetricsLogger.action(MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_FULL); mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS); mIActivityManager.requestFullBugReport(); - // Close shade so user sees the activity - mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade); + // Maybe close shade (depends on a flag) so user sees the activity + mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShadeForBugreport); } catch (RemoteException e) { } return false; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index a3632705d865..c49079422b20 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -91,6 +91,7 @@ import android.view.animation.AnimationUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor.Configuration; @@ -322,6 +323,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // true if the keyguard is hidden by another window private boolean mOccluded = false; + /** + * Whether the {@link #mOccludeAnimationController} is currently playing the occlusion + * animation. + */ + private boolean mOccludeAnimationPlaying = false; + private boolean mWakeAndUnlocking = false; /** @@ -401,6 +408,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final float mWindowCornerRadius; /** + * The duration in milliseconds of the dream open animation. + */ + private final int mDreamOpenAnimationDuration; + + /** * The animation used for hiding keyguard. This is used to fetch the animation timings if * WindowManager is not providing us with them. */ @@ -831,15 +843,22 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, /** * Animation launch controller for activities that occlude the keyguard. */ - private final ActivityLaunchAnimator.Controller mOccludeAnimationController = + @VisibleForTesting + final ActivityLaunchAnimator.Controller mOccludeAnimationController = new ActivityLaunchAnimator.Controller() { @Override - public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {} + public void onLaunchAnimationStart(boolean isExpandingFullyAbove) { + mOccludeAnimationPlaying = true; + } @Override public void onLaunchAnimationCancelled(@Nullable Boolean newKeyguardOccludedState) { Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: " + mOccluded); + mOccludeAnimationPlaying = false; + + // Ensure keyguard state is set correctly if we're cancelled. + mCentralSurfaces.updateIsKeyguard(); } @Override @@ -848,6 +867,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mCentralSurfaces.instantCollapseNotificationPanel(); } + mOccludeAnimationPlaying = false; + + // Hide the keyguard now that we're done launching the occluding activity over + // it. + mCentralSurfaces.updateIsKeyguard(); + mInteractionJankMonitor.end(CUJ_LOCKSCREEN_OCCLUSION); } @@ -946,8 +971,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } mOccludeByDreamAnimator = ValueAnimator.ofFloat(0f, 1f); - // Use the same duration as for the UNOCCLUDE. - mOccludeByDreamAnimator.setDuration(UNOCCLUDE_ANIMATION_DURATION); + mOccludeByDreamAnimator.setDuration(mDreamOpenAnimationDuration); mOccludeByDreamAnimator.setInterpolator(Interpolators.LINEAR); mOccludeByDreamAnimator.addUpdateListener( animation -> { @@ -1179,6 +1203,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mPowerButtonY = context.getResources().getDimensionPixelSize( R.dimen.physical_power_button_center_screen_location_y); mWindowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); + + mDreamOpenAnimationDuration = context.getResources().getInteger( + com.android.internal.R.integer.config_dreamOpenAnimationDuration); } public void userActivity() { @@ -1760,6 +1787,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, return mShowing && !mOccluded; } + public boolean isOccludeAnimationPlaying() { + return mOccludeAnimationPlaying; + } + /** * Notify us when the keyguard is occluded by another window */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 56f1ac46a875..56a1f1ae936e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -43,6 +43,7 @@ import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; +import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.statusbar.NotificationShadeDepthController; @@ -72,6 +73,7 @@ import dagger.Provides; FalsingModule.class, KeyguardQuickAffordanceModule.class, KeyguardRepositoryModule.class, + StartKeyguardTransitionModule.class, }) public class KeyguardModule { /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 45b668e609ea..b186ae0ceec4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -21,6 +21,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.Position import com.android.systemui.dagger.SysUISingleton import com.android.systemui.doze.DozeHost +import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject @@ -85,6 +86,9 @@ interface KeyguardRepository { */ val dozeAmount: Flow<Float> + /** Observable for the [StatusBarState] */ + val statusBarState: Flow<StatusBarState> + /** * Returns `true` if the keyguard is showing; `false` otherwise. * @@ -185,6 +189,24 @@ constructor( return keyguardStateController.isShowing } + override val statusBarState: Flow<StatusBarState> = conflatedCallbackFlow { + val callback = + object : StatusBarStateController.StateListener { + override fun onStateChanged(state: Int) { + trySendWithFailureLogging(statusBarStateIntToObject(state), TAG, "state") + } + } + + statusBarStateController.addCallback(callback) + trySendWithFailureLogging( + statusBarStateIntToObject(statusBarStateController.getState()), + TAG, + "initial state" + ) + + awaitClose { statusBarStateController.removeCallback(callback) } + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } @@ -197,6 +219,15 @@ constructor( _clockPosition.value = Position(x, y) } + private fun statusBarStateIntToObject(value: Int): StatusBarState { + return when (value) { + 0 -> StatusBarState.SHADE + 1 -> StatusBarState.KEYGUARD + 2 -> StatusBarState.SHADE_LOCKED + else -> throw IllegalArgumentException("Invalid StatusBarState value: $value") + } + } + companion object { private const val TAG = "KeyguardRepositoryImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt new file mode 100644 index 000000000000..e8532ecfdc37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -0,0 +1,169 @@ +/* + * 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.keyguard.data.repository + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener +import android.annotation.FloatRange +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter + +@SysUISingleton +class KeyguardTransitionRepository @Inject constructor() { + /* + * Each transition between [KeyguardState]s will have an associated Flow. + * In order to collect these events, clients should call [transition]. + */ + private val _transitions = MutableStateFlow(TransitionStep()) + val transitions = _transitions.asStateFlow() + + /* Information about the active transition. */ + private var currentTransitionInfo: TransitionInfo? = null + /* + * When manual control of the transition is requested, a unique [UUID] is used as the handle + * to permit calls to [updateTransition] + */ + private var updateTransitionId: UUID? = null + + /** + * Interactors that require information about changes between [KeyguardState]s will call this to + * register themselves for flowable [TransitionStep]s when that transition occurs. + */ + fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> { + return transitions.filter { step -> step.from == from && step.to == to } + } + + /** + * Begin a transition from one state to another. The [info.from] must match + * [currentTransitionInfo.to], or the request will be denied. This is enforced to avoid + * unplanned transitions. + */ + fun startTransition(info: TransitionInfo): UUID? { + if (currentTransitionInfo != null) { + // Open questions: + // * Queue of transitions? buffer of 1? + // * Are transitions cancellable if a new one is triggered? + // * What validation does this need to do? + Log.wtf(TAG, "Transition still active: $currentTransitionInfo") + return null + } + currentTransitionInfo?.animator?.cancel() + + currentTransitionInfo = info + info.animator?.let { animator -> + // An animator was provided, so use it to run the transition + animator.setFloatValues(0f, 1f) + val updateListener = + object : AnimatorUpdateListener { + override fun onAnimationUpdate(animation: ValueAnimator) { + emitTransition( + info, + (animation.getAnimatedValue() as Float), + TransitionState.RUNNING + ) + } + } + val adapter = + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + Log.i(TAG, "Starting transition: $info") + emitTransition(info, 0f, TransitionState.STARTED) + } + override fun onAnimationCancel(animation: Animator) { + Log.i(TAG, "Cancelling transition: $info") + } + override fun onAnimationEnd(animation: Animator) { + Log.i(TAG, "Ending transition: $info") + emitTransition(info, 1f, TransitionState.FINISHED) + animator.removeListener(this) + animator.removeUpdateListener(updateListener) + } + } + animator.addListener(adapter) + animator.addUpdateListener(updateListener) + animator.start() + return@startTransition null + } + ?: run { + Log.i(TAG, "Starting transition (manual): $info") + emitTransition(info, 0f, TransitionState.STARTED) + + // No animator, so it's manual. Provide a mechanism to callback + updateTransitionId = UUID.randomUUID() + return@startTransition updateTransitionId + } + } + + /** + * Allows manual control of a transition. When calling [startTransition], the consumer must pass + * in a null animator. In return, it will get a unique [UUID] that will be validated to allow + * further updates. + * + * When the transition is over, TransitionState.FINISHED must be passed into the [state] + * parameter. + */ + fun updateTransition( + transitionId: UUID, + @FloatRange(from = 0.0, to = 1.0) value: Float, + state: TransitionState + ) { + if (updateTransitionId != transitionId) { + Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId") + return + } + + if (currentTransitionInfo == null) { + Log.wtf(TAG, "Attempting to update with null 'currentTransitionInfo'") + return + } + + currentTransitionInfo?.let { info -> + if (state == TransitionState.FINISHED) { + updateTransitionId = null + Log.i(TAG, "Ending transition: $info") + } + + emitTransition(info, value, state) + } + } + + private fun emitTransition( + info: TransitionInfo, + value: Float, + transitionState: TransitionState + ) { + if (transitionState == TransitionState.FINISHED) { + currentTransitionInfo = null + } + _transitions.value = TransitionStep(info.from, info.to, value, transitionState) + } + + companion object { + private const val TAG = "KeyguardTransitionRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt new file mode 100644 index 000000000000..400376663f1a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt @@ -0,0 +1,77 @@ +/* + * 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.keyguard.domain.interactor + +import android.animation.ValueAnimator +import com.android.systemui.animation.Interpolators +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@SysUISingleton +class AodLockscreenTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : TransitionInteractor("AOD<->LOCKSCREEN") { + + override fun start() { + scope.launch { + keyguardRepository.isDozing.collect { isDozing -> + if (isDozing) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + getAnimator(), + ) + ) + } else { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + getAnimator(), + ) + ) + } + } + } + } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 500L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt index 7d4db37c6b0f..2af9318d92ec 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt @@ -273,8 +273,8 @@ constructor( /** Tell the bouncer to start the pre hide animation. */ fun startDisappearAnimation(runnable: Runnable) { val finishRunnable = Runnable { - repository.setStartDisappearAnimation(null) runnable.run() + repository.setStartDisappearAnimation(null) } repository.setStartDisappearAnimation(finishRunnable) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 192919e32cf6..fc2269c6b01c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -38,7 +38,7 @@ constructor( val dozeAmount: Flow<Float> = repository.dozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow<Boolean> = repository.isDozing - /** Whether the keyguard is showing ot not. */ + /** Whether the keyguard is showing to not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing fun isKeyguardShowing(): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt new file mode 100644 index 000000000000..b166681433a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -0,0 +1,49 @@ +/* + * 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.keyguard.domain.interactor + +import android.util.Log +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import java.util.Set +import javax.inject.Inject + +@SysUISingleton +class KeyguardTransitionCoreStartable +@Inject +constructor( + private val interactors: Set<TransitionInteractor>, +) : CoreStartable { + + override fun start() { + // By listing the interactors in a when, the compiler will help enforce all classes + // extending the sealed class [TransitionInteractor] will be initialized. + interactors.forEach { + // `when` needs to be an expression in order for the compiler to enforce it being + // exhaustive + val ret = + when (it) { + is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it") + is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") + } + it.start() + } + } + + companion object { + private const val TAG = "KeyguardTransitionCoreStartable" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt new file mode 100644 index 000000000000..7409aec57b4c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.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.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionStep +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +/** Encapsulates business-logic related to the keyguard transitions. */ +@SysUISingleton +class KeyguardTransitionInteractor +@Inject +constructor( + repository: KeyguardTransitionRepository, +) { + /** AOD->LOCKSCREEN transition information. */ + val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN) + + /** LOCKSCREEN->AOD transition information. */ + val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD) + + /** + * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <-> + * Lockscreen (0f). + */ + val dozeAmountTransition: Flow<TransitionStep> = + merge( + aodToLockscreenTransition.map { step -> step.copy(value = 1f - step.value) }, + lockscreenToAodTransition, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt new file mode 100644 index 000000000000..3c2a12e3836a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt @@ -0,0 +1,98 @@ +/* + * 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.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.util.kotlin.sample +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +@SysUISingleton +class LockscreenBouncerTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, + private val shadeRepository: ShadeRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : TransitionInteractor("LOCKSCREEN<->BOUNCER") { + + private var transitionId: UUID? = null + + override fun start() { + scope.launch { + shadeRepository.shadeModel.sample( + combine( + keyguardTransitionRepository.transitions, + keyguardRepository.statusBarState, + ) { transitions, statusBarState -> + Pair(transitions, statusBarState) + } + ) { shadeModel, pair -> + val (transitions, statusBarState) = pair + + val id = transitionId + if (id != null) { + // An existing `id` means a transition is started, and calls to + // `updateTransition` will control it until FINISHED + keyguardTransitionRepository.updateTransition( + id, + shadeModel.expansionAmount, + if (shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f) { + transitionId = null + TransitionState.FINISHED + } else { + TransitionState.RUNNING + } + ) + } else { + // TODO (b/251849525): Remove statusbarstate check when that state is integrated + // into KeyguardTransitionRepository + val isOnLockscreen = + transitions.transitionState == TransitionState.FINISHED && + transitions.to == KeyguardState.LOCKSCREEN + if ( + isOnLockscreen && + shadeModel.isUserDragging && + statusBarState != SHADE_LOCKED + ) { + transitionId = + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.BOUNCER, + animator = null, + ) + ) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt new file mode 100644 index 000000000000..74c542c0043f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt @@ -0,0 +1,42 @@ +/* + * 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.keyguard.domain.interactor + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet + +@Module +abstract class StartKeyguardTransitionModule { + + @Binds + @IntoMap + @ClassKey(KeyguardTransitionCoreStartable::class) + abstract fun bind(impl: KeyguardTransitionCoreStartable): CoreStartable + + @Binds + @IntoSet + abstract fun lockscreenBouncer( + impl: LockscreenBouncerTransitionInteractor + ): TransitionInteractor + + @Binds + @IntoSet + abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt new file mode 100644 index 000000000000..a2a46d9e3a71 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.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.keyguard.domain.interactor +/** + * Each TransitionInteractor is responsible for determining under which conditions to notify + * [KeyguardTransitionRepository] to signal a transition. When (and if) the transition occurs is + * determined by [KeyguardTransitionRepository]. + * + * [name] field should be a unique identifiable string representing this state, used primarily for + * logging + * + * MUST list implementing classes in dagger module [StartKeyguardTransitionModule] and also in the + * 'when' clause of [KeyguardTransitionCoreStartable] + */ +sealed class TransitionInteractor(val name: String) { + + abstract fun start() +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt new file mode 100644 index 000000000000..f66d5d3650c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.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.keyguard.shared.model + +/** List of all possible states to transition to/from */ +enum class KeyguardState { + /** For initialization only */ + NONE, + /* Always-on Display. The device is in a low-power mode with a minimal UI visible */ + AOD, + /* + * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS + * (Fingerprint Sensor) variations, for the user to verify their credentials + */ + BOUNCER, + /* + * Device is actively displaying keyguard UI and is not in low-power mode. Device may be + * unlocked if SWIPE security method is used, or if face lockscreen bypass is false. + */ + LOCKSCREEN, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt new file mode 100644 index 000000000000..bb953477583d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt @@ -0,0 +1,23 @@ +/* + * 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.keyguard.shared.model + +/** See [com.android.systemui.statusbar.StatusBarState] for definitions */ +enum class StatusBarState { + SHADE, + KEYGUARD, + SHADE_LOCKED, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt new file mode 100644 index 000000000000..bfccf3fe076c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt @@ -0,0 +1,35 @@ +/* + * 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.keyguard.shared.model + +import android.animation.ValueAnimator + +/** Tracks who is controlling the current transition, and how to run it. */ +data class TransitionInfo( + val ownerName: String, + val from: KeyguardState, + val to: KeyguardState, + val animator: ValueAnimator?, // 'null' animator signal manual control +) { + override fun toString(): String = + "TransitionInfo(ownerName=$ownerName, from=$from, to=$to, " + + (if (animator != null) { + "animated" + } else { + "manual" + }) + + ")" +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt new file mode 100644 index 000000000000..d8691c17f53d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt @@ -0,0 +1,24 @@ +/* + * 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.keyguard.shared.model + +/** Possible states for a running transition between [State] */ +enum class TransitionState { + NONE, + STARTED, + RUNNING, + FINISHED +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt new file mode 100644 index 000000000000..688ec912aac8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt @@ -0,0 +1,24 @@ +/* + * 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.keyguard.shared.model + +/** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */ +data class TransitionStep( + val from: KeyguardState = KeyguardState.NONE, + val to: KeyguardState = KeyguardState.NONE, + val value: Float = 0f, // constrained [0.0, 1.0] + val transitionState: TransitionState = TransitionState.NONE, +) diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt index 5651399cb891..f9e341c8629a 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt @@ -19,6 +19,9 @@ package com.android.systemui.log import android.app.ActivityManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogcatEchoTracker + import javax.inject.Inject @SysUISingleton @@ -26,7 +29,7 @@ class LogBufferFactory @Inject constructor( private val dumpManager: DumpManager, private val logcatEchoTracker: LogcatEchoTracker ) { - /* limit the size of maxPoolSize for low ram (Go) devices */ + /* limitiometricMessageDeferralLogger the size of maxPoolSize for low ram (Go) devices */ private fun adjustMaxSize(requestedMaxSize: Int): Int { return if (ActivityManager.isLowRamDeviceStatic()) { minOf(requestedMaxSize, 20) /* low ram max log size*/ diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java index 7f1ad6d20c16..eeadf406060d 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java @@ -23,7 +23,7 @@ import java.lang.annotation.RetentionPolicy; import javax.inject.Qualifier; /** - * A {@link com.android.systemui.log.LogBuffer} for BiometricMessages processing such as + * A {@link com.android.systemui.plugins.log.LogBuffer} for BiometricMessages processing such as * {@link com.android.systemui.biometrics.FaceHelpMessageDeferral} */ @Qualifier diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java index 7d1f1c2709fa..5cca1ab2abe7 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java index 9ca0293fbd86..1d016d837b02 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java index 7c5f4025117f..c9f78bcdeef8 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt new file mode 100644 index 000000000000..0645236226bd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt @@ -0,0 +1,25 @@ +/* + * 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.log.dagger + +import javax.inject.Qualifier + +/** A [com.android.systemui.plugins.log.LogBuffer] for keyguard clock logs. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyguardClockLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java index 08d969b5eb77..76d20bea4bdf 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 1b81e88e62ba..9adb855d66ac 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -22,11 +22,11 @@ import android.os.Looper; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.log.LogBuffer; import com.android.systemui.log.LogBufferFactory; -import com.android.systemui.log.LogcatEchoTracker; -import com.android.systemui.log.LogcatEchoTrackerDebug; -import com.android.systemui.log.LogcatEchoTrackerProd; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogcatEchoTracker; +import com.android.systemui.plugins.log.LogcatEchoTrackerDebug; +import com.android.systemui.plugins.log.LogcatEchoTrackerProd; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.util.Compile; @@ -43,7 +43,7 @@ public class LogModule { @SysUISingleton @DozeLog public static LogBuffer provideDozeLogBuffer(LogBufferFactory factory) { - return factory.create("DozeLog", 120); + return factory.create("DozeLog", 150); } /** Provides a logging buffer for all logs related to the data layer of notifications. */ @@ -250,7 +250,7 @@ public class LogModule { /** * Provides a buffer for our connections and disconnections to MediaBrowserService. * - * See {@link com.android.systemui.media.ResumeMediaBrowser}. + * See {@link com.android.systemui.media.controls.resume.ResumeMediaBrowser}. */ @Provides @SysUISingleton @@ -262,7 +262,7 @@ public class LogModule { /** * Provides a buffer for updates to the media carousel. * - * See {@link com.android.systemui.media.MediaCarouselController}. + * See {@link com.android.systemui.media.controls.ui.MediaCarouselController}. */ @Provides @SysUISingleton @@ -316,6 +316,16 @@ public class LogModule { } /** + * Provides a {@link LogBuffer} for keyguard clock logs. + */ + @Provides + @SysUISingleton + @KeyguardClockLog + public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) { + return factory.create("KeyguardClockLog", 500); + } + + /** * Provides a {@link LogBuffer} for use by {@link com.android.keyguard.KeyguardUpdateMonitor}. */ @Provides diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java index 1d7ba94af4ed..af433476b38c 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -26,7 +26,7 @@ import java.lang.annotation.Retention; import javax.inject.Qualifier; /** - * A {@link LogBuffer} for {@link com.android.systemui.media.ResumeMediaBrowser} + * A {@link LogBuffer} for {@link com.android.systemui.media.controls.resume.ResumeMediaBrowser} */ @Qualifier @Documented diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java index b03655a543f7..f4dac6efe371 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -26,7 +26,7 @@ import java.lang.annotation.Retention; import javax.inject.Qualifier; /** - * A {@link LogBuffer} for {@link com.android.systemui.media.MediaCarouselController} + * A {@link LogBuffer} for {@link com.android.systemui.media.controls.ui.MediaCarouselController} */ @Qualifier @Documented diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java index c67d8bebe313..73690ab6c24d 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java index 53963fc8d431..0c2cd92d1bb0 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -26,7 +26,7 @@ import java.lang.annotation.Retention; import javax.inject.Qualifier; /** - * A {@link LogBuffer} for {@link com.android.systemui.media.MediaTimeoutLogger} + * A {@link LogBuffer} for {@link com.android.systemui.media.controls.pipeline.MediaTimeoutLogger} */ @Qualifier @Documented diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java index 5c572e8ef554..1570d434bc62 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java index edab8c319f87..bf216c6991d2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java index 75a34fc22c3c..5b7f4bb103b4 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -26,7 +26,7 @@ import java.lang.annotation.Retention; import javax.inject.Qualifier; /** - * A {@link LogBuffer} for {@link com.android.systemui.media.MediaViewLogger} + * A {@link LogBuffer} for {@link com.android.systemui.media.controls.ui.MediaViewLogger} */ @Qualifier @Documented diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java index b1c6dcfcb13b..6d91f0c97c8a 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java index 20fc6ff445a6..26af4964f7b8 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java index fcc184a317b8..61daf9c8d71c 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java index 760fbf3928b6..a59afa0fed1b 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java index a0b686487bec..6f8ea7ff2e9b 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java index 8c8753a07339..835d3490293c 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java index 7259eebf19b6..6e2bd7b2e1b5 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java index e96e532f94bf..77b1bf5fd630 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java index 557a254e5c09..9fd166b759d2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java index dd5010cf39a8..dd168bac5654 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java index bd0d298ebdee..d24bfcb88188 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java index b237f2d74483..67cdb722055b 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java index f26b3164f488..af0f7c518e64 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java index dd6837563a74..4c276e2bfaab 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java index 8671dbfdf1fe..ba8b27c23ec1 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt deleted file mode 100644 index 556560c3534c..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media - -import android.animation.ArgbEvaluator -import android.animation.ValueAnimator -import android.animation.ValueAnimator.AnimatorUpdateListener -import android.content.Context -import android.content.res.ColorStateList -import android.content.res.Configuration -import android.content.res.Configuration.UI_MODE_NIGHT_YES -import android.graphics.drawable.RippleDrawable -import com.android.internal.R -import com.android.internal.annotations.VisibleForTesting -import com.android.settingslib.Utils -import com.android.systemui.monet.ColorScheme - -/** - * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme] - * is triggered. - */ -interface ColorTransition { - fun updateColorScheme(scheme: ColorScheme?): Boolean -} - -/** - * A [ColorTransition] that animates between two specific colors. - * It uses a ValueAnimator to execute the animation and interpolate between the source color and - * the target color. - * - * Selection of the target color from the scheme, and application of the interpolated color - * are delegated to callbacks. - */ -open class AnimatingColorTransition( - private val defaultColor: Int, - private val extractColor: (ColorScheme) -> Int, - private val applyColor: (Int) -> Unit -) : AnimatorUpdateListener, ColorTransition { - - private val argbEvaluator = ArgbEvaluator() - private val valueAnimator = buildAnimator() - var sourceColor: Int = defaultColor - var currentColor: Int = defaultColor - var targetColor: Int = defaultColor - - override fun onAnimationUpdate(animation: ValueAnimator) { - currentColor = argbEvaluator.evaluate( - animation.animatedFraction, sourceColor, targetColor - ) as Int - applyColor(currentColor) - } - - override fun updateColorScheme(scheme: ColorScheme?): Boolean { - val newTargetColor = if (scheme == null) defaultColor else extractColor(scheme) - if (newTargetColor != targetColor) { - sourceColor = currentColor - targetColor = newTargetColor - valueAnimator.cancel() - valueAnimator.start() - return true - } - return false - } - - init { - applyColor(defaultColor) - } - - @VisibleForTesting - open fun buildAnimator(): ValueAnimator { - val animator = ValueAnimator.ofFloat(0f, 1f) - animator.duration = 333 - animator.addUpdateListener(this) - return animator - } -} - -typealias AnimatingColorTransitionFactory = - (Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition - -/** - * ColorSchemeTransition constructs a ColorTransition for each color in the scheme - * that needs to be transitioned when changed. It also sets up the assignment functions for sending - * the sending the interpolated colors to the appropriate views. - */ -class ColorSchemeTransition internal constructor( - private val context: Context, - private val mediaViewHolder: MediaViewHolder, - animatingColorTransitionFactory: AnimatingColorTransitionFactory -) { - constructor(context: Context, mediaViewHolder: MediaViewHolder) : - this(context, mediaViewHolder, ::AnimatingColorTransition) - - val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95) - val surfaceColor = animatingColorTransitionFactory( - bgColor, - ::surfaceFromScheme - ) { surfaceColor -> - val colorList = ColorStateList.valueOf(surfaceColor) - mediaViewHolder.seamlessIcon.imageTintList = colorList - mediaViewHolder.seamlessText.setTextColor(surfaceColor) - mediaViewHolder.albumView.backgroundTintList = colorList - mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor) - } - - val accentPrimary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimary), - ::accentPrimaryFromScheme - ) { accentPrimary -> - val accentColorList = ColorStateList.valueOf(accentPrimary) - mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList - mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary) - } - - val accentSecondary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimary), - ::accentSecondaryFromScheme - ) { accentSecondary -> - val colorList = ColorStateList.valueOf(accentSecondary) - (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let { - it.setColor(colorList) - it.effectColor = colorList - } - } - - val colorSeamless = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimary), - { colorScheme: ColorScheme -> - // A1-100 dark in dark theme, A1-200 in light theme - if (context.resources.configuration.uiMode and - Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES) - colorScheme.accent1[2] - else colorScheme.accent1[3] - }, { seamlessColor: Int -> - val accentColorList = ColorStateList.valueOf(seamlessColor) - mediaViewHolder.seamlessButton.backgroundTintList = accentColorList - }) - - val textPrimary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimary), - ::textPrimaryFromScheme - ) { textPrimary -> - mediaViewHolder.titleText.setTextColor(textPrimary) - val textColorList = ColorStateList.valueOf(textPrimary) - mediaViewHolder.seekBar.thumb.setTintList(textColorList) - mediaViewHolder.seekBar.progressTintList = textColorList - mediaViewHolder.scrubbingElapsedTimeView.setTextColor(textColorList) - mediaViewHolder.scrubbingTotalTimeView.setTextColor(textColorList) - for (button in mediaViewHolder.getTransparentActionButtons()) { - button.imageTintList = textColorList - } - mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary) - } - - val textPrimaryInverse = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimaryInverse), - ::textPrimaryInverseFromScheme - ) { textPrimaryInverse -> - mediaViewHolder.actionPlayPause.imageTintList = ColorStateList.valueOf(textPrimaryInverse) - } - - val textSecondary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorSecondary), - ::textSecondaryFromScheme - ) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) } - - val textTertiary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorTertiary), - ::textTertiaryFromScheme - ) { textTertiary -> - mediaViewHolder.seekBar.progressBackgroundTintList = ColorStateList.valueOf(textTertiary) - } - - val colorTransitions = arrayOf( - surfaceColor, - colorSeamless, - accentPrimary, - accentSecondary, - textPrimary, - textPrimaryInverse, - textSecondary, - textTertiary, - ) - - private fun loadDefaultColor(id: Int): Int { - return Utils.getColorAttr(context, id).defaultColor - } - - fun updateColorScheme(colorScheme: ColorScheme?): Boolean { - var anyChanged = false - colorTransitions.forEach { anyChanged = it.updateColorScheme(colorScheme) || anyChanged } - colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme } - return anyChanged - } -} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt deleted file mode 100644 index 80bff83d03a0..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ /dev/null @@ -1,1143 +0,0 @@ -package com.android.systemui.media - -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.res.ColorStateList -import android.content.res.Configuration -import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS -import android.util.Log -import android.util.MathUtils -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.animation.PathInterpolator -import android.widget.LinearLayout -import androidx.annotation.VisibleForTesting -import com.android.internal.logging.InstanceId -import com.android.systemui.Dumpable -import com.android.systemui.R -import com.android.systemui.classifier.FalsingCollector -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.dump.DumpManager -import com.android.systemui.media.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.qs.PageIndicator -import com.android.systemui.shared.system.SysUiStatsLog -import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener -import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider -import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.util.Utils -import com.android.systemui.util.animation.UniqueObjectHostView -import com.android.systemui.util.animation.requiresRemeasuring -import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.systemui.util.time.SystemClock -import com.android.systemui.util.traceSection -import java.io.PrintWriter -import java.util.TreeMap -import javax.inject.Inject -import javax.inject.Provider - -private const val TAG = "MediaCarouselController" -private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS) -private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) - -/** - * Class that is responsible for keeping the view carousel up to date. - * This also handles changes in state and applies them to the media carousel like the expansion. - */ -@SysUISingleton -class MediaCarouselController @Inject constructor( - private val context: Context, - private val mediaControlPanelFactory: Provider<MediaControlPanel>, - private val visualStabilityProvider: VisualStabilityProvider, - private val mediaHostStatesManager: MediaHostStatesManager, - private val activityStarter: ActivityStarter, - private val systemClock: SystemClock, - @Main executor: DelayableExecutor, - private val mediaManager: MediaDataManager, - configurationController: ConfigurationController, - falsingCollector: FalsingCollector, - falsingManager: FalsingManager, - dumpManager: DumpManager, - private val logger: MediaUiEventLogger, - private val debugLogger: MediaCarouselControllerLogger -) : Dumpable { - /** - * The current width of the carousel - */ - private var currentCarouselWidth: Int = 0 - - /** - * The current height of the carousel - */ - private var currentCarouselHeight: Int = 0 - - /** - * Are we currently showing only active players - */ - private var currentlyShowingOnlyActive: Boolean = false - - /** - * Is the player currently visible (at the end of the transformation - */ - private var playersVisible: Boolean = false - /** - * The desired location where we'll be at the end of the transformation. Usually this matches - * the end location, except when we're still waiting on a state update call. - */ - @MediaLocation - private var desiredLocation: Int = -1 - - /** - * The ending location of the view where it ends when all animations and transitions have - * finished - */ - @MediaLocation - @VisibleForTesting - var currentEndLocation: Int = -1 - - /** - * The ending location of the view where it ends when all animations and transitions have - * finished - */ - @MediaLocation - private var currentStartLocation: Int = -1 - - /** - * The progress of the transition or 1.0 if there is no transition happening - */ - private var currentTransitionProgress: Float = 1.0f - - /** - * The measured width of the carousel - */ - private var carouselMeasureWidth: Int = 0 - - /** - * The measured height of the carousel - */ - private var carouselMeasureHeight: Int = 0 - private var desiredHostState: MediaHostState? = null - private val mediaCarousel: MediaScrollView - val mediaCarouselScrollHandler: MediaCarouselScrollHandler - val mediaFrame: ViewGroup - @VisibleForTesting - lateinit var settingsButton: View - private set - private val mediaContent: ViewGroup - @VisibleForTesting - val pageIndicator: PageIndicator - private val visualStabilityCallback: OnReorderingAllowedListener - private var needsReordering: Boolean = false - private var keysNeedRemoval = mutableSetOf<String>() - var shouldScrollToActivePlayer: Boolean = false - private var isRtl: Boolean = false - set(value) { - if (value != field) { - field = value - mediaFrame.layoutDirection = - if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR - mediaCarouselScrollHandler.scrollToStart() - } - } - private var currentlyExpanded = true - set(value) { - if (field != value) { - field = value - for (player in MediaPlayerData.players()) { - player.setListening(field) - } - } - } - - companion object { - const val ANIMATION_BASE_DURATION = 2200f - const val DURATION = 167f - const val DETAILS_DELAY = 1067f - const val CONTROLS_DELAY = 1400f - const val PAGINATION_DELAY = 1900f - const val MEDIATITLES_DELAY = 1000f - const val MEDIACONTAINERS_DELAY = 967f - val TRANSFORM_BEZIER = PathInterpolator (0.68F, 0F, 0F, 1F) - val REVERSE_BEZIER = PathInterpolator (0F, 0.68F, 1F, 0F) - - fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float { - val transformStartFraction = delay / ANIMATION_BASE_DURATION - val transformDurationFraction = duration / ANIMATION_BASE_DURATION - val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction) - return MathUtils.constrain((squishinessToTime - transformStartFraction) / - transformDurationFraction, 0F, 1F) - } - } - - private val configListener = object : ConfigurationController.ConfigurationListener { - override fun onDensityOrFontScaleChanged() { - // System font changes should only happen when UMO is offscreen or a flicker may occur - updatePlayers(recreateMedia = true) - inflateSettingsButton() - } - - override fun onThemeChanged() { - updatePlayers(recreateMedia = false) - inflateSettingsButton() - } - - override fun onConfigChanged(newConfig: Configuration?) { - if (newConfig == null) return - isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL - } - - override fun onUiModeChanged() { - updatePlayers(recreateMedia = false) - inflateSettingsButton() - } - } - - /** - * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility. - * It will be called when the container is out of view. - */ - lateinit var updateUserVisibility: () -> Unit - lateinit var updateHostVisibility: () -> Unit - - private val isReorderingAllowed: Boolean - get() = visualStabilityProvider.isReorderingAllowed - - init { - dumpManager.registerDumpable(TAG, this) - mediaFrame = inflateMediaCarousel() - mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) - pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) - mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator, - executor, this::onSwipeToDismiss, this::updatePageIndicatorLocation, - this::closeGuts, falsingCollector, falsingManager, this::logSmartspaceImpression, - logger) - isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL - inflateSettingsButton() - mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) - configurationController.addCallback(configListener) - visualStabilityCallback = OnReorderingAllowedListener { - if (needsReordering) { - needsReordering = false - reorderAllPlayers(previousVisiblePlayerKey = null) - } - - keysNeedRemoval.forEach { - removePlayer(it) - } - if (keysNeedRemoval.size > 0) { - // Carousel visibility may need to be updated after late removals - updateHostVisibility() - } - keysNeedRemoval.clear() - - // Update user visibility so that no extra impression will be logged when - // activeMediaIndex resets to 0 - if (this::updateUserVisibility.isInitialized) { - updateUserVisibility() - } - - // Let's reset our scroll position - mediaCarouselScrollHandler.scrollToStart() - } - visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback) - mediaManager.addListener(object : MediaDataManager.Listener { - override fun onMediaDataLoaded( - key: String, - oldKey: String?, - data: MediaData, - immediately: Boolean, - receivedSmartspaceCardLatency: Int, - isSsReactivated: Boolean - ) { - debugLogger.logMediaLoaded(key) - if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) { - // Log card received if a new resumable media card is added - MediaPlayerData.getMediaPlayer(key)?.let { - /* ktlint-disable max-line-length */ - logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED - it.mSmartspaceId, - it.mUid, - surfaces = intArrayOf( - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY), - rank = MediaPlayerData.getMediaPlayerIndex(key)) - /* ktlint-disable max-line-length */ - } - if (mediaCarouselScrollHandler.visibleToUser && - mediaCarouselScrollHandler.visibleMediaIndex - == MediaPlayerData.getMediaPlayerIndex(key)) { - logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) - } - } else if (receivedSmartspaceCardLatency != 0) { - // Log resume card received if resumable media card is reactivated and - // resume card is ranked first - MediaPlayerData.players().forEachIndexed { index, it -> - if (it.recommendationViewHolder == null) { - it.mSmartspaceId = SmallHash.hash(it.mUid + - systemClock.currentTimeMillis().toInt()) - it.mIsImpressed = false - /* ktlint-disable max-line-length */ - logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED - it.mSmartspaceId, - it.mUid, - surfaces = intArrayOf( - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY), - rank = index, - receivedLatencyMillis = receivedSmartspaceCardLatency) - /* ktlint-disable max-line-length */ - } - } - // If media container area already visible to the user, log impression for - // reactivated card. - if (mediaCarouselScrollHandler.visibleToUser && - !mediaCarouselScrollHandler.qsExpanded) { - logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) - } - } - - val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active - if (canRemove && !Utils.useMediaResumption(context)) { - // This view isn't playing, let's remove this! This happens e.g when - // dismissing/timing out a view. We still have the data around because - // resumption could be on, but we should save the resources and release this. - if (isReorderingAllowed) { - onMediaDataRemoved(key) - } else { - keysNeedRemoval.add(key) - } - } else { - keysNeedRemoval.remove(key) - } - } - - override fun onSmartspaceMediaDataLoaded( - key: String, - data: SmartspaceMediaData, - shouldPrioritize: Boolean - ) { - debugLogger.logRecommendationLoaded(key) - // Log the case where the hidden media carousel with the existed inactive resume - // media is shown by the Smartspace signal. - if (data.isActive) { - val hasActivatedExistedResumeMedia = - !mediaManager.hasActiveMedia() && - mediaManager.hasAnyMedia() && - shouldPrioritize - if (hasActivatedExistedResumeMedia) { - // Log resume card received if resumable media card is reactivated and - // recommendation card is valid and ranked first - MediaPlayerData.players().forEachIndexed { index, it -> - if (it.recommendationViewHolder == null) { - it.mSmartspaceId = SmallHash.hash(it.mUid + - systemClock.currentTimeMillis().toInt()) - it.mIsImpressed = false - /* ktlint-disable max-line-length */ - logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED - it.mSmartspaceId, - it.mUid, - surfaces = intArrayOf( - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY), - rank = index, - receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt()) - /* ktlint-disable max-line-length */ - } - } - } - addSmartspaceMediaRecommendations(key, data, shouldPrioritize) - MediaPlayerData.getMediaPlayer(key)?.let { - /* ktlint-disable max-line-length */ - logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED - it.mSmartspaceId, - it.mUid, - surfaces = intArrayOf( - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY), - rank = MediaPlayerData.getMediaPlayerIndex(key), - receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt()) - /* ktlint-disable max-line-length */ - } - if (mediaCarouselScrollHandler.visibleToUser && - mediaCarouselScrollHandler.visibleMediaIndex - == MediaPlayerData.getMediaPlayerIndex(key)) { - logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) - } - } else { - onSmartspaceMediaDataRemoved(data.targetId, immediately = true) - } - } - - override fun onMediaDataRemoved(key: String) { - debugLogger.logMediaRemoved(key) - removePlayer(key) - } - - override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { - debugLogger.logRecommendationRemoved(key, immediately) - if (immediately || isReorderingAllowed) { - removePlayer(key) - if (!immediately) { - // Although it wasn't requested, we were able to process the removal - // immediately since reordering is allowed. So, notify hosts to update - if (this@MediaCarouselController::updateHostVisibility.isInitialized) { - updateHostVisibility() - } - } - } else { - keysNeedRemoval.add(key) - } - } - }) - mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - // The pageIndicator is not laid out yet when we get the current state update, - // Lets make sure we have the right dimensions - updatePageIndicatorLocation() - } - mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback { - override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) { - if (location == desiredLocation) { - onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false) - } - } - }) - } - - private fun inflateSettingsButton() { - val settings = LayoutInflater.from(context).inflate(R.layout.media_carousel_settings_button, - mediaFrame, false) as View - if (this::settingsButton.isInitialized) { - mediaFrame.removeView(settingsButton) - } - settingsButton = settings - mediaFrame.addView(settingsButton) - mediaCarouselScrollHandler.onSettingsButtonUpdated(settings) - settingsButton.setOnClickListener { - logger.logCarouselSettings() - activityStarter.startActivity(settingsIntent, true /* dismissShade */) - } - } - - private fun inflateMediaCarousel(): ViewGroup { - val mediaCarousel = LayoutInflater.from(context).inflate(R.layout.media_carousel, - UniqueObjectHostView(context), false) as ViewGroup - // Because this is inflated when not attached to the true view hierarchy, it resolves some - // potential issues to force that the layout direction is defined by the locale - // (rather than inherited from the parent, which would resolve to LTR when unattached). - mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE - return mediaCarousel - } - - private fun reorderAllPlayers(previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?) { - mediaContent.removeAllViews() - for (mediaPlayer in MediaPlayerData.players()) { - mediaPlayer.mediaViewHolder?.let { - mediaContent.addView(it.player) - } ?: mediaPlayer.recommendationViewHolder?.let { - mediaContent.addView(it.recommendations) - } - } - mediaCarouselScrollHandler.onPlayersChanged() - - // Automatically scroll to the active player if needed - if (shouldScrollToActivePlayer) { - shouldScrollToActivePlayer = false - val activeMediaIndex = MediaPlayerData.firstActiveMediaIndex() - if (activeMediaIndex != -1) { - previousVisiblePlayerKey?.let { - val previousVisibleIndex = MediaPlayerData.playerKeys() - .indexOfFirst { key -> it == key } - mediaCarouselScrollHandler - .scrollToPlayer(previousVisibleIndex, activeMediaIndex) - } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = activeMediaIndex) - } - } - } - - // Returns true if new player is added - private fun addOrUpdatePlayer( - key: String, - oldKey: String?, - data: MediaData, - isSsReactivated: Boolean - ): Boolean = traceSection("MediaCarouselController#addOrUpdatePlayer") { - MediaPlayerData.moveIfExists(oldKey, key) - val existingPlayer = MediaPlayerData.getMediaPlayer(key) - val curVisibleMediaKey = MediaPlayerData.playerKeys() - .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) - val isCurVisibleMediaPlaying = curVisibleMediaKey?.data?.isPlaying - if (existingPlayer == null) { - val newPlayer = mediaControlPanelFactory.get() - newPlayer.attachPlayer(MediaViewHolder.create( - LayoutInflater.from(context), mediaContent)) - newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions - val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT) - newPlayer.mediaViewHolder?.player?.setLayoutParams(lp) - newPlayer.bindPlayer(data, key) - newPlayer.setListening(currentlyExpanded) - MediaPlayerData.addMediaPlayer( - key, data, newPlayer, systemClock, isSsReactivated, debugLogger - ) - updatePlayerToState(newPlayer, noAnimation = true) - if (data.active) { - reorderAllPlayers(curVisibleMediaKey) - } else { - needsReordering = true - } - } else { - existingPlayer.bindPlayer(data, key) - MediaPlayerData.addMediaPlayer( - key, data, existingPlayer, systemClock, isSsReactivated, debugLogger - ) - // Check the playing status of both current visible and new media players - // To make sure we scroll to the active playing media card. - if (isReorderingAllowed || - shouldScrollToActivePlayer && - data.isPlaying == true && - isCurVisibleMediaPlaying == false - ) { - reorderAllPlayers(curVisibleMediaKey) - } else { - needsReordering = true - } - } - updatePageIndicator() - mediaCarouselScrollHandler.onPlayersChanged() - mediaFrame.requiresRemeasuring = true - // Check postcondition: mediaContent should have the same number of children as there are - // elements in mediaPlayers. - if (MediaPlayerData.players().size != mediaContent.childCount) { - Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") - } - return existingPlayer == null - } - - private fun addSmartspaceMediaRecommendations( - key: String, - data: SmartspaceMediaData, - shouldPrioritize: Boolean - ) = traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") { - if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel") - if (MediaPlayerData.getMediaPlayer(key) != null) { - Log.w(TAG, "Skip adding smartspace target in carousel") - return - } - - val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey() - existingSmartspaceMediaKey?.let { - val removedPlayer = MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey) - removedPlayer?.run { debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey) } - } - - val newRecs = mediaControlPanelFactory.get() - newRecs.attachRecommendation( - RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)) - newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions - val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT) - newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp) - newRecs.bindRecommendation(data) - val curVisibleMediaKey = MediaPlayerData.playerKeys() - .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) - MediaPlayerData.addMediaRecommendation( - key, data, newRecs, shouldPrioritize, systemClock, debugLogger - ) - updatePlayerToState(newRecs, noAnimation = true) - reorderAllPlayers(curVisibleMediaKey) - updatePageIndicator() - mediaFrame.requiresRemeasuring = true - // Check postcondition: mediaContent should have the same number of children as there are - // elements in mediaPlayers. - if (MediaPlayerData.players().size != mediaContent.childCount) { - Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") - } - } - - fun removePlayer( - key: String, - dismissMediaData: Boolean = true, - dismissRecommendation: Boolean = true - ) { - if (key == MediaPlayerData.smartspaceMediaKey()) { - MediaPlayerData.smartspaceMediaData?.let { - logger.logRecommendationRemoved(it.packageName, it.instanceId) - } - } - val removed = MediaPlayerData.removeMediaPlayer(key) - removed?.apply { - mediaCarouselScrollHandler.onPrePlayerRemoved(removed) - mediaContent.removeView(removed.mediaViewHolder?.player) - mediaContent.removeView(removed.recommendationViewHolder?.recommendations) - removed.onDestroy() - mediaCarouselScrollHandler.onPlayersChanged() - updatePageIndicator() - - if (dismissMediaData) { - // Inform the media manager of a potentially late dismissal - mediaManager.dismissMediaData(key, delay = 0L) - } - if (dismissRecommendation) { - // Inform the media manager of a potentially late dismissal - mediaManager.dismissSmartspaceRecommendation(key, delay = 0L) - } - } - } - - private fun updatePlayers(recreateMedia: Boolean) { - pageIndicator.tintList = ColorStateList.valueOf( - context.getColor(R.color.media_paging_indicator) - ) - - MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) -> - if (isSsMediaRec) { - val smartspaceMediaData = MediaPlayerData.smartspaceMediaData - removePlayer(key, dismissMediaData = false, dismissRecommendation = false) - smartspaceMediaData?.let { - addSmartspaceMediaRecommendations( - it.targetId, it, MediaPlayerData.shouldPrioritizeSs) - } - } else { - val isSsReactivated = MediaPlayerData.isSsReactivated(key) - if (recreateMedia) { - removePlayer(key, dismissMediaData = false, dismissRecommendation = false) - } - addOrUpdatePlayer( - key = key, oldKey = null, data = data, isSsReactivated = isSsReactivated) - } - } - } - - private fun updatePageIndicator() { - val numPages = mediaContent.getChildCount() - pageIndicator.setNumPages(numPages) - if (numPages == 1) { - pageIndicator.setLocation(0f) - } - updatePageIndicatorAlpha() - } - - /** - * Set a new interpolated state for all players. This is a state that is usually controlled - * by a finger movement where the user drags from one state to the next. - * - * @param startLocation the start location of our state or -1 if this is directly set - * @param endLocation the ending location of our state. - * @param progress the progress of the transition between startLocation and endlocation. If - * this is not a guided transformation, this will be 1.0f - * @param immediately should this state be applied immediately, canceling all animations? - */ - fun setCurrentState( - @MediaLocation startLocation: Int, - @MediaLocation endLocation: Int, - progress: Float, - immediately: Boolean - ) { - if (startLocation != currentStartLocation || - endLocation != currentEndLocation || - progress != currentTransitionProgress || - immediately - ) { - currentStartLocation = startLocation - currentEndLocation = endLocation - currentTransitionProgress = progress - for (mediaPlayer in MediaPlayerData.players()) { - updatePlayerToState(mediaPlayer, immediately) - } - maybeResetSettingsCog() - updatePageIndicatorAlpha() - } - } - - @VisibleForTesting - fun updatePageIndicatorAlpha() { - val hostStates = mediaHostStatesManager.mediaHostStates - val endIsVisible = hostStates[currentEndLocation]?.visible ?: false - val startIsVisible = hostStates[currentStartLocation]?.visible ?: false - val startAlpha = if (startIsVisible) 1.0f else 0.0f - // when squishing in split shade, only use endState, which keeps changing - // to provide squishFraction - val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F - val endAlpha = (if (endIsVisible) 1.0f else 0.0f) * - calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION) - var alpha = 1.0f - if (!endIsVisible || !startIsVisible) { - var progress = currentTransitionProgress - if (!endIsVisible) { - progress = 1.0f - progress - } - // Let's fade in quickly at the end where the view is visible - progress = MathUtils.constrain( - MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress), - 0.0f, - 1.0f) - alpha = MathUtils.lerp(startAlpha, endAlpha, progress) - } - pageIndicator.alpha = alpha - } - - private fun updatePageIndicatorLocation() { - // Update the location of the page indicator, carousel clipping - val translationX = if (isRtl) { - (pageIndicator.width - currentCarouselWidth) / 2.0f - } else { - (currentCarouselWidth - pageIndicator.width) / 2.0f - } - pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation - val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams - pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height - - layoutParams.bottomMargin).toFloat() - } - - /** - * Update the dimension of this carousel. - */ - private fun updateCarouselDimensions() { - var width = 0 - var height = 0 - for (mediaPlayer in MediaPlayerData.players()) { - val controller = mediaPlayer.mediaViewController - // When transitioning the view to gone, the view gets smaller, but the translation - // Doesn't, let's add the translation - width = Math.max(width, controller.currentWidth + controller.translationX.toInt()) - height = Math.max(height, controller.currentHeight + controller.translationY.toInt()) - } - if (width != currentCarouselWidth || height != currentCarouselHeight) { - currentCarouselWidth = width - currentCarouselHeight = height - mediaCarouselScrollHandler.setCarouselBounds( - currentCarouselWidth, currentCarouselHeight) - updatePageIndicatorLocation() - updatePageIndicatorAlpha() - } - } - - private fun maybeResetSettingsCog() { - val hostStates = mediaHostStatesManager.mediaHostStates - val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia - ?: true - val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia - ?: endShowsActive - if (currentlyShowingOnlyActive != endShowsActive || - ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) && - startShowsActive != endShowsActive)) { - // Whenever we're transitioning from between differing states or the endstate differs - // we reset the translation - currentlyShowingOnlyActive = endShowsActive - mediaCarouselScrollHandler.resetTranslation(animate = true) - } - } - - private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) { - mediaPlayer.mediaViewController.setCurrentState( - startLocation = currentStartLocation, - endLocation = currentEndLocation, - transitionProgress = currentTransitionProgress, - applyImmediately = noAnimation) - } - - /** - * The desired location of this view has changed. We should remeasure the view to match - * the new bounds and kick off bounds animations if necessary. - * If an animation is happening, an animation is kicked of externally, which sets a new - * current state until we reach the targetState. - * - * @param desiredLocation the location we're going to - * @param desiredHostState the target state we're transitioning to - * @param animate should this be animated - */ - fun onDesiredLocationChanged( - desiredLocation: Int, - desiredHostState: MediaHostState?, - animate: Boolean, - duration: Long = 200, - startDelay: Long = 0 - ) = traceSection("MediaCarouselController#onDesiredLocationChanged") { - desiredHostState?.let { - if (this.desiredLocation != desiredLocation) { - // Only log an event when location changes - logger.logCarouselPosition(desiredLocation) - } - - // This is a hosting view, let's remeasure our players - this.desiredLocation = desiredLocation - this.desiredHostState = it - currentlyExpanded = it.expansion > 0 - - val shouldCloseGuts = !currentlyExpanded && - !mediaManager.hasActiveMediaOrRecommendation() && - desiredHostState.showsOnlyActiveMedia - - for (mediaPlayer in MediaPlayerData.players()) { - if (animate) { - mediaPlayer.mediaViewController.animatePendingStateChange( - duration = duration, - delay = startDelay) - } - if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) { - mediaPlayer.closeGuts(!animate) - } - - mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation) - } - mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia - mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded - val nowVisible = it.visible - if (nowVisible != playersVisible) { - playersVisible = nowVisible - if (nowVisible) { - mediaCarouselScrollHandler.resetTranslation() - } - } - updateCarouselSize() - } - } - - fun closeGuts(immediate: Boolean = true) { - MediaPlayerData.players().forEach { - it.closeGuts(immediate) - } - } - - /** - * Update the size of the carousel, remeasuring it if necessary. - */ - private fun updateCarouselSize() { - val width = desiredHostState?.measurementInput?.width ?: 0 - val height = desiredHostState?.measurementInput?.height ?: 0 - if (width != carouselMeasureWidth && width != 0 || - height != carouselMeasureHeight && height != 0) { - carouselMeasureWidth = width - carouselMeasureHeight = height - val playerWidthPlusPadding = carouselMeasureWidth + - context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) - // Let's remeasure the carousel - val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0 - val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0 - mediaCarousel.measure(widthSpec, heightSpec) - mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight) - // Update the padding after layout; view widths are used in RTL to calculate scrollX - mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding - } - } - - /** - * Log the user impression for media card at visibleMediaIndex. - */ - fun logSmartspaceImpression(qsExpanded: Boolean) { - val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex - if (MediaPlayerData.players().size > visibleMediaIndex) { - val mediaControlPanel = MediaPlayerData.players().elementAt(visibleMediaIndex) - val hasActiveMediaOrRecommendationCard = - MediaPlayerData.hasActiveMediaOrRecommendationCard() - if (!hasActiveMediaOrRecommendationCard && !qsExpanded) { - // Skip logging if on LS or QQS, and there is no active media card - return - } - logSmartspaceCardReported(800, // SMARTSPACE_CARD_SEEN - mediaControlPanel.mSmartspaceId, - mediaControlPanel.mUid, - intArrayOf(mediaControlPanel.surfaceForSmartspaceLogging)) - mediaControlPanel.mIsImpressed = true - } - } - - @JvmOverloads - /** - * Log Smartspace events - * - * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN) - * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new - * instanceId - * @param uid uid for the application that media comes from - * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when - * the event happened - * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1 - * for tapping on card but not on any media item, 0 for first media item, 1 for second, etc. - * @param interactedSubcardCardinality how many media items were shown to the user when there - * is user interaction - * @param rank the rank for media card in the media carousel, starting from 0 - * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency - * between headphone connection to sysUI displays media recommendation card - * @param isSwipeToDismiss whether is to log swipe-to-dismiss event - * - */ - fun logSmartspaceCardReported( - eventId: Int, - instanceId: Int, - uid: Int, - surfaces: IntArray, - interactedSubcardRank: Int = 0, - interactedSubcardCardinality: Int = 0, - rank: Int = mediaCarouselScrollHandler.visibleMediaIndex, - receivedLatencyMillis: Int = 0, - isSwipeToDismiss: Boolean = false - ) { - if (MediaPlayerData.players().size <= rank) { - return - } - - val mediaControlKey = MediaPlayerData.playerKeys().elementAt(rank) - // Only log media resume card when Smartspace data is available - if (!mediaControlKey.isSsMediaRec && - !mediaManager.smartspaceMediaData.isActive && - MediaPlayerData.smartspaceMediaData == null) { - return - } - - val cardinality = mediaContent.getChildCount() - surfaces.forEach { surface -> - /* ktlint-disable max-line-length */ - SysUiStatsLog.write(SysUiStatsLog.SMARTSPACE_CARD_REPORTED, - eventId, - instanceId, - // Deprecated, replaced with AiAi feature type so we don't need to create logging - // card type for each new feature. - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD, - surface, - // Use -1 as rank value to indicate user swipe to dismiss the card - if (isSwipeToDismiss) -1 else rank, - cardinality, - if (mediaControlKey.isSsMediaRec) - 15 // MEDIA_RECOMMENDATION - else if (mediaControlKey.isSsReactivated) - 43 // MEDIA_RESUME_SS_ACTIVATED - else - 31, // MEDIA_RESUME - uid, - interactedSubcardRank, - interactedSubcardCardinality, - receivedLatencyMillis, - null, // Media cards cannot have subcards. - null // Media cards don't have dimensions today. - ) - /* ktlint-disable max-line-length */ - if (DEBUG) { - Log.d(TAG, "Log Smartspace card event id: $eventId instance id: $instanceId" + - " surface: $surface rank: $rank cardinality: $cardinality " + - "isRecommendationCard: ${mediaControlKey.isSsMediaRec} " + - "isSsReactivated: ${mediaControlKey.isSsReactivated}" + - "uid: $uid " + - "interactedSubcardRank: $interactedSubcardRank " + - "interactedSubcardCardinality: $interactedSubcardCardinality " + - "received_latency_millis: $receivedLatencyMillis") - } - } - } - - private fun onSwipeToDismiss() { - MediaPlayerData.players().forEachIndexed { - index, it -> - if (it.mIsImpressed) { - logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT, - it.mSmartspaceId, - it.mUid, - intArrayOf(it.surfaceForSmartspaceLogging), - rank = index, - isSwipeToDismiss = true) - // Reset card impressed state when swipe to dismissed - it.mIsImpressed = false - } - } - logger.logSwipeDismiss() - mediaManager.onSwipeToDismiss() - } - - fun getCurrentVisibleMediaContentIntent(): PendingIntent? { - return MediaPlayerData.playerKeys() - .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)?.data?.clickIntent - } - - override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.apply { - println("keysNeedRemoval: $keysNeedRemoval") - println("dataKeys: ${MediaPlayerData.dataKeys()}") - println("playerSortKeys: ${MediaPlayerData.playerKeys()}") - println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}") - println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}") - println("current size: $currentCarouselWidth x $currentCarouselHeight") - println("location: $desiredLocation") - println("state: ${desiredHostState?.expansion}, " + - "only active ${desiredHostState?.showsOnlyActiveMedia}") - } - } -} - -@VisibleForTesting -internal object MediaPlayerData { - private val EMPTY = MediaData( - userId = -1, - initialized = false, - app = null, - appIcon = null, - artist = null, - song = null, - artwork = null, - actions = emptyList(), - actionsToShowInCompact = emptyList(), - packageName = "INVALID", - token = null, - clickIntent = null, - device = null, - active = true, - resumeAction = null, - instanceId = InstanceId.fakeInstanceId(-1), - appUid = -1) - // Whether should prioritize Smartspace card. - internal var shouldPrioritizeSs: Boolean = false - private set - internal var smartspaceMediaData: SmartspaceMediaData? = null - private set - - data class MediaSortKey( - val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation. - val data: MediaData, - val updateTime: Long = 0, - val isSsReactivated: Boolean = false - ) - - private val comparator = compareByDescending<MediaSortKey> { - it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL } - .thenByDescending { - it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL } - .thenByDescending { it.data.active } - .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec } - .thenByDescending { !it.data.resumption } - .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE } - .thenByDescending { it.data.lastActive } - .thenByDescending { it.updateTime } - .thenByDescending { it.data.notificationKey } - - private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) - private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() - - fun addMediaPlayer( - key: String, - data: MediaData, - player: MediaControlPanel, - clock: SystemClock, - isSsReactivated: Boolean, - debugLogger: MediaCarouselControllerLogger? = null - ) { - val removedPlayer = removeMediaPlayer(key) - if (removedPlayer != null && removedPlayer != player) { - debugLogger?.logPotentialMemoryLeak(key) - } - val sortKey = MediaSortKey(isSsMediaRec = false, - data, clock.currentTimeMillis(), isSsReactivated = isSsReactivated) - mediaData.put(key, sortKey) - mediaPlayers.put(sortKey, player) - } - - fun addMediaRecommendation( - key: String, - data: SmartspaceMediaData, - player: MediaControlPanel, - shouldPrioritize: Boolean, - clock: SystemClock, - debugLogger: MediaCarouselControllerLogger? = null - ) { - shouldPrioritizeSs = shouldPrioritize - val removedPlayer = removeMediaPlayer(key) - if (removedPlayer != null && removedPlayer != player) { - debugLogger?.logPotentialMemoryLeak(key) - } - val sortKey = MediaSortKey(isSsMediaRec = true, - EMPTY.copy(isPlaying = false), clock.currentTimeMillis(), isSsReactivated = true) - mediaData.put(key, sortKey) - mediaPlayers.put(sortKey, player) - smartspaceMediaData = data - } - - fun moveIfExists( - oldKey: String?, - newKey: String, - debugLogger: MediaCarouselControllerLogger? = null - ) { - if (oldKey == null || oldKey == newKey) { - return - } - - mediaData.remove(oldKey)?.let { - val removedPlayer = removeMediaPlayer(newKey) - removedPlayer?.run { debugLogger?.logPotentialMemoryLeak(newKey) } - mediaData.put(newKey, it) - } - } - - fun getMediaPlayer(key: String): MediaControlPanel? { - return mediaData.get(key)?.let { mediaPlayers.get(it) } - } - - fun getMediaPlayerIndex(key: String): Int { - val sortKey = mediaData.get(key) - mediaPlayers.entries.forEachIndexed { index, e -> - if (e.key == sortKey) { - return index - } - } - return -1 - } - - fun removeMediaPlayer(key: String) = mediaData.remove(key)?.let { - if (it.isSsMediaRec) { - smartspaceMediaData = null - } - mediaPlayers.remove(it) - } - - fun mediaData() = mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) } - - fun dataKeys() = mediaData.keys - - fun players() = mediaPlayers.values - - fun playerKeys() = mediaPlayers.keys - - /** Returns the index of the first non-timeout media. */ - fun firstActiveMediaIndex(): Int { - mediaPlayers.entries.forEachIndexed { index, e -> - if (!e.key.isSsMediaRec && e.key.data.active) { - return index - } - } - return -1 - } - - /** Returns the existing Smartspace target id. */ - fun smartspaceMediaKey(): String? { - mediaData.entries.forEach { e -> - if (e.value.isSsMediaRec) { - return e.key - } - } - return null - } - - @VisibleForTesting - fun clear() { - mediaData.clear() - mediaPlayers.clear() - } - - /* Returns true if there is active media player card or recommendation card */ - fun hasActiveMediaOrRecommendationCard(): Boolean { - if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) { - return true - } - if (firstActiveMediaIndex() != -1) { - return true - } - return false - } - - fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false -} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt index 1ac2a078c8a0..be357ee0ff73 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt @@ -182,8 +182,7 @@ class MediaProjectionAppSelectorActivity( override fun shouldGetOnlyDefaultActivities() = false - // TODO(b/240924732) flip the flag when the recents selector is ready - override fun shouldShowContentPreview() = false + override fun shouldShowContentPreview() = true override fun createContentPreviewView(parent: ViewGroup): ViewGroup = recentsViewController.createView(parent) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt deleted file mode 100644 index d9c58c0d0d76..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media - -import android.media.session.PlaybackState -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.dagger.MediaTimeoutListenerLog -import javax.inject.Inject - -private const val TAG = "MediaTimeout" - -/** - * A buffered log for [MediaTimeoutListener] events - */ -@SysUISingleton -class MediaTimeoutLogger @Inject constructor( - @MediaTimeoutListenerLog private val buffer: LogBuffer -) { - fun logReuseListener(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - }, - { - "reuse listener: $str1" - } - ) - - fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = oldKey - str2 = newKey - bool1 = hadListener - }, - { - "migrate from $str1 to $str2, had listener? $bool1" - } - ) - - fun logUpdateListener(key: String, wasPlaying: Boolean) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - bool1 = wasPlaying - }, - { - "updating $str1, was playing? $bool1" - } - ) - - fun logDelayedUpdate(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - }, - { - "deliver delayed playback state for $str1" - } - ) - - fun logSessionDestroyed(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - }, - { - "session destroyed $str1" - } - ) - - fun logPlaybackState(key: String, state: PlaybackState?) = buffer.log( - TAG, - LogLevel.VERBOSE, - { - str1 = key - str2 = state?.toString() - }, - { - "state update: key=$str1 state=$str2" - } - ) - - fun logStateCallback(key: String) = buffer.log( - TAG, - LogLevel.VERBOSE, - { - str1 = key - }, - { - "dispatching state update for $key" - } - ) - - fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - bool1 = playing - bool2 = resumption - }, - { - "schedule timeout $str1, playing=$bool1 resumption=$bool2" - } - ) - - fun logCancelIgnored(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - }, - { - "cancellation already exists for $str1" - } - ) - - fun logTimeout(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - }, - { - "execute timeout for $str1" - } - ) - - fun logTimeoutCancelled(key: String, reason: String) = buffer.log( - TAG, - LogLevel.VERBOSE, - { - str1 = key - str2 = reason - }, - { - "media timeout cancelled for $str1, reason: $str2" - } - ) -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/GutsViewHolder.kt index 73240b54a27a..531506771459 100644 --- a/packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/GutsViewHolder.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models import android.content.res.ColorStateList import android.util.Log @@ -23,6 +23,9 @@ import android.view.ViewGroup import android.widget.ImageButton import android.widget.TextView import com.android.systemui.R +import com.android.systemui.media.controls.ui.accentPrimaryFromScheme +import com.android.systemui.media.controls.ui.surfaceFromScheme +import com.android.systemui.media.controls.ui.textPrimaryFromScheme import com.android.systemui.monet.ColorScheme /** @@ -95,11 +98,6 @@ class GutsViewHolder constructor(itemView: View) { } companion object { - val ids = setOf( - R.id.remove_text, - R.id.cancel, - R.id.dismiss, - R.id.settings - ) + val ids = setOf(R.id.remove_text, R.id.cancel, R.id.dismiss, R.id.settings) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt index 5b2cda038bbd..f006442906e7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.player import android.app.PendingIntent import android.graphics.drawable.Drawable @@ -27,69 +27,42 @@ import com.android.systemui.R data class MediaData( val userId: Int, val initialized: Boolean = false, - /** - * App name that will be displayed on the player. - */ + /** App name that will be displayed on the player. */ val app: String?, - /** - * App icon shown on player. - */ + /** App icon shown on player. */ val appIcon: Icon?, - /** - * Artist name. - */ + /** Artist name. */ val artist: CharSequence?, - /** - * Song name. - */ + /** Song name. */ val song: CharSequence?, - /** - * Album artwork. - */ + /** Album artwork. */ val artwork: Icon?, - /** - * List of generic action buttons for the media player, based on notification actions - */ + /** List of generic action buttons for the media player, based on notification actions */ val actions: List<MediaAction>, - /** - * Same as above, but shown on smaller versions of the player, like in QQS or keyguard. - */ + /** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */ val actionsToShowInCompact: List<Int>, /** - * Semantic actions buttons, based on the PlaybackState of the media session. - * If present, these actions will be preferred in the UI over [actions] + * Semantic actions buttons, based on the PlaybackState of the media session. If present, these + * actions will be preferred in the UI over [actions] */ val semanticActions: MediaButton? = null, - /** - * Package name of the app that's posting the media. - */ + /** Package name of the app that's posting the media. */ val packageName: String, - /** - * Unique media session identifier. - */ + /** Unique media session identifier. */ val token: MediaSession.Token?, - /** - * Action to perform when the player is tapped. - * This is unrelated to {@link #actions}. - */ + /** Action to perform when the player is tapped. This is unrelated to {@link #actions}. */ val clickIntent: PendingIntent?, - /** - * Where the media is playing: phone, headphones, ear buds, remote session. - */ + /** Where the media is playing: phone, headphones, ear buds, remote session. */ val device: MediaDeviceData?, /** - * When active, a player will be displayed on keyguard and quick-quick settings. - * This is unrelated to the stream being playing or not, a player will not be active if - * timed out, or in resumption mode. + * When active, a player will be displayed on keyguard and quick-quick settings. This is + * unrelated to the stream being playing or not, a player will not be active if timed out, or in + * resumption mode. */ var active: Boolean, - /** - * Action that should be performed to restart a non active session. - */ + /** Action that should be performed to restart a non active session. */ var resumeAction: Runnable?, - /** - * Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE - */ + /** Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE */ var playbackLocation: Int = PLAYBACK_LOCAL, /** * Indicates that this player is a resumption player (ie. It only shows a play actions which @@ -102,29 +75,19 @@ data class MediaData( val notificationKey: String? = null, var hasCheckedForResume: Boolean = false, - /** - * If apps do not report PlaybackState, set as null to imply 'undetermined' - */ + /** If apps do not report PlaybackState, set as null to imply 'undetermined' */ val isPlaying: Boolean? = null, - /** - * Set from the notification and used as fallback when PlaybackState cannot be determined - */ + /** Set from the notification and used as fallback when PlaybackState cannot be determined */ val isClearable: Boolean = true, - /** - * Timestamp when this player was last active. - */ + /** Timestamp when this player was last active. */ var lastActive: Long = 0L, - /** - * Instance ID for logging purposes - */ + /** Instance ID for logging purposes */ val instanceId: InstanceId, - /** - * The UID of the app, used for logging - */ + /** The UID of the app, used for logging */ val appUid: Int ) { companion object { @@ -141,37 +104,21 @@ data class MediaData( } } -/** - * Contains [MediaAction] objects which represent specific buttons in the UI - */ +/** Contains [MediaAction] objects which represent specific buttons in the UI */ data class MediaButton( - /** - * Play/pause button - */ + /** Play/pause button */ val playOrPause: MediaAction? = null, - /** - * Next button, or custom action - */ + /** Next button, or custom action */ val nextOrCustom: MediaAction? = null, - /** - * Previous button, or custom action - */ + /** Previous button, or custom action */ val prevOrCustom: MediaAction? = null, - /** - * First custom action space - */ + /** First custom action space */ val custom0: MediaAction? = null, - /** - * Second custom action space - */ + /** Second custom action space */ val custom1: MediaAction? = null, - /** - * Whether to reserve the empty space when the nextOrCustom is null - */ + /** Whether to reserve the empty space when the nextOrCustom is null */ val reserveNext: Boolean = false, - /** - * Whether to reserve the empty space when the prevOrCustom is null - */ + /** Whether to reserve the empty space when the prevOrCustom is null */ val reservePrev: Boolean = false ) { fun getActionById(id: Int): MediaAction? { @@ -201,7 +148,8 @@ data class MediaAction( /** State of the media device. */ data class MediaDeviceData -@JvmOverloads constructor( +@JvmOverloads +constructor( /** Whether or not to enable the chip */ val enabled: Boolean, @@ -221,8 +169,8 @@ data class MediaDeviceData val showBroadcastButton: Boolean ) { /** - * Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon - * is ignored because it can change by reference frequently depending on the device type's + * Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon is + * ignored because it can change by reference frequently depending on the device type's * implementation, but this is not usually relevant unless other info has changed */ fun equalsWithoutIcon(other: MediaDeviceData?): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt index fc9515c050e5..2511324a943e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.player import android.view.LayoutInflater import android.view.View @@ -25,13 +25,12 @@ import android.widget.SeekBar import android.widget.TextView import androidx.constraintlayout.widget.Barrier import com.android.systemui.R +import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.util.animation.TransitionLayout private const val TAG = "MediaViewHolder" -/** - * Holder class for media player view - */ +/** Holder class for media player view */ class MediaViewHolder constructor(itemView: View) { val player = itemView as TransitionLayout @@ -52,8 +51,7 @@ class MediaViewHolder constructor(itemView: View) { // These views are only shown while the user is actively scrubbing val scrubbingElapsedTimeView: TextView = itemView.requireViewById(R.id.media_scrubbing_elapsed_time) - val scrubbingTotalTimeView: TextView = - itemView.requireViewById(R.id.media_scrubbing_total_time) + val scrubbingTotalTimeView: TextView = itemView.requireViewById(R.id.media_scrubbing_total_time) val gutsViewHolder = GutsViewHolder(itemView) @@ -86,15 +84,7 @@ class MediaViewHolder constructor(itemView: View) { } fun getTransparentActionButtons(): List<ImageButton> { - return listOf( - actionNext, - actionPrev, - action0, - action1, - action2, - action3, - action4 - ) + return listOf(actionNext, actionPrev, action0, action1, action2, action3, action4) } fun marquee(start: Boolean, delay: Long) { @@ -108,10 +98,8 @@ class MediaViewHolder constructor(itemView: View) { * @param inflater LayoutInflater to use to inflate the layout. * @param parent Parent of inflated view. */ - @JvmStatic fun create( - inflater: LayoutInflater, - parent: ViewGroup - ): MediaViewHolder { + @JvmStatic + fun create(inflater: LayoutInflater, parent: ViewGroup): MediaViewHolder { val mediaView = inflater.inflate(R.layout.media_session_view, parent, false) mediaView.setLayerType(View.LAYER_TYPE_HARDWARE, null) // Because this media view (a TransitionLayout) is used to measure and layout the views @@ -124,7 +112,8 @@ class MediaViewHolder constructor(itemView: View) { } } - val controlsIds = setOf( + val controlsIds = + setOf( R.id.icon, R.id.app_name, R.id.header_title, @@ -142,27 +131,23 @@ class MediaViewHolder constructor(itemView: View) { R.id.icon, R.id.media_scrubbing_elapsed_time, R.id.media_scrubbing_total_time - ) + ) // Buttons used for notification-based actions - val genericButtonIds = setOf( - R.id.action0, - R.id.action1, - R.id.action2, - R.id.action3, - R.id.action4 - ) - - val expandedBottomActionIds = setOf( - R.id.actionPrev, - R.id.actionNext, - R.id.action0, - R.id.action1, - R.id.action2, - R.id.action3, - R.id.action4, - R.id.media_scrubbing_elapsed_time, - R.id.media_scrubbing_total_time - ) + val genericButtonIds = + setOf(R.id.action0, R.id.action1, R.id.action2, R.id.action3, R.id.action4) + + val expandedBottomActionIds = + setOf( + R.id.actionPrev, + R.id.actionNext, + R.id.action0, + R.id.action1, + R.id.action2, + R.id.action3, + R.id.action4, + R.id.media_scrubbing_elapsed_time, + R.id.media_scrubbing_total_time + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt index 121021f19f70..37d956bd09eb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.player import android.animation.Animator import android.animation.ObjectAnimator @@ -24,40 +24,56 @@ import androidx.lifecycle.Observer import com.android.internal.annotations.VisibleForTesting import com.android.systemui.R import com.android.systemui.animation.Interpolators +import com.android.systemui.media.controls.ui.SquigglyProgress /** * Observer for changes from SeekBarViewModel. * * <p>Updates the seek bar views in response to changes to the model. */ -open class SeekBarObserver( - private val holder: MediaViewHolder -) : Observer<SeekBarViewModel.Progress> { +open class SeekBarObserver(private val holder: MediaViewHolder) : + Observer<SeekBarViewModel.Progress> { companion object { @JvmStatic val RESET_ANIMATION_DURATION_MS: Int = 750 @JvmStatic val RESET_ANIMATION_THRESHOLD_MS: Int = 250 } - val seekBarEnabledMaxHeight = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_height) - val seekBarDisabledHeight = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_height) - val seekBarEnabledVerticalPadding = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_session_enabled_seekbar_vertical_padding) - val seekBarDisabledVerticalPadding = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_session_disabled_seekbar_vertical_padding) + val seekBarEnabledMaxHeight = + holder.seekBar.context.resources.getDimensionPixelSize( + R.dimen.qs_media_enabled_seekbar_height + ) + val seekBarDisabledHeight = + holder.seekBar.context.resources.getDimensionPixelSize( + R.dimen.qs_media_disabled_seekbar_height + ) + val seekBarEnabledVerticalPadding = + holder.seekBar.context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_enabled_seekbar_vertical_padding + ) + val seekBarDisabledVerticalPadding = + holder.seekBar.context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_disabled_seekbar_vertical_padding + ) var seekBarResetAnimator: Animator? = null init { - val seekBarProgressWavelength = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength).toFloat() - val seekBarProgressAmplitude = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude).toFloat() - val seekBarProgressPhase = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_phase).toFloat() - val seekBarProgressStrokeWidth = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_stroke_width).toFloat() + val seekBarProgressWavelength = + holder.seekBar.context.resources + .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength) + .toFloat() + val seekBarProgressAmplitude = + holder.seekBar.context.resources + .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude) + .toFloat() + val seekBarProgressPhase = + holder.seekBar.context.resources + .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_phase) + .toFloat() + val seekBarProgressStrokeWidth = + holder.seekBar.context.resources + .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_stroke_width) + .toFloat() val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress progressDrawable?.let { it.waveLength = seekBarProgressWavelength @@ -97,16 +113,18 @@ open class SeekBarObserver( } holder.seekBar.setMax(data.duration) - val totalTimeString = DateUtils.formatElapsedTime( - data.duration / DateUtils.SECOND_IN_MILLIS) + val totalTimeString = + DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS) if (data.scrubbing) { holder.scrubbingTotalTimeView.text = totalTimeString } data.elapsedTime?.let { if (!data.scrubbing && !(seekBarResetAnimator?.isRunning ?: false)) { - if (it <= RESET_ANIMATION_THRESHOLD_MS && - holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS) { + if ( + it <= RESET_ANIMATION_THRESHOLD_MS && + holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS + ) { // This animation resets for every additional update to zero. val animator = buildResetAnimator(it) animator.start() @@ -116,24 +134,29 @@ open class SeekBarObserver( } } - val elapsedTimeString = DateUtils.formatElapsedTime( - it / DateUtils.SECOND_IN_MILLIS) + val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS) if (data.scrubbing) { holder.scrubbingElapsedTimeView.text = elapsedTimeString } - holder.seekBar.contentDescription = holder.seekBar.context.getString( - R.string.controls_media_seekbar_description, - elapsedTimeString, - totalTimeString - ) + holder.seekBar.contentDescription = + holder.seekBar.context.getString( + R.string.controls_media_seekbar_description, + elapsedTimeString, + totalTimeString + ) } } @VisibleForTesting open fun buildResetAnimator(targetTime: Int): Animator { - val animator = ObjectAnimator.ofInt(holder.seekBar, "progress", - holder.seekBar.progress, targetTime + RESET_ANIMATION_DURATION_MS) + val animator = + ObjectAnimator.ofInt( + holder.seekBar, + "progress", + holder.seekBar.progress, + targetTime + RESET_ANIMATION_DURATION_MS + ) animator.setAutoCancel(true) animator.duration = RESET_ANIMATION_DURATION_MS.toLong() animator.interpolator = Interpolators.EMPHASIZED diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt index 0f78a1e2ff50..bba5f350dd16 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.player import android.media.MediaMetadata import android.media.session.MediaController @@ -42,8 +42,8 @@ private const val MIN_FLING_VELOCITY_SCALE_FACTOR = 10 private fun PlaybackState.isInMotion(): Boolean { return this.state == PlaybackState.STATE_PLAYING || - this.state == PlaybackState.STATE_FAST_FORWARDING || - this.state == PlaybackState.STATE_REWINDING + this.state == PlaybackState.STATE_FAST_FORWARDING || + this.state == PlaybackState.STATE_REWINDING } /** @@ -59,8 +59,8 @@ private fun PlaybackState.computePosition(duration: Long): Long { val updateTime = this.getLastPositionUpdateTime() val currentTime = SystemClock.elapsedRealtime() if (updateTime > 0) { - var position = (this.playbackSpeed * (currentTime - updateTime)).toLong() + - this.getPosition() + var position = + (this.playbackSpeed * (currentTime - updateTime)).toLong() + this.getPosition() if (duration >= 0 && position > duration) { position = duration.toLong() } else if (position < 0) { @@ -73,7 +73,9 @@ private fun PlaybackState.computePosition(duration: Long): Long { } /** ViewModel for seek bar in QS media player. */ -class SeekBarViewModel @Inject constructor( +class SeekBarViewModel +@Inject +constructor( @Background private val bgExecutor: RepeatableExecutor, private val falsingManager: FalsingManager, ) { @@ -86,9 +88,7 @@ class SeekBarViewModel @Inject constructor( } _progress.postValue(value) } - private val _progress = MutableLiveData<Progress>().apply { - postValue(_data) - } + private val _progress = MutableLiveData<Progress>().apply { postValue(_data) } val progress: LiveData<Progress> get() = _progress private var controller: MediaController? = null @@ -100,20 +100,21 @@ class SeekBarViewModel @Inject constructor( } } private var playbackState: PlaybackState? = null - private var callback = object : MediaController.Callback() { - override fun onPlaybackStateChanged(state: PlaybackState?) { - playbackState = state - if (playbackState == null || PlaybackState.STATE_NONE.equals(playbackState)) { - clearController() - } else { - checkIfPollingNeeded() + private var callback = + object : MediaController.Callback() { + override fun onPlaybackStateChanged(state: PlaybackState?) { + playbackState = state + if (playbackState == null || PlaybackState.STATE_NONE.equals(playbackState)) { + clearController() + } else { + checkIfPollingNeeded() + } } - } - override fun onSessionDestroyed() { - clearController() + override fun onSessionDestroyed() { + clearController() + } } - } private var cancel: Runnable? = null /** Indicates if the seek interaction is considered a false guesture. */ @@ -121,12 +122,13 @@ class SeekBarViewModel @Inject constructor( /** Listening state (QS open or closed) is used to control polling of progress. */ var listening = true - set(value) = bgExecutor.execute { - if (field != value) { - field = value - checkIfPollingNeeded() + set(value) = + bgExecutor.execute { + if (field != value) { + field = value + checkIfPollingNeeded() + } } - } private var scrubbingChangeListener: ScrubbingChangeListener? = null private var enabledChangeListener: EnabledChangeListener? = null @@ -144,14 +146,13 @@ class SeekBarViewModel @Inject constructor( lateinit var logSeek: () -> Unit - /** - * Event indicating that the user has started interacting with the seek bar. - */ + /** Event indicating that the user has started interacting with the seek bar. */ @AnyThread - fun onSeekStarting() = bgExecutor.execute { - scrubbing = true - isFalseSeek = false - } + fun onSeekStarting() = + bgExecutor.execute { + scrubbing = true + isFalseSeek = false + } /** * Event indicating that the user has moved the seek bar. @@ -159,47 +160,51 @@ class SeekBarViewModel @Inject constructor( * @param position Current location in the track. */ @AnyThread - fun onSeekProgress(position: Long) = bgExecutor.execute { - if (scrubbing) { - // The user hasn't yet finished their touch gesture, so only update the data for visual - // feedback and don't update [controller] yet. - _data = _data.copy(elapsedTime = position.toInt()) - } else { - // The seek progress came from an a11y action and we should immediately update to the - // new position. (a11y actions to change the seekbar position don't trigger - // SeekBar.OnSeekBarChangeListener.onStartTrackingTouch or onStopTrackingTouch.) - onSeek(position) + fun onSeekProgress(position: Long) = + bgExecutor.execute { + if (scrubbing) { + // The user hasn't yet finished their touch gesture, so only update the data for + // visual + // feedback and don't update [controller] yet. + _data = _data.copy(elapsedTime = position.toInt()) + } else { + // The seek progress came from an a11y action and we should immediately update to + // the + // new position. (a11y actions to change the seekbar position don't trigger + // SeekBar.OnSeekBarChangeListener.onStartTrackingTouch or onStopTrackingTouch.) + onSeek(position) + } } - } - /** - * Event indicating that the seek interaction is a false gesture and it should be ignored. - */ + /** Event indicating that the seek interaction is a false gesture and it should be ignored. */ @AnyThread - fun onSeekFalse() = bgExecutor.execute { - if (scrubbing) { - isFalseSeek = true + fun onSeekFalse() = + bgExecutor.execute { + if (scrubbing) { + isFalseSeek = true + } } - } /** * Handle request to change the current position in the media track. * @param position Place to seek to in the track. */ @AnyThread - fun onSeek(position: Long) = bgExecutor.execute { - if (isFalseSeek) { - scrubbing = false - checkPlaybackPosition() - } else { - logSeek() - controller?.transportControls?.seekTo(position) - // Invalidate the cached playbackState to avoid the thumb jumping back to the previous - // position. - playbackState = null - scrubbing = false + fun onSeek(position: Long) = + bgExecutor.execute { + if (isFalseSeek) { + scrubbing = false + checkPlaybackPosition() + } else { + logSeek() + controller?.transportControls?.seekTo(position) + // Invalidate the cached playbackState to avoid the thumb jumping back to the + // previous + // position. + playbackState = null + scrubbing = false + } } - } /** * Updates media information. @@ -216,11 +221,18 @@ class SeekBarViewModel @Inject constructor( val seekAvailable = ((playbackState?.actions ?: 0L) and PlaybackState.ACTION_SEEK_TO) != 0L val position = playbackState?.position?.toInt() val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() ?: 0 - val playing = NotificationMediaManager - .isPlayingState(playbackState?.state ?: PlaybackState.STATE_NONE) - val enabled = if (playbackState == null || - playbackState?.getState() == PlaybackState.STATE_NONE || - (duration <= 0)) false else true + val playing = + NotificationMediaManager.isPlayingState( + playbackState?.state ?: PlaybackState.STATE_NONE + ) + val enabled = + if ( + playbackState == null || + playbackState?.getState() == PlaybackState.STATE_NONE || + (duration <= 0) + ) + false + else true _data = Progress(enabled, seekAvailable, playing, scrubbing, position, duration) checkIfPollingNeeded() } @@ -231,26 +243,26 @@ class SeekBarViewModel @Inject constructor( * This should be called when the media session behind the controller has been destroyed. */ @AnyThread - fun clearController() = bgExecutor.execute { - controller = null - playbackState = null - cancel?.run() - cancel = null - _data = _data.copy(enabled = false) - } + fun clearController() = + bgExecutor.execute { + controller = null + playbackState = null + cancel?.run() + cancel = null + _data = _data.copy(enabled = false) + } - /** - * Call to clean up any resources. - */ + /** Call to clean up any resources. */ @AnyThread - fun onDestroy() = bgExecutor.execute { - controller = null - playbackState = null - cancel?.run() - cancel = null - scrubbingChangeListener = null - enabledChangeListener = null - } + fun onDestroy() = + bgExecutor.execute { + controller = null + playbackState = null + cancel?.run() + cancel = null + scrubbingChangeListener = null + enabledChangeListener = null + } @WorkerThread private fun checkPlaybackPosition() { @@ -266,8 +278,12 @@ class SeekBarViewModel @Inject constructor( val needed = listening && !scrubbing && playbackState?.isInMotion() ?: false if (needed) { if (cancel == null) { - cancel = bgExecutor.executeRepeatedly(this::checkPlaybackPosition, 0L, - POSITION_UPDATE_INTERVAL_MILLIS) + cancel = + bgExecutor.executeRepeatedly( + this::checkPlaybackPosition, + 0L, + POSITION_UPDATE_INTERVAL_MILLIS + ) } } else { cancel?.run() @@ -353,9 +369,10 @@ class SeekBarViewModel @Inject constructor( // Gesture detector helps decide which touch events to intercept. private val detector = GestureDetectorCompat(bar.context, this) // Velocity threshold used to decide when a fling is considered a false gesture. - private val flingVelocity: Int = ViewConfiguration.get(bar.context).run { - getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_SCALE_FACTOR - } + private val flingVelocity: Int = + ViewConfiguration.get(bar.context).run { + getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_SCALE_FACTOR + } // Indicates if the gesture should go to the seek bar or if it should be intercepted. private var shouldGoToSeekBar = false @@ -385,9 +402,9 @@ class SeekBarViewModel @Inject constructor( /** * Handle down events that press down on the thumb. * - * On the down action, determine a target box around the thumb to know when a scroll - * gesture starts by clicking on the thumb. The target box will be used by subsequent - * onScroll events. + * On the down action, determine a target box around the thumb to know when a scroll gesture + * starts by clicking on the thumb. The target box will be used by subsequent onScroll + * events. * * Returns true when the down event hits within the target box of the thumb. */ @@ -398,17 +415,19 @@ class SeekBarViewModel @Inject constructor( // TODO: account for thumb offset val progress = bar.getProgress() val range = bar.max - bar.min - val widthFraction = if (range > 0) { - (progress - bar.min).toDouble() / range - } else { - 0.0 - } + val widthFraction = + if (range > 0) { + (progress - bar.min).toDouble() / range + } else { + 0.0 + } val availableWidth = bar.width - padL - padR - val thumbX = if (bar.isLayoutRtl()) { - padL + availableWidth * (1 - widthFraction) - } else { - padL + availableWidth * widthFraction - } + val thumbX = + if (bar.isLayoutRtl()) { + padL + availableWidth * (1 - widthFraction) + } else { + padL + availableWidth * widthFraction + } // Set the min, max boundaries of the thumb box. // I'm cheating by using the height of the seek bar as the width of the box. val halfHeight: Int = bar.height / 2 diff --git a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt index 8ae75fc34acb..1a10b18a5a69 100644 --- a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.recommendation import android.view.LayoutInflater import android.view.View @@ -22,6 +22,8 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import com.android.systemui.R +import com.android.systemui.media.controls.models.GutsViewHolder +import com.android.systemui.media.controls.ui.IlluminationDrawable import com.android.systemui.util.animation.TransitionLayout private const val TAG = "RecommendationViewHolder" @@ -33,26 +35,30 @@ class RecommendationViewHolder private constructor(itemView: View) { // Recommendation screen val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon) - val mediaCoverItems = listOf<ImageView>( - itemView.requireViewById(R.id.media_cover1), - itemView.requireViewById(R.id.media_cover2), - itemView.requireViewById(R.id.media_cover3) - ) - val mediaCoverContainers = listOf<ViewGroup>( - itemView.requireViewById(R.id.media_cover1_container), - itemView.requireViewById(R.id.media_cover2_container), - itemView.requireViewById(R.id.media_cover3_container) - ) - val mediaTitles: List<TextView> = listOf( - itemView.requireViewById(R.id.media_title1), - itemView.requireViewById(R.id.media_title2), - itemView.requireViewById(R.id.media_title3) - ) - val mediaSubtitles: List<TextView> = listOf( - itemView.requireViewById(R.id.media_subtitle1), - itemView.requireViewById(R.id.media_subtitle2), - itemView.requireViewById(R.id.media_subtitle3) - ) + val mediaCoverItems = + listOf<ImageView>( + itemView.requireViewById(R.id.media_cover1), + itemView.requireViewById(R.id.media_cover2), + itemView.requireViewById(R.id.media_cover3) + ) + val mediaCoverContainers = + listOf<ViewGroup>( + itemView.requireViewById(R.id.media_cover1_container), + itemView.requireViewById(R.id.media_cover2_container), + itemView.requireViewById(R.id.media_cover3_container) + ) + val mediaTitles: List<TextView> = + listOf( + itemView.requireViewById(R.id.media_title1), + itemView.requireViewById(R.id.media_title2), + itemView.requireViewById(R.id.media_title3) + ) + val mediaSubtitles: List<TextView> = + listOf( + itemView.requireViewById(R.id.media_subtitle1), + itemView.requireViewById(R.id.media_subtitle2), + itemView.requireViewById(R.id.media_subtitle3) + ) val gutsViewHolder = GutsViewHolder(itemView) @@ -76,13 +82,14 @@ class RecommendationViewHolder private constructor(itemView: View) { * @param inflater LayoutInflater to use to inflate the layout. * @param parent Parent of inflated view. */ - @JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup): - RecommendationViewHolder { + @JvmStatic + fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder { val itemView = inflater.inflate( R.layout.media_smartspace_recommendations, parent, - false /* attachToRoot */) + false /* attachToRoot */ + ) // Because this media view (a TransitionLayout) is used to measure and layout the views // in various states before being attached to its parent, we can't depend on the default // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction. @@ -91,35 +98,38 @@ class RecommendationViewHolder private constructor(itemView: View) { } // Res Ids for the control components on the recommendation view. - val controlsIds = setOf( - R.id.recommendation_card_icon, - R.id.media_cover1, - R.id.media_cover2, - R.id.media_cover3, - R.id.media_cover1_container, - R.id.media_cover2_container, - R.id.media_cover3_container, - R.id.media_title1, - R.id.media_title2, - R.id.media_title3, - R.id.media_subtitle1, - R.id.media_subtitle2, - R.id.media_subtitle3 - ) + val controlsIds = + setOf( + R.id.recommendation_card_icon, + R.id.media_cover1, + R.id.media_cover2, + R.id.media_cover3, + R.id.media_cover1_container, + R.id.media_cover2_container, + R.id.media_cover3_container, + R.id.media_title1, + R.id.media_title2, + R.id.media_title3, + R.id.media_subtitle1, + R.id.media_subtitle2, + R.id.media_subtitle3 + ) - val mediaTitlesAndSubtitlesIds = setOf( - R.id.media_title1, - R.id.media_title2, - R.id.media_title3, - R.id.media_subtitle1, - R.id.media_subtitle2, - R.id.media_subtitle3 - ) + val mediaTitlesAndSubtitlesIds = + setOf( + R.id.media_title1, + R.id.media_title2, + R.id.media_title3, + R.id.media_subtitle1, + R.id.media_subtitle2, + R.id.media_subtitle3 + ) - val mediaContainersIds = setOf( - R.id.media_cover1_container, - R.id.media_cover2_container, - R.id.media_cover3_container - ) + val mediaContainersIds = + setOf( + R.id.media_cover1_container, + R.id.media_cover2_container, + R.id.media_cover3_container + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt index c8f17d93bcc8..1df42c641df6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.recommendation import android.app.smartspace.SmartspaceAction import android.content.Context @@ -22,55 +22,41 @@ import android.content.Intent import android.content.pm.PackageManager import android.text.TextUtils import android.util.Log +import androidx.annotation.VisibleForTesting import com.android.internal.logging.InstanceId -import com.android.systemui.media.MediaControlPanel.KEY_SMARTSPACE_APP_NAME + +@VisibleForTesting const val KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME" /** State of a Smartspace media recommendations view. */ data class SmartspaceMediaData( - /** - * Unique id of a Smartspace media target. - */ + /** Unique id of a Smartspace media target. */ val targetId: String, - /** - * Indicates if the status is active. - */ + /** Indicates if the status is active. */ val isActive: Boolean, - /** - * Package name of the media recommendations' provider-app. - */ + /** Package name of the media recommendations' provider-app. */ val packageName: String, - /** - * Action to perform when the card is tapped. Also contains the target's extra info. - */ + /** Action to perform when the card is tapped. Also contains the target's extra info. */ val cardAction: SmartspaceAction?, - /** - * List of media recommendations. - */ + /** List of media recommendations. */ val recommendations: List<SmartspaceAction>, - /** - * Intent for the user's initiated dismissal. - */ + /** Intent for the user's initiated dismissal. */ val dismissIntent: Intent?, - /** - * The timestamp in milliseconds that headphone is connected. - */ + /** The timestamp in milliseconds that headphone is connected. */ val headphoneConnectionTimeMillis: Long, - /** - * Instance ID for [MediaUiEventLogger] - */ + /** Instance ID for [MediaUiEventLogger] */ val instanceId: InstanceId ) { /** * Indicates if all the data is valid. * * TODO(b/230333302): Make MediaControlPanel more flexible so that we can display fewer than + * ``` * [NUM_REQUIRED_RECOMMENDATIONS]. + * ``` */ fun isValid() = getValidRecommendations().size >= NUM_REQUIRED_RECOMMENDATIONS - /** - * Returns the list of [recommendations] that have valid data. - */ + /** Returns the list of [recommendations] that have valid data. */ fun getValidRecommendations() = recommendations.filter { it.icon != null } /** Returns the upstream app name if available. */ @@ -89,9 +75,10 @@ data class SmartspaceMediaData( Log.w( TAG, "Package $packageName does not have a main launcher activity. " + - "Fallback to full app name") + "Fallback to full app name" + ) return try { - val applicationInfo = packageManager.getApplicationInfo(packageName, /* flags= */ 0) + val applicationInfo = packageManager.getApplicationInfo(packageName, /* flags= */ 0) packageManager.getApplicationLabel(applicationInfo) } catch (e: PackageManager.NameNotFoundException) { null diff --git a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataProvider.kt index 140a1fef93f7..a7ed69a9ab73 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataProvider.kt @@ -1,4 +1,20 @@ -package com.android.systemui.media +/* + * 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.media.controls.models.recommendation import android.app.smartspace.SmartspaceTarget import android.util.Log @@ -23,7 +39,7 @@ class SmartspaceMediaDataProvider @Inject constructor() : BcSmartspaceDataPlugin smartspaceMediaTargetListeners.remove(smartspaceTargetListener) } - /** Updates Smartspace data and propagates it to any listeners. */ + /** Updates Smartspace data and propagates it to any listeners. */ override fun onTargetsAvailable(targets: List<SmartspaceTarget>) { // Filter out non-media targets. val mediaTargets = mutableListOf<SmartspaceTarget>() diff --git a/packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt index 94a0835f22f8..ff763d81f950 100644 --- a/packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt @@ -14,20 +14,18 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.content.Context - import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.media.InfoMediaManager import com.android.settingslib.media.LocalMediaManager - import javax.inject.Inject -/** - * Factory to create [LocalMediaManager] objects. - */ -class LocalMediaManagerFactory @Inject constructor( +/** Factory to create [LocalMediaManager] objects. */ +class LocalMediaManagerFactory +@Inject +constructor( private val context: Context, private val localBluetoothManager: LocalBluetoothManager? ) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatest.kt index 311973ad5af0..789ef407ea9d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatest.kt @@ -14,15 +14,16 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaDeviceData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import javax.inject.Inject -/** - * Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events. - */ -class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, - MediaDeviceManager.Listener { +/** Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events. */ +class MediaDataCombineLatest @Inject constructor() : + MediaDataManager.Listener, MediaDeviceManager.Listener { private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf() @@ -60,11 +61,7 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } } - override fun onMediaDeviceChanged( - key: String, - oldKey: String?, - data: MediaDeviceData? - ) { + override fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) { if (oldKey != null && oldKey != key && entries.contains(oldKey)) { entries[key] = entries.remove(oldKey)?.first to data update(key, oldKey) @@ -83,9 +80,7 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, */ fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) - /** - * Remove a listener registered with addListener. - */ + /** Remove a listener registered with addListener. */ fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener) private fun update(key: String, oldKey: String?) { @@ -93,18 +88,14 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, if (entry != null && device != null) { val data = entry.copy(device = device) val listenersCopy = listeners.toSet() - listenersCopy.forEach { - it.onMediaDataLoaded(key, oldKey, data) - } + listenersCopy.forEach { it.onMediaDataLoaded(key, oldKey, data) } } } private fun remove(key: String) { entries.remove(key)?.let { val listenersCopy = listeners.toSet() - listenersCopy.forEach { - it.onMediaDataRemoved(key) - } + listenersCopy.forEach { it.onMediaDataRemoved(key) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt index e0c8d66cb6fd..45b319b274b2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.content.Context import android.os.SystemProperties @@ -23,6 +23,9 @@ import com.android.internal.annotations.VisibleForTesting import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.settings.CurrentUserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.util.time.SystemClock @@ -34,7 +37,8 @@ import kotlin.collections.LinkedHashMap private const val TAG = "MediaDataFilter" private const val DEBUG = true -private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = ("com.google" + +private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = + ("com.google" + ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity") private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds" @@ -43,8 +47,8 @@ private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age * available within this time window, smartspace recommendations will be shown instead. */ @VisibleForTesting -internal val SMARTSPACE_MAX_AGE = SystemProperties - .getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30)) +internal val SMARTSPACE_MAX_AGE = + SystemProperties.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30)) /** * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user @@ -54,7 +58,9 @@ internal val SMARTSPACE_MAX_AGE = SystemProperties * This is added at the end of the pipeline since we may still need to handle callbacks from * background users (e.g. timeouts). */ -class MediaDataFilter @Inject constructor( +class MediaDataFilter +@Inject +constructor( private val context: Context, private val broadcastDispatcher: BroadcastDispatcher, private val broadcastSender: BroadcastSender, @@ -76,12 +82,13 @@ class MediaDataFilter @Inject constructor( private var reactivatedKey: String? = null init { - userTracker = object : CurrentUserTracker(broadcastDispatcher) { - override fun onUserSwitched(newUserId: Int) { - // Post this so we can be sure lockscreenUserManager already got the broadcast - executor.execute { handleUserSwitched(newUserId) } + userTracker = + object : CurrentUserTracker(broadcastDispatcher) { + override fun onUserSwitched(newUserId: Int) { + // Post this so we can be sure lockscreenUserManager already got the broadcast + executor.execute { handleUserSwitched(newUserId) } + } } - } userTracker.startTracking() } @@ -108,9 +115,7 @@ class MediaDataFilter @Inject constructor( userEntries.put(key, data) // Notify listeners - listeners.forEach { - it.onMediaDataLoaded(key, oldKey, data) - } + listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) } } override fun onSmartspaceMediaDataLoaded( @@ -128,14 +133,11 @@ class MediaDataFilter @Inject constructor( smartspaceMediaData = data // Before forwarding the smartspace target, first check if we have recently inactive media - val sorted = userEntries.toSortedMap(compareBy { - userEntries.get(it)?.lastActive ?: -1 - }) + val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 }) val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted) var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE data.cardAction?.let { - val smartspaceMaxAgeSeconds = - it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0) + val smartspaceMaxAgeSeconds = it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0) if (smartspaceMaxAgeSeconds > 0) { smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds) } @@ -152,13 +154,21 @@ class MediaDataFilter @Inject constructor( Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") reactivatedKey = lastActiveKey val mediaData = sorted.get(lastActiveKey)!!.copy(active = true) - logger.logRecommendationActivated(mediaData.appUid, mediaData.packageName, - mediaData.instanceId) + logger.logRecommendationActivated( + mediaData.appUid, + mediaData.packageName, + mediaData.instanceId + ) listeners.forEach { - it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, - receivedSmartspaceCardLatency = + it.onMediaDataLoaded( + lastActiveKey, + lastActiveKey, + mediaData, + receivedSmartspaceCardLatency = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis) - .toInt(), isSsReactivated = true) + .toInt(), + isSsReactivated = true + ) } } } else { @@ -170,8 +180,10 @@ class MediaDataFilter @Inject constructor( Log.d(TAG, "Invalid recommendation data. Skip showing the rec card") return } - logger.logRecommendationAdded(smartspaceMediaData.packageName, - smartspaceMediaData.instanceId) + logger.logRecommendationAdded( + smartspaceMediaData.packageName, + smartspaceMediaData.instanceId + ) listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) } } @@ -179,9 +191,7 @@ class MediaDataFilter @Inject constructor( allEntries.remove(key) userEntries.remove(key)?.let { // Only notify listeners if something actually changed - listeners.forEach { - it.onMediaDataRemoved(key) - } + listeners.forEach { it.onMediaDataRemoved(key) } } } @@ -194,16 +204,17 @@ class MediaDataFilter @Inject constructor( // Notify listeners to update with actual active value userEntries.get(lastActiveKey)?.let { mediaData -> listeners.forEach { - it.onMediaDataLoaded( - lastActiveKey, lastActiveKey, mediaData, immediately) + it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately) } } } if (smartspaceMediaData.isActive) { - smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId) + smartspaceMediaData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId + ) } listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } } @@ -218,25 +229,19 @@ class MediaDataFilter @Inject constructor( userEntries.clear() keyCopy.forEach { if (DEBUG) Log.d(TAG, "Removing $it after user change") - listenersCopy.forEach { listener -> - listener.onMediaDataRemoved(it) - } + listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) } } allEntries.forEach { (key, data) -> if (lockscreenUserManager.isCurrentProfile(data.userId)) { if (DEBUG) Log.d(TAG, "Re-adding $key after user change") userEntries.put(key, data) - listenersCopy.forEach { listener -> - listener.onMediaDataLoaded(key, null, data) - } + listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) } } } } - /** - * Invoked when the user has dismissed the media carousel - */ + /** Invoked when the user has dismissed the media carousel */ fun onSwipeToDismiss() { if (DEBUG) Log.d(TAG, "Media carousel swiped away") val mediaKeys = userEntries.keys.toSet() @@ -247,55 +252,52 @@ class MediaDataFilter @Inject constructor( if (smartspaceMediaData.isActive) { val dismissIntent = smartspaceMediaData.dismissIntent if (dismissIntent == null) { - Log.w(TAG, "Cannot create dismiss action click action: " + - "extras missing dismiss_intent.") - } else if (dismissIntent.getComponent() != null && - dismissIntent.getComponent().getClassName() - == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME) { + Log.w( + TAG, + "Cannot create dismiss action click action: " + "extras missing dismiss_intent." + ) + } else if ( + dismissIntent.getComponent() != null && + dismissIntent.getComponent().getClassName() == + EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME + ) { // Dismiss the card Smartspace data through Smartspace trampoline activity. context.startActivity(dismissIntent) } else { broadcastSender.sendBroadcast(dismissIntent) } - smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId) - mediaDataManager.dismissSmartspaceRecommendation(smartspaceMediaData.targetId, - delay = 0L) + smartspaceMediaData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId + ) + mediaDataManager.dismissSmartspaceRecommendation( + smartspaceMediaData.targetId, + delay = 0L + ) } } - /** - * Are there any active media entries, including the recommendation? - */ - fun hasActiveMediaOrRecommendation() = userEntries.any { it.value.active } || + /** Are there any active media entries, including the recommendation? */ + fun hasActiveMediaOrRecommendation() = + userEntries.any { it.value.active } || (smartspaceMediaData.isActive && (smartspaceMediaData.isValid() || reactivatedKey != null)) - /** - * Are there any media entries we should display? - */ - fun hasAnyMediaOrRecommendation() = userEntries.isNotEmpty() || - (smartspaceMediaData.isActive && smartspaceMediaData.isValid()) + /** Are there any media entries we should display? */ + fun hasAnyMediaOrRecommendation() = + userEntries.isNotEmpty() || (smartspaceMediaData.isActive && smartspaceMediaData.isValid()) - /** - * Are there any media notifications active (excluding the recommendation)? - */ + /** Are there any media notifications active (excluding the recommendation)? */ fun hasActiveMedia() = userEntries.any { it.value.active } - /** - * Are there any media entries we should display (excluding the recommendation)? - */ + /** Are there any media entries we should display (excluding the recommendation)? */ fun hasAnyMedia() = userEntries.isNotEmpty() - /** - * Add a listener for filtered [MediaData] changes - */ + /** Add a listener for filtered [MediaData] changes */ fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener) - /** - * Remove a listener that was registered with addListener - */ + /** Remove a listener that was registered with addListener */ fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener) /** @@ -315,8 +317,6 @@ class MediaDataFilter @Inject constructor( val now = systemClock.elapsedRealtime() val lastActiveKey = sortedEntries.lastKey() // most recently active - return sortedEntries.get(lastActiveKey)?.let { - now - it.lastActive - } ?: Long.MAX_VALUE + return sortedEntries.get(lastActiveKey)?.let { now - it.lastActive } ?: Long.MAX_VALUE } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index 896fb4765c57..14dd99023b92 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.app.Notification import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME @@ -57,6 +57,17 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.models.player.MediaAction +import com.android.systemui.media.controls.models.player.MediaButton +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaDeviceData +import com.android.systemui.media.controls.models.player.MediaViewHolder +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider +import com.android.systemui.media.controls.resume.MediaResumeListener +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState @@ -75,17 +86,19 @@ import java.util.concurrent.Executors import javax.inject.Inject // URI fields to try loading album art from -private val ART_URIS = arrayOf( +private val ART_URIS = + arrayOf( MediaMetadata.METADATA_KEY_ALBUM_ART_URI, MediaMetadata.METADATA_KEY_ART_URI, MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI -) + ) private const val TAG = "MediaDataManager" private const val DEBUG = true private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent" -private val LOADING = MediaData( +private val LOADING = + MediaData( userId = -1, initialized = false, app = null, @@ -102,37 +115,41 @@ private val LOADING = MediaData( active = true, resumeAction = null, instanceId = InstanceId.fakeInstanceId(-1), - appUid = Process.INVALID_UID) + appUid = Process.INVALID_UID + ) @VisibleForTesting -internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData( - targetId = "INVALID", - isActive = false, - packageName = "INVALID", - cardAction = null, - recommendations = emptyList(), - dismissIntent = null, - headphoneConnectionTimeMillis = 0, - instanceId = InstanceId.fakeInstanceId(-1)) +internal val EMPTY_SMARTSPACE_MEDIA_DATA = + SmartspaceMediaData( + targetId = "INVALID", + isActive = false, + packageName = "INVALID", + cardAction = null, + recommendations = emptyList(), + dismissIntent = null, + headphoneConnectionTimeMillis = 0, + instanceId = InstanceId.fakeInstanceId(-1) + ) fun isMediaNotification(sbn: StatusBarNotification): Boolean { return sbn.notification.isMediaNotification() } /** - * Allow recommendations from smartspace to show in media controls. - * Requires [Utils.useQsMediaPlayer] to be enabled. - * On by default, but can be disabled by setting to 0 + * Allow recommendations from smartspace to show in media controls. Requires + * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0 */ private fun allowMediaRecommendations(context: Context): Boolean { - val flag = Settings.Secure.getInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1) + val flag = + Settings.Secure.getInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 1 + ) return Utils.useQsMediaPlayer(context) && flag > 0 } -/** - * A class that facilitates management and loading of Media Data, ready for binding. - */ +/** A class that facilitates management and loading of Media Data, ready for binding. */ @SysUISingleton class MediaDataManager( private val context: Context, @@ -159,24 +176,24 @@ class MediaDataManager( companion object { // UI surface label for subscribing Smartspace updates. - @JvmField - val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" + @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" // Smartspace package name's extra key. - @JvmField - val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name" + @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name" // Maximum number of actions allowed in compact view - @JvmField - val MAX_COMPACT_ACTIONS = 3 + @JvmField val MAX_COMPACT_ACTIONS = 3 // Maximum number of actions allowed in expanded view - @JvmField - val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size + @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size } - private val themeText = com.android.settingslib.Utils.getColorAttr(context, - com.android.internal.R.attr.textColorPrimary).defaultColor + private val themeText = + com.android.settingslib.Utils.getColorAttr( + context, + com.android.internal.R.attr.textColorPrimary + ) + .defaultColor // Internal listeners are part of the internal pipeline. External listeners (those registered // with [MediaDeviceManager.addListener]) receive events after they have propagated through @@ -192,9 +209,7 @@ class MediaDataManager( private var smartspaceSession: SmartspaceSession? = null private var allowMediaRecommendations = allowMediaRecommendations(context) - /** - * Check whether this notification is an RCN - */ + /** Check whether this notification is an RCN */ private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean { return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE) } @@ -219,29 +234,44 @@ class MediaDataManager( tunerService: TunerService, mediaFlags: MediaFlags, logger: MediaUiEventLogger - ) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory, - broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener, - mediaSessionBasedFilter, mediaDeviceManager, mediaDataCombineLatest, mediaDataFilter, - activityStarter, smartspaceMediaDataProvider, Utils.useMediaResumption(context), - Utils.useQsMediaPlayer(context), clock, tunerService, mediaFlags, logger) - - private val appChangeReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - when (intent.action) { - Intent.ACTION_PACKAGES_SUSPENDED -> { - val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST) - packages?.forEach { - removeAllForPackage(it) + ) : this( + context, + backgroundExecutor, + foregroundExecutor, + mediaControllerFactory, + broadcastDispatcher, + dumpManager, + mediaTimeoutListener, + mediaResumeListener, + mediaSessionBasedFilter, + mediaDeviceManager, + mediaDataCombineLatest, + mediaDataFilter, + activityStarter, + smartspaceMediaDataProvider, + Utils.useMediaResumption(context), + Utils.useQsMediaPlayer(context), + clock, + tunerService, + mediaFlags, + logger + ) + + private val appChangeReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + Intent.ACTION_PACKAGES_SUSPENDED -> { + val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST) + packages?.forEach { removeAllForPackage(it) } } - } - Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_RESTARTED -> { - intent.data?.encodedSchemeSpecificPart?.let { - removeAllForPackage(it) + Intent.ACTION_PACKAGE_REMOVED, + Intent.ACTION_PACKAGE_RESTARTED -> { + intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) } } } } } - } init { dumpManager.registerDumpable(TAG, this) @@ -262,20 +292,23 @@ class MediaDataManager( // Set up links back into the pipeline for listeners that need to send events upstream. mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> - setTimedOut(key, timedOut) } + setTimedOut(key, timedOut) + } mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> - updateState(key, state) } + updateState(key, state) + } mediaResumeListener.setManager(this) mediaDataFilter.mediaDataManager = this val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) - val uninstallFilter = IntentFilter().apply { - addAction(Intent.ACTION_PACKAGE_REMOVED) - addAction(Intent.ACTION_PACKAGE_RESTARTED) - addDataScheme("package") - } + val uninstallFilter = + IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_REMOVED) + addAction(Intent.ACTION_PACKAGE_RESTARTED) + addDataScheme("package") + } // BroadcastDispatcher does not allow filters with data schemes context.registerReceiver(appChangeReceiver, uninstallFilter) @@ -283,8 +316,10 @@ class MediaDataManager( smartspaceMediaDataProvider.registerListener(this) val smartspaceManager: SmartspaceManager = context.getSystemService(SmartspaceManager::class.java) - smartspaceSession = smartspaceManager.createSmartspaceSession( - SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()) + smartspaceSession = + smartspaceManager.createSmartspaceSession( + SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build() + ) smartspaceSession?.let { it.addOnTargetsAvailableListener( // Use a new thread listening to Smartspace updates instead of using the existing @@ -296,17 +331,24 @@ class MediaDataManager( Executors.newCachedThreadPool(), SmartspaceSession.OnTargetsAvailableListener { targets -> smartspaceMediaDataProvider.onTargetsAvailable(targets) - }) + } + ) } smartspaceSession?.let { it.requestSmartspaceUpdate() } - tunerService.addTunable(object : TunerService.Tunable { - override fun onTuningChanged(key: String?, newValue: String?) { - allowMediaRecommendations = allowMediaRecommendations(context) - if (!allowMediaRecommendations) { - dismissSmartspaceRecommendation(key = smartspaceMediaData.targetId, delay = 0L) + tunerService.addTunable( + object : TunerService.Tunable { + override fun onTuningChanged(key: String?, newValue: String?) { + allowMediaRecommendations = allowMediaRecommendations(context) + if (!allowMediaRecommendations) { + dismissSmartspaceRecommendation( + key = smartspaceMediaData.targetId, + delay = 0L + ) + } } - } - }, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION) + }, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION + ) } fun destroy() { @@ -321,10 +363,7 @@ class MediaDataManager( val oldKey = findExistingEntry(key, sbn.packageName) if (oldKey == null) { val instanceId = logger.getNewInstanceId() - val temp = LOADING.copy( - packageName = sbn.packageName, - instanceId = instanceId - ) + val temp = LOADING.copy(packageName = sbn.packageName, instanceId = instanceId) mediaEntries.put(key, temp) logEvent = true } else if (oldKey != key) { @@ -342,9 +381,7 @@ class MediaDataManager( private fun removeAllForPackage(packageName: String) { Assert.isMainThread() val toRemove = mediaEntries.filter { it.value.packageName == packageName } - toRemove.forEach { - removeEntry(it.key) - } + toRemove.forEach { removeEntry(it.key) } } fun setResumeAction(key: String, action: Runnable?) { @@ -366,32 +403,41 @@ class MediaDataManager( // Resume controls don't have a notification key, so store by package name instead if (!mediaEntries.containsKey(packageName)) { val instanceId = logger.getNewInstanceId() - val appUid = try { - context.packageManager.getApplicationInfo(packageName, 0)?.uid!! - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "Could not get app UID for $packageName", e) - Process.INVALID_UID - } + val appUid = + try { + context.packageManager.getApplicationInfo(packageName, 0)?.uid!! + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Could not get app UID for $packageName", e) + Process.INVALID_UID + } - val resumeData = LOADING.copy( - packageName = packageName, - resumeAction = action, - hasCheckedForResume = true, - instanceId = instanceId, - appUid = appUid - ) + val resumeData = + LOADING.copy( + packageName = packageName, + resumeAction = action, + hasCheckedForResume = true, + instanceId = instanceId, + appUid = appUid + ) mediaEntries.put(packageName, resumeData) logger.logResumeMediaAdded(appUid, packageName, instanceId) } backgroundExecutor.execute { - loadMediaDataInBgForResumption(userId, desc, action, token, appName, appIntent, - packageName) + loadMediaDataInBgForResumption( + userId, + desc, + action, + token, + appName, + appIntent, + packageName + ) } } /** - * Check if there is an existing entry that matches the key or package name. - * Returns the key that matches, or null if not found. + * Check if there is an existing entry that matches the key or package name. Returns the key + * that matches, or null if not found. */ private fun findExistingEntry(key: String, packageName: String): String? { if (mediaEntries.containsKey(key)) { @@ -410,32 +456,24 @@ class MediaDataManager( oldKey: String?, logEvent: Boolean = false ) { - backgroundExecutor.execute { - loadMediaDataInBg(key, sbn, oldKey, logEvent) - } + backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, logEvent) } } - /** - * Add a listener for changes in this class - */ + /** Add a listener for changes in this class */ fun addListener(listener: Listener) { // mediaDataFilter is the current end of the internal pipeline. Register external // listeners as listeners to it. mediaDataFilter.addListener(listener) } - /** - * Remove a listener for changes in this class - */ + /** Remove a listener for changes in this class */ fun removeListener(listener: Listener) { // Since mediaDataFilter is the current end of the internal pipelie, external listeners // have been registered to it. So, they need to be removed from it too. mediaDataFilter.removeListener(listener) } - /** - * Add a listener for internal events. - */ + /** Add a listener for internal events. */ private fun addInternalListener(listener: Listener) = internalListeners.add(listener) /** @@ -483,8 +521,8 @@ class MediaDataManager( } /** - * Called whenever the player has been paused or stopped for a while, or swiped from QQS. - * This will make the player not active anymore, hiding it from QQS and Keyguard. + * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This + * will make the player not active anymore, hiding it from QQS and Keyguard. * @see MediaData.active */ internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) { @@ -506,9 +544,7 @@ class MediaDataManager( } } - /** - * Called when the player's [PlaybackState] has been updated with new actions and/or state - */ + /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ private fun updateState(key: String, state: PlaybackState) { mediaEntries.get(key)?.let { val token = it.token @@ -516,22 +552,23 @@ class MediaDataManager( if (DEBUG) Log.d(TAG, "State updated, but token was null") return } - val actions = createActionsFromState(it.packageName, - mediaControllerFactory.create(it.token), UserHandle(it.userId)) + val actions = + createActionsFromState( + it.packageName, + mediaControllerFactory.create(it.token), + UserHandle(it.userId) + ) // Control buttons // If flag is enabled and controller has a PlaybackState, // create actions from session info // otherwise, no need to update semantic actions. - val data = if (actions != null) { - it.copy( - semanticActions = actions, - isPlaying = isPlayingState(state.state)) - } else { - it.copy( - isPlaying = isPlayingState(state.state) - ) - } + val data = + if (actions != null) { + it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) + } else { + it.copy(isPlaying = isPlayingState(state.state)) + } if (DEBUG) Log.d(TAG, "State updated outside of notification") onMediaDataLoaded(key, key, data) } @@ -544,9 +581,7 @@ class MediaDataManager( notifyMediaDataRemoved(key) } - /** - * Dismiss a media entry. Returns false if the key was not found. - */ + /** Dismiss a media entry. Returns false if the key was not found. */ fun dismissMediaData(key: String, delay: Long): Boolean { val existed = mediaEntries[key] != null backgroundExecutor.execute { @@ -564,9 +599,8 @@ class MediaDataManager( } /** - * Called whenever the recommendation has been expired, or swiped from QQS. - * This will make the recommendation view to not be shown anymore during this headphone - * connection session. + * Called whenever the recommendation has been expired, or swiped from QQS. This will make the + * recommendation view to not be shown anymore during this headphone connection session. */ fun dismissSmartspaceRecommendation(key: String, delay: Long) { if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { @@ -576,13 +610,16 @@ class MediaDataManager( if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target") if (smartspaceMediaData.isActive) { - smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId) + smartspaceMediaData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId + ) } foregroundExecutor.executeDelayed( - { notifySmartspaceMediaDataRemoved( - smartspaceMediaData.targetId, immediately = true) }, delay) + { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) }, + delay + ) } private fun loadMediaDataInBgForResumption( @@ -610,11 +647,12 @@ class MediaDataManager( if (artworkBitmap == null && desc.iconUri != null) { artworkBitmap = loadBitmapFromUri(desc.iconUri!!) } - val artworkIcon = if (artworkBitmap != null) { - Icon.createWithBitmap(artworkBitmap) - } else { - null - } + val artworkIcon = + if (artworkBitmap != null) { + Icon.createWithBitmap(artworkBitmap) + } else { + null + } val currentEntry = mediaEntries.get(packageName) val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() @@ -623,13 +661,34 @@ class MediaDataManager( val mediaAction = getResumeMediaAction(resumeAction) val lastActive = systemClock.elapsedRealtime() foregroundExecutor.execute { - onMediaDataLoaded(packageName, null, MediaData(userId, true, appName, - null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0), - MediaButton(playOrPause = mediaAction), packageName, token, appIntent, - device = null, active = false, - resumeAction = resumeAction, resumption = true, notificationKey = packageName, - hasCheckedForResume = true, lastActive = lastActive, instanceId = instanceId, - appUid = appUid)) + onMediaDataLoaded( + packageName, + null, + MediaData( + userId, + true, + appName, + null, + desc.subtitle, + desc.title, + artworkIcon, + listOf(mediaAction), + listOf(0), + MediaButton(playOrPause = mediaAction), + packageName, + token, + appIntent, + device = null, + active = false, + resumeAction = resumeAction, + resumption = true, + notificationKey = packageName, + hasCheckedForResume = true, + lastActive = lastActive, + instanceId = instanceId, + appUid = appUid + ) + ) } } @@ -639,8 +698,11 @@ class MediaDataManager( oldKey: String?, logEvent: Boolean = false ) { - val token = sbn.notification.extras.getParcelable( - Notification.EXTRA_MEDIA_SESSION, MediaSession.Token::class.java) + val token = + sbn.notification.extras.getParcelable( + Notification.EXTRA_MEDIA_SESSION, + MediaSession.Token::class.java + ) if (token == null) { return } @@ -648,10 +710,12 @@ class MediaDataManager( val metadata = mediaController.metadata val notif: Notification = sbn.notification - val appInfo = notif.extras.getParcelable( - Notification.EXTRA_BUILDER_APPLICATION_INFO, - ApplicationInfo::class.java - ) ?: getAppInfoFromPackage(sbn.packageName) + val appInfo = + notif.extras.getParcelable( + Notification.EXTRA_BUILDER_APPLICATION_INFO, + ApplicationInfo::class.java + ) + ?: getAppInfoFromPackage(sbn.packageName) // Album art var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } @@ -661,11 +725,12 @@ class MediaDataManager( if (artworkBitmap == null) { artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) } - val artWorkIcon = if (artworkBitmap == null) { - notif.getLargeIcon() - } else { - Icon.createWithBitmap(artworkBitmap) - } + val artWorkIcon = + if (artworkBitmap == null) { + notif.getLargeIcon() + } else { + Icon.createWithBitmap(artworkBitmap) + } // App name val appName = getAppName(sbn, appInfo) @@ -694,17 +759,27 @@ class MediaDataManager( val extras = sbn.notification.extras val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null) val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1) - val deviceIntent = extras.getParcelable( - Notification.EXTRA_MEDIA_REMOTE_INTENT, PendingIntent::class.java) + val deviceIntent = + extras.getParcelable( + Notification.EXTRA_MEDIA_REMOTE_INTENT, + PendingIntent::class.java + ) Log.d(TAG, "$key is RCN for $deviceName") if (deviceName != null && deviceIcon > -1) { // Name and icon must be present, but intent may be null val enabled = deviceIntent != null && deviceIntent.isActivity - val deviceDrawable = Icon.createWithResource(sbn.packageName, deviceIcon) + val deviceDrawable = + Icon.createWithResource(sbn.packageName, deviceIcon) .loadDrawable(sbn.getPackageContext(context)) - device = MediaDeviceData(enabled, deviceDrawable, deviceName, deviceIntent, - showBroadcastButton = false) + device = + MediaDeviceData( + enabled, + deviceDrawable, + deviceName, + deviceIntent, + showBroadcastButton = false + ) } } @@ -721,10 +796,13 @@ class MediaDataManager( } val playbackLocation = - if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE - else if (mediaController.playbackInfo?.playbackType == - MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) MediaData.PLAYBACK_LOCAL - else MediaData.PLAYBACK_CAST_LOCAL + if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE + else if ( + mediaController.playbackInfo?.playbackType == + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL + ) + MediaData.PLAYBACK_LOCAL + else MediaData.PLAYBACK_CAST_LOCAL val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null val currentEntry = mediaEntries.get(key) @@ -742,13 +820,36 @@ class MediaDataManager( val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true val active = mediaEntries[key]?.active ?: true - onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, appName, - smallIcon, artist, song, artWorkIcon, actionIcons, actionsToShowCollapsed, - semanticActions, sbn.packageName, token, notif.contentIntent, device, - active, resumeAction = resumeAction, playbackLocation = playbackLocation, - notificationKey = key, hasCheckedForResume = hasCheckedForResume, - isPlaying = isPlaying, isClearable = sbn.isClearable(), - lastActive = lastActive, instanceId = instanceId, appUid = appUid)) + onMediaDataLoaded( + key, + oldKey, + MediaData( + sbn.normalizedUserId, + true, + appName, + smallIcon, + artist, + song, + artWorkIcon, + actionIcons, + actionsToShowCollapsed, + semanticActions, + sbn.packageName, + token, + notif.contentIntent, + device, + active, + resumeAction = resumeAction, + playbackLocation = playbackLocation, + notificationKey = key, + hasCheckedForResume = hasCheckedForResume, + isPlaying = isPlaying, + isClearable = sbn.isClearable(), + lastActive = lastActive, + instanceId = instanceId, + appUid = appUid + ) + ) } } @@ -774,27 +875,33 @@ class MediaDataManager( } } - /** - * Generate action buttons based on notification actions - */ - private fun createActionsFromNotification(sbn: StatusBarNotification): - Pair<List<MediaAction>, List<Int>> { + /** Generate action buttons based on notification actions */ + private fun createActionsFromNotification( + sbn: StatusBarNotification + ): Pair<List<MediaAction>, List<Int>> { val notif = sbn.notification val actionIcons: MutableList<MediaAction> = ArrayList() val actions = notif.actions - var actionsToShowCollapsed = notif.extras.getIntArray( - Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() ?: mutableListOf() + var actionsToShowCollapsed = + notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() + ?: mutableListOf() if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { - Log.e(TAG, "Too many compact actions for ${sbn.key}," + - "limiting to first $MAX_COMPACT_ACTIONS") + Log.e( + TAG, + "Too many compact actions for ${sbn.key}," + + "limiting to first $MAX_COMPACT_ACTIONS" + ) actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) } if (actions != null) { for ((index, action) in actions.withIndex()) { if (index == MAX_NOTIFICATION_ACTIONS) { - Log.w(TAG, "Too many notification actions for ${sbn.key}," + - " limiting to first $MAX_NOTIFICATION_ACTIONS") + Log.w( + TAG, + "Too many notification actions for ${sbn.key}," + + " limiting to first $MAX_NOTIFICATION_ACTIONS" + ) break } if (action.getIcon() == null) { @@ -802,33 +909,38 @@ class MediaDataManager( actionsToShowCollapsed.remove(index) continue } - val runnable = if (action.actionIntent != null) { - Runnable { - if (action.actionIntent.isActivity) { - activityStarter.startPendingIntentDismissingKeyguard( - action.actionIntent) - } else if (action.isAuthenticationRequired()) { - activityStarter.dismissKeyguardThenExecute({ - var result = sendPendingIntent(action.actionIntent) - result - }, {}, true) - } else { - sendPendingIntent(action.actionIntent) + val runnable = + if (action.actionIntent != null) { + Runnable { + if (action.actionIntent.isActivity) { + activityStarter.startPendingIntentDismissingKeyguard( + action.actionIntent + ) + } else if (action.isAuthenticationRequired()) { + activityStarter.dismissKeyguardThenExecute( + { + var result = sendPendingIntent(action.actionIntent) + result + }, + {}, + true + ) + } else { + sendPendingIntent(action.actionIntent) + } } + } else { + null } - } else { - null - } - val mediaActionIcon = if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { - Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) - } else { - action.getIcon() - }.setTint(themeText).loadDrawable(context) - val mediaAction = MediaAction( - mediaActionIcon, - runnable, - action.title, - null) + val mediaActionIcon = + if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { + Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) + } else { + action.getIcon() + } + .setTint(themeText) + .loadDrawable(context) + val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) actionIcons.add(mediaAction) } } @@ -841,7 +953,9 @@ class MediaDataManager( * @param packageName Package name for the media app * @param controller MediaController for the current session * @return a Pair consisting of a list of media actions, and a list of ints representing which + * ``` * of those actions should be shown in the compact player + * ``` */ private fun createActionsFromState( packageName: String, @@ -854,59 +968,69 @@ class MediaDataManager( } // First, check for standard actions - val playOrPause = if (isConnectingState(state.state)) { - // Spinner needs to be animating to render anything. Start it here. - val drawable = context.getDrawable( - com.android.internal.R.drawable.progress_small_material) - (drawable as Animatable).start() - MediaAction( - drawable, - null, // no action to perform when clicked - context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), - // Specify a rebind id to prevent the spinner from restarting on later binds. - com.android.internal.R.drawable.progress_small_material - ) - } else if (isPlayingState(state.state)) { - getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) - } else { - getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) - } - val prevButton = getStandardAction(controller, state.actions, - PlaybackState.ACTION_SKIP_TO_PREVIOUS) - val nextButton = getStandardAction(controller, state.actions, - PlaybackState.ACTION_SKIP_TO_NEXT) + val playOrPause = + if (isConnectingState(state.state)) { + // Spinner needs to be animating to render anything. Start it here. + val drawable = + context.getDrawable(com.android.internal.R.drawable.progress_small_material) + (drawable as Animatable).start() + MediaAction( + drawable, + null, // no action to perform when clicked + context.getString(R.string.controls_media_button_connecting), + context.getDrawable(R.drawable.ic_media_connecting_container), + // Specify a rebind id to prevent the spinner from restarting on later binds. + com.android.internal.R.drawable.progress_small_material + ) + } else if (isPlayingState(state.state)) { + getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) + } else { + getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) + } + val prevButton = + getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS) + val nextButton = + getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT) // Then, create a way to build any custom actions that will be needed - val customActions = state.customActions.asSequence().filterNotNull().map { - getCustomAction(state, packageName, controller, it) - }.iterator() + val customActions = + state.customActions + .asSequence() + .filterNotNull() + .map { getCustomAction(state, packageName, controller, it) } + .iterator() fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null // Finally, assign the remaining button slots: play/pause A B C D // A = previous, else custom action (if not reserved) // B = next, else custom action (if not reserved) // C and D are always custom actions - val reservePrev = controller.extras?.getBoolean( - MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV) == true - val reserveNext = controller.extras?.getBoolean( - MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT) == true - - val prevOrCustom = if (prevButton != null) { - prevButton - } else if (!reservePrev) { - nextCustomAction() - } else { - null - } + val reservePrev = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV + ) == true + val reserveNext = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT + ) == true + + val prevOrCustom = + if (prevButton != null) { + prevButton + } else if (!reservePrev) { + nextCustomAction() + } else { + null + } - val nextOrCustom = if (nextButton != null) { - nextButton - } else if (!reserveNext) { - nextCustomAction() - } else { - null - } + val nextOrCustom = + if (nextButton != null) { + nextButton + } else if (!reserveNext) { + nextCustomAction() + } else { + null + } return MediaButton( playOrPause, @@ -925,11 +1049,14 @@ class MediaDataManager( * @param controller MediaController for the session * @param stateActions The actions included with the session's [PlaybackState] * @param action A [PlaybackState.Actions] value representing what action to generate. One of: + * ``` * [PlaybackState.ACTION_PLAY] * [PlaybackState.ACTION_PAUSE] * [PlaybackState.ACTION_SKIP_TO_PREVIOUS] * [PlaybackState.ACTION_SKIP_TO_NEXT] - * @return A [MediaAction] with correct values set, or null if the state doesn't support it + * @return + * ``` + * A [MediaAction] with correct values set, or null if the state doesn't support it */ private fun getStandardAction( controller: MediaController, @@ -977,20 +1104,18 @@ class MediaDataManager( } } - /** - * Check whether the actions from a [PlaybackState] include a specific action - */ + /** Check whether the actions from a [PlaybackState] include a specific action */ private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean { - if ((action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && - (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)) { + if ( + (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && + (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L) + ) { return true } return (stateActions and action != 0L) } - /** - * Get a [MediaAction] representing a [PlaybackState.CustomAction] - */ + /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */ private fun getCustomAction( state: PlaybackState, packageName: String, @@ -1005,9 +1130,7 @@ class MediaDataManager( ) } - /** - * Load a bitmap from the various Art metadata URIs - */ + /** Load a bitmap from the various Art metadata URIs */ private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { for (uri in ART_URIS) { val uriString = metadata.getString(uri) @@ -1042,16 +1165,18 @@ class MediaDataManager( return null } - if (!uri.scheme.equals(ContentResolver.SCHEME_CONTENT) && + if ( + !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) && !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) && - !uri.scheme.equals(ContentResolver.SCHEME_FILE)) { + !uri.scheme.equals(ContentResolver.SCHEME_FILE) + ) { return null } val source = ImageDecoder.createSource(context.getContentResolver(), uri) return try { - ImageDecoder.decodeBitmap(source) { - decoder, info, source -> decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE + ImageDecoder.decodeBitmap(source) { decoder, _, _ -> + decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE } } catch (e: IOException) { Log.e(TAG, "Unable to load bitmap", e) @@ -1065,25 +1190,23 @@ class MediaDataManager( private fun getResumeMediaAction(action: Runnable): MediaAction { return MediaAction( Icon.createWithResource(context, R.drawable.ic_media_play) - .setTint(themeText).loadDrawable(context), + .setTint(themeText) + .loadDrawable(context), action, context.getString(R.string.controls_media_resume), context.getDrawable(R.drawable.ic_media_play_container) ) } - fun onMediaDataLoaded( - key: String, - oldKey: String?, - data: MediaData - ) = traceSection("MediaDataManager#onMediaDataLoaded") { - Assert.isMainThread() - if (mediaEntries.containsKey(key)) { - // Otherwise this was removed already - mediaEntries.put(key, data) - notifyMediaDataLoaded(key, oldKey, data) + fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) = + traceSection("MediaDataManager#onMediaDataLoaded") { + Assert.isMainThread() + if (mediaEntries.containsKey(key)) { + // Otherwise this was removed already + mediaEntries.put(key, data) + notifyMediaDataLoaded(key, oldKey, data) + } } - } override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) { if (!allowMediaRecommendations) { @@ -1100,9 +1223,11 @@ class MediaDataManager( if (DEBUG) { Log.d(TAG, "Set Smartspace media to be inactive for the data update") } - smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId) + smartspaceMediaData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId + ) notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false) } 1 -> { @@ -1113,15 +1238,16 @@ class MediaDataManager( } if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.") smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true) - notifySmartspaceMediaDataLoaded( - smartspaceMediaData.targetId, smartspaceMediaData) + notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) } else -> { // There should NOT be more than 1 Smartspace media update. When it happens, it // indicates a bad state or an error. Reset the status accordingly. Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") notifySmartspaceMediaDataRemoved( - smartspaceMediaData.targetId, false /* immediately */) + smartspaceMediaData.targetId, + false /* immediately */ + ) smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA } } @@ -1134,10 +1260,17 @@ class MediaDataManager( Log.d(TAG, "Not removing $key because resumable") // Move to resume key (aka package name) if that key doesn't already exist. val resumeAction = getResumeMediaAction(removed.resumeAction!!) - val updated = removed.copy(token = null, actions = listOf(resumeAction), + val updated = + removed.copy( + token = null, + actions = listOf(resumeAction), semanticActions = MediaButton(playOrPause = resumeAction), - actionsToShowInCompact = listOf(0), active = false, resumption = true, - isPlaying = false, isClearable = true) + actionsToShowInCompact = listOf(0), + active = false, + resumption = true, + isPlaying = false, + isClearable = true + ) val pkg = removed.packageName val migrate = mediaEntries.put(pkg, updated) == null // Notify listeners of "new" controls when migrating or removed and update when not @@ -1179,33 +1312,27 @@ class MediaDataManager( } } - /** - * Invoked when the user has dismissed the media carousel - */ + /** Invoked when the user has dismissed the media carousel */ fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss() - /** - * Are there any media notifications active, including the recommendations? - */ + /** Are there any media notifications active, including the recommendations? */ fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation() /** * Are there any media entries we should display, including the recommendations? - * If resumption is enabled, this will include inactive players - * If resumption is disabled, we only want to show active players + * - If resumption is enabled, this will include inactive players + * - If resumption is disabled, we only want to show active players */ fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation() - /** - * Are there any resume media notifications active, excluding the recommendations? - */ + /** Are there any resume media notifications active, excluding the recommendations? */ fun hasActiveMedia() = mediaDataFilter.hasActiveMedia() /** - * Are there any resume media notifications active, excluding the recommendations? - * If resumption is enabled, this will include inactive players - * If resumption is disabled, we only want to show active players - */ + * Are there any resume media notifications active, excluding the recommendations? + * - If resumption is enabled, this will include inactive players + * - If resumption is disabled, we only want to show active players + */ fun hasAnyMedia() = mediaDataFilter.hasAnyMedia() interface Listener { @@ -1275,10 +1402,9 @@ class MediaDataManager( ): SmartspaceMediaData { var dismissIntent: Intent? = null if (target.baseAction != null && target.baseAction.extras != null) { - dismissIntent = target - .baseAction - .extras - .getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent? + dismissIntent = + target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) + as Intent? } packageName(target)?.let { return SmartspaceMediaData( @@ -1289,14 +1415,16 @@ class MediaDataManager( recommendations = target.iconGrid, dismissIntent = dismissIntent, headphoneConnectionTimeMillis = target.creationTimeMillis, - instanceId = logger.getNewInstanceId()) + instanceId = logger.getNewInstanceId() + ) } - return EMPTY_SMARTSPACE_MEDIA_DATA - .copy(targetId = target.smartspaceTargetId, - isActive = isActive, - dismissIntent = dismissIntent, - headphoneConnectionTimeMillis = target.creationTimeMillis, - instanceId = logger.getNewInstanceId()) + return EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = target.smartspaceTargetId, + isActive = isActive, + dismissIntent = dismissIntent, + headphoneConnectionTimeMillis = target.creationTimeMillis, + instanceId = logger.getNewInstanceId() + ) } private fun packageName(target: SmartspaceTarget): String? { @@ -1308,8 +1436,9 @@ class MediaDataManager( for (recommendation in recommendationList) { val extras = recommendation.extras extras?.let { - it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { - packageName -> return packageName } + it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName -> + return packageName + } } } Log.w(TAG, "No valid package name is provided.") diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt index b3a4ddf8ec1f..6a512be091e1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.bluetooth.BluetoothLeBroadcast import android.bluetooth.BluetoothLeBroadcastMetadata @@ -36,6 +36,10 @@ import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaDeviceData +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaDataUtils import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory import com.android.systemui.statusbar.policy.ConfigurationController @@ -47,10 +51,10 @@ private const val PLAYBACK_TYPE_UNKNOWN = 0 private const val TAG = "MediaDeviceManager" private const val DEBUG = true -/** - * Provides information about the route (ie. device) where playback is occurring. - */ -class MediaDeviceManager @Inject constructor( +/** Provides information about the route (ie. device) where playback is occurring. */ +class MediaDeviceManager +@Inject +constructor( private val context: Context, private val controllerFactory: MediaControllerFactory, private val localMediaManagerFactory: LocalMediaManagerFactory, @@ -70,14 +74,10 @@ class MediaDeviceManager @Inject constructor( dumpManager.registerDumpable(javaClass.name, this) } - /** - * Add a listener for changes to the media route (ie. device). - */ + /** Add a listener for changes to the media route (ie. device). */ fun addListener(listener: Listener) = listeners.add(listener) - /** - * Remove a listener that has been registered with addListener. - */ + /** Remove a listener that has been registered with addListener. */ fun removeListener(listener: Listener) = listeners.remove(listener) override fun onMediaDataLoaded( @@ -101,19 +101,11 @@ class MediaDeviceManager @Inject constructor( processDevice(key, oldKey, data.device) return } - val controller = data.token?.let { - controllerFactory.create(it) - } + val controller = data.token?.let { controllerFactory.create(it) } val localMediaManager = localMediaManagerFactory.create(data.packageName) val muteAwaitConnectionManager = - muteAwaitConnectionManagerFactory.create(localMediaManager) - entry = Entry( - key, - oldKey, - controller, - localMediaManager, - muteAwaitConnectionManager - ) + muteAwaitConnectionManagerFactory.create(localMediaManager) + entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager) entries[key] = entry entry.start() } @@ -122,11 +114,7 @@ class MediaDeviceManager @Inject constructor( override fun onMediaDataRemoved(key: String) { val token = entries.remove(key) token?.stop() - token?.let { - listeners.forEach { - it.onKeyRemoved(key) - } - } + token?.let { listeners.forEach { it.onKeyRemoved(key) } } } override fun dump(pw: PrintWriter, args: Array<String>) { @@ -141,9 +129,7 @@ class MediaDeviceManager @Inject constructor( @MainThread private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) { - listeners.forEach { - it.onMediaDeviceChanged(key, oldKey, device) - } + listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) } } interface Listener { @@ -159,8 +145,10 @@ class MediaDeviceManager @Inject constructor( val controller: MediaController?, val localMediaManager: LocalMediaManager, val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager? - ) : LocalMediaManager.DeviceCallback, MediaController.Callback(), - BluetoothLeBroadcast.Callback { + ) : + LocalMediaManager.DeviceCallback, + MediaController.Callback(), + BluetoothLeBroadcast.Callback { val token get() = controller?.sessionToken @@ -171,54 +159,52 @@ class MediaDeviceManager @Inject constructor( val sameWithoutIcon = value != null && value.equalsWithoutIcon(field) if (!started || !sameWithoutIcon) { field = value - fgExecutor.execute { - processDevice(key, oldKey, value) - } + fgExecutor.execute { processDevice(key, oldKey, value) } } } // A device that is not yet connected but is expected to connect imminently. Because it's // expected to connect imminently, it should be displayed as the current device. private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null private var broadcastDescription: String? = null - private val configListener = object : ConfigurationController.ConfigurationListener { - override fun onLocaleListChanged() { - updateCurrent() + private val configListener = + object : ConfigurationController.ConfigurationListener { + override fun onLocaleListChanged() { + updateCurrent() + } } - } @AnyThread - fun start() = bgExecutor.execute { - if (!started) { - localMediaManager.registerCallback(this) - localMediaManager.startScan() - muteAwaitConnectionManager?.startListening() - playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN - controller?.registerCallback(this) - updateCurrent() - started = true - configurationController.addCallback(configListener) + fun start() = + bgExecutor.execute { + if (!started) { + localMediaManager.registerCallback(this) + localMediaManager.startScan() + muteAwaitConnectionManager?.startListening() + playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN + controller?.registerCallback(this) + updateCurrent() + started = true + configurationController.addCallback(configListener) + } } - } @AnyThread - fun stop() = bgExecutor.execute { - if (started) { - started = false - controller?.unregisterCallback(this) - localMediaManager.stopScan() - localMediaManager.unregisterCallback(this) - muteAwaitConnectionManager?.stopListening() - configurationController.removeCallback(configListener) + fun stop() = + bgExecutor.execute { + if (started) { + started = false + controller?.unregisterCallback(this) + localMediaManager.stopScan() + localMediaManager.unregisterCallback(this) + muteAwaitConnectionManager?.stopListening() + configurationController.removeCallback(configListener) + } } - } fun dump(pw: PrintWriter) { - val routingSession = controller?.let { - mr2manager.getRoutingSessionForMediaController(it) - } - val selectedRoutes = routingSession?.let { - mr2manager.getSelectedRoutes(it) - } + val routingSession = + controller?.let { mr2manager.getRoutingSessionForMediaController(it) } + val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) } with(pw) { println(" current device is ${current?.name}") val type = controller?.playbackInfo?.playbackType @@ -238,14 +224,11 @@ class MediaDeviceManager @Inject constructor( updateCurrent() } - override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute { - updateCurrent() - } + override fun onDeviceListUpdate(devices: List<MediaDevice>?) = + bgExecutor.execute { updateCurrent() } override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) { - bgExecutor.execute { - updateCurrent() - } + bgExecutor.execute { updateCurrent() } } override fun onAboutToConnectDeviceAdded( @@ -253,14 +236,17 @@ class MediaDeviceManager @Inject constructor( deviceName: String, deviceIcon: Drawable? ) { - aboutToConnectDeviceOverride = AboutToConnectDevice( - fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress), - backupMediaDeviceData = MediaDeviceData( - /* enabled */ enabled = true, - /* icon */ deviceIcon, - /* name */ deviceName, - /* showBroadcastButton */ showBroadcastButton = false) - ) + aboutToConnectDeviceOverride = + AboutToConnectDevice( + fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress), + backupMediaDeviceData = + MediaDeviceData( + /* enabled */ enabled = true, + /* icon */ deviceIcon, + /* name */ deviceName, + /* showBroadcastButton */ showBroadcastButton = false + ) + ) updateCurrent() } @@ -287,8 +273,11 @@ class MediaDeviceManager @Inject constructor( metadata: BluetoothLeBroadcastMetadata ) { if (DEBUG) { - Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " + - "metadata = $metadata") + Log.d( + TAG, + "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " + + "metadata = $metadata" + ) } updateCurrent() } @@ -315,8 +304,10 @@ class MediaDeviceManager @Inject constructor( override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) { if (DEBUG) { - Log.d(TAG, "onBroadcastUpdateFailed(), reason = $reason , " + - "broadcastId = $broadcastId") + Log.d( + TAG, + "onBroadcastUpdateFailed(), reason = $reason , " + "broadcastId = $broadcastId" + ) } } @@ -327,34 +318,45 @@ class MediaDeviceManager @Inject constructor( @WorkerThread private fun updateCurrent() { if (isLeAudioBroadcastEnabled()) { - current = MediaDeviceData( + current = + MediaDeviceData( /* enabled */ true, /* icon */ context.getDrawable(R.drawable.settings_input_antenna), /* name */ broadcastDescription, /* intent */ null, - /* showBroadcastButton */ showBroadcastButton = true) + /* showBroadcastButton */ showBroadcastButton = true + ) } else { val aboutToConnect = aboutToConnectDeviceOverride - if (aboutToConnect != null && + if ( + aboutToConnect != null && aboutToConnect.fullMediaDevice == null && - aboutToConnect.backupMediaDeviceData != null) { + aboutToConnect.backupMediaDeviceData != null + ) { // Only use [backupMediaDeviceData] when we don't have [fullMediaDevice]. current = aboutToConnect.backupMediaDeviceData return } - val device = aboutToConnect?.fullMediaDevice - ?: localMediaManager.currentConnectedDevice + val device = + aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) } // If we have a controller but get a null route, then don't trust the device val enabled = device != null && (controller == null || route != null) - val name = if (controller == null || route != null) { - route?.name?.toString() ?: device?.name - } else { - null - } - current = MediaDeviceData(enabled, device?.iconWithoutBackground, name, - id = device?.id, showBroadcastButton = false) + val name = + if (controller == null || route != null) { + route?.name?.toString() ?: device?.name + } else { + null + } + current = + MediaDeviceData( + enabled, + device?.iconWithoutBackground, + name, + id = device?.id, + showBroadcastButton = false + ) } } @@ -384,13 +386,16 @@ class MediaDeviceManager @Inject constructor( // unexpected result. // Check the current media app's name is the same with current broadcast app's name // or not. - var mediaApp = MediaDataUtils.getAppLabel( - context, localMediaManager.packageName, - context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)) + var mediaApp = + MediaDataUtils.getAppLabel( + context, + localMediaManager.packageName, + context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name) + ) var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp) if (isCurrentBroadcastedApp) { - broadcastDescription = context.getString( - R.string.broadcasting_description_is_broadcasting) + broadcastDescription = + context.getString(R.string.broadcasting_description_is_broadcasting) } else { broadcastDescription = currentBroadcastedApp } @@ -403,9 +408,9 @@ class MediaDeviceManager @Inject constructor( * [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information. * * @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If - * non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData]. + * non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData]. * @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum - * information required to display the device. Only use if [fullMediaDevice] is null. + * information required to display the device. Only use if [fullMediaDevice] is null. */ private data class AboutToConnectDevice( val fullMediaDevice: MediaDevice? = null, diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt index 31792967899d..ab93b292308e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.content.ComponentName import android.content.Context @@ -25,6 +25,8 @@ import android.media.session.MediaSessionManager import android.util.Log import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins import java.util.concurrent.Executor import javax.inject.Inject @@ -38,7 +40,9 @@ private const val TAG = "MediaSessionBasedFilter" * sessions. In this situation, there should only be a media object for the remote session. To * achieve this, update events for the local session need to be filtered. */ -class MediaSessionBasedFilter @Inject constructor( +class MediaSessionBasedFilter +@Inject +constructor( context: Context, private val sessionManager: MediaSessionManager, @Main private val foregroundExecutor: Executor, @@ -50,7 +54,7 @@ class MediaSessionBasedFilter @Inject constructor( // Keep track of MediaControllers for a given package to check if an app is casting and it // filter loaded events for local sessions. private val packageControllers: LinkedHashMap<String, MutableList<MediaController>> = - LinkedHashMap() + LinkedHashMap() // Keep track of the key used for the session tokens. This information is used to know when to // dispatch a removed event so that a media object for a local session will be removed. @@ -59,11 +63,12 @@ class MediaSessionBasedFilter @Inject constructor( // Keep track of which media session tokens have associated notifications. private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf() - private val sessionListener = object : MediaSessionManager.OnActiveSessionsChangedListener { - override fun onActiveSessionsChanged(controllers: List<MediaController>) { - handleControllersChanged(controllers) + private val sessionListener = + object : MediaSessionManager.OnActiveSessionsChangedListener { + override fun onActiveSessionsChanged(controllers: List<MediaController>) { + handleControllersChanged(controllers) + } } - } init { backgroundExecutor.execute { @@ -73,14 +78,10 @@ class MediaSessionBasedFilter @Inject constructor( } } - /** - * Add a listener for filtered [MediaData] changes - */ + /** Add a listener for filtered [MediaData] changes */ fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) - /** - * Remove a listener that was registered with addListener - */ + /** Remove a listener that was registered with addListener */ fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener) /** @@ -100,31 +101,32 @@ class MediaSessionBasedFilter @Inject constructor( isSsReactivated: Boolean ) { backgroundExecutor.execute { - data.token?.let { - tokensWithNotifications.add(it) - } + data.token?.let { tokensWithNotifications.add(it) } val isMigration = oldKey != null && key != oldKey if (isMigration) { keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) } } if (data.token != null) { - keyedTokens.get(key)?.let { - tokens -> - tokens.add(data.token) - } ?: run { - val tokens = mutableSetOf(data.token) - keyedTokens.put(key, tokens) - } + keyedTokens.get(key)?.let { tokens -> tokens.add(data.token) } + ?: run { + val tokens = mutableSetOf(data.token) + keyedTokens.put(key, tokens) + } } // Determine if an app is casting by checking if it has a session with playback type // PLAYBACK_TYPE_REMOTE. - val remoteControllers = packageControllers.get(data.packageName)?.filter { - it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE - } + val remoteControllers = + packageControllers.get(data.packageName)?.filter { + it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE + } // Limiting search to only apps with a single remote session. val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null - if (isMigration || remote == null || remote.sessionToken == data.token || - !tokensWithNotifications.contains(remote.sessionToken)) { + if ( + isMigration || + remote == null || + remote.sessionToken == data.token || + !tokensWithNotifications.contains(remote.sessionToken) + ) { // Not filtering in this case. Passing the event along to listeners. dispatchMediaDataLoaded(key, oldKey, data, immediately) } else { @@ -146,9 +148,7 @@ class MediaSessionBasedFilter @Inject constructor( data: SmartspaceMediaData, shouldPrioritize: Boolean ) { - backgroundExecutor.execute { - dispatchSmartspaceMediaDataLoaded(key, data) - } + backgroundExecutor.execute { dispatchSmartspaceMediaDataLoaded(key, data) } } override fun onMediaDataRemoved(key: String) { @@ -160,9 +160,7 @@ class MediaSessionBasedFilter @Inject constructor( } override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { - backgroundExecutor.execute { - dispatchSmartspaceMediaDataRemoved(key, immediately) - } + backgroundExecutor.execute { dispatchSmartspaceMediaDataRemoved(key, immediately) } } private fun dispatchMediaDataLoaded( @@ -177,9 +175,7 @@ class MediaSessionBasedFilter @Inject constructor( } private fun dispatchMediaDataRemoved(key: String) { - foregroundExecutor.execute { - listeners.toSet().forEach { it.onMediaDataRemoved(key) } - } + foregroundExecutor.execute { listeners.toSet().forEach { it.onMediaDataRemoved(key) } } } private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { @@ -196,15 +192,12 @@ class MediaSessionBasedFilter @Inject constructor( private fun handleControllersChanged(controllers: List<MediaController>) { packageControllers.clear() - controllers.forEach { - controller -> - packageControllers.get(controller.packageName)?.let { - tokens -> - tokens.add(controller) - } ?: run { - val tokens = mutableListOf(controller) - packageControllers.put(controller.packageName, tokens) - } + controllers.forEach { controller -> + packageControllers.get(controller.packageName)?.let { tokens -> tokens.add(controller) } + ?: run { + val tokens = mutableListOf(controller) + packageControllers.put(controller.packageName, tokens) + } } tokensWithNotifications.retainAll(controllers.map { it.sessionToken }) } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt index 93a29ef03393..7f5c82fb5eee 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.media.session.MediaController import android.media.session.PlaybackState @@ -22,6 +22,8 @@ import android.os.SystemProperties import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.SysuiStatusBarStateController @@ -31,18 +33,18 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject @VisibleForTesting -val PAUSED_MEDIA_TIMEOUT = SystemProperties - .getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10)) +val PAUSED_MEDIA_TIMEOUT = + SystemProperties.getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10)) @VisibleForTesting -val RESUME_MEDIA_TIMEOUT = SystemProperties - .getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3)) +val RESUME_MEDIA_TIMEOUT = + SystemProperties.getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3)) -/** - * Controller responsible for keeping track of playback states and expiring inactive streams. - */ +/** Controller responsible for keeping track of playback states and expiring inactive streams. */ @SysUISingleton -class MediaTimeoutListener @Inject constructor( +class MediaTimeoutListener +@Inject +constructor( private val mediaControllerFactory: MediaControllerFactory, @Main private val mainExecutor: DelayableExecutor, private val logger: MediaTimeoutLogger, @@ -56,7 +58,9 @@ class MediaTimeoutListener @Inject constructor( * Callback representing that a media object is now expired: * @param key Media control unique identifier * @param timedOut True when expired for {@code PAUSED_MEDIA_TIMEOUT} for active media, + * ``` * or {@code RESUME_MEDIA_TIMEOUT} for resume media + * ``` */ lateinit var timeoutCallback: (String, Boolean) -> Unit @@ -68,21 +72,25 @@ class MediaTimeoutListener @Inject constructor( lateinit var stateCallback: (String, PlaybackState) -> Unit init { - statusBarStateController.addCallback(object : StatusBarStateController.StateListener { - override fun onDozingChanged(isDozing: Boolean) { - if (!isDozing) { - // Check whether any timeouts should have expired - mediaListeners.forEach { (key, listener) -> - if (listener.cancellation != null && - listener.expiration <= systemClock.elapsedRealtime()) { - // We dozed too long - timeout now, and cancel the pending one - listener.expireMediaTimeout(key, "timeout happened while dozing") - listener.doTimeout() + statusBarStateController.addCallback( + object : StatusBarStateController.StateListener { + override fun onDozingChanged(isDozing: Boolean) { + if (!isDozing) { + // Check whether any timeouts should have expired + mediaListeners.forEach { (key, listener) -> + if ( + listener.cancellation != null && + listener.expiration <= systemClock.elapsedRealtime() + ) { + // We dozed too long - timeout now, and cancel the pending one + listener.expireMediaTimeout(key, "timeout happened while dozing") + listener.doTimeout() + } } } } } - }) + ) } override fun onMediaDataLoaded( @@ -145,10 +153,8 @@ class MediaTimeoutListener @Inject constructor( return mediaListeners[key]?.timedOut ?: false } - private inner class PlaybackStateListener( - var key: String, - data: MediaData - ) : MediaController.Callback() { + private inner class PlaybackStateListener(var key: String, data: MediaData) : + MediaController.Callback() { var timedOut = false var lastState: PlaybackState? = null @@ -162,11 +168,12 @@ class MediaTimeoutListener @Inject constructor( mediaController?.unregisterCallback(this) field = value val token = field.token - mediaController = if (token != null) { - mediaControllerFactory.create(token) - } else { - null - } + mediaController = + if (token != null) { + mediaControllerFactory.create(token) + } else { + null + } mediaController?.registerCallback(this) // Let's register the cancellations, but not dispatch events now. // Timeouts didn't happen yet and reentrant events are troublesome. @@ -212,7 +219,8 @@ class MediaTimeoutListener @Inject constructor( logger.logPlaybackState(key, state) val playingStateSame = (state?.state?.isPlaying() == isPlaying()) - val actionsSame = (lastState?.actions == state?.actions) && + val actionsSame = + (lastState?.actions == state?.actions) && areCustomActionListsEqual(lastState?.customActions, state?.customActions) val resumptionChanged = resumption != mediaData.resumption @@ -237,15 +245,14 @@ class MediaTimeoutListener @Inject constructor( return } expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state, $resumption") - val timeout = if (mediaData.resumption) { - RESUME_MEDIA_TIMEOUT - } else { - PAUSED_MEDIA_TIMEOUT - } + val timeout = + if (mediaData.resumption) { + RESUME_MEDIA_TIMEOUT + } else { + PAUSED_MEDIA_TIMEOUT + } expiration = systemClock.elapsedRealtime() + timeout - cancellation = mainExecutor.executeDelayed({ - doTimeout() - }, timeout) + cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout) } else { expireMediaTimeout(key, "playback started - $state, $key") timedOut = false @@ -301,9 +308,11 @@ class MediaTimeoutListener @Inject constructor( firstAction: PlaybackState.CustomAction, secondAction: PlaybackState.CustomAction ): Boolean { - if (firstAction.action != secondAction.action || + if ( + firstAction.action != secondAction.action || firstAction.name != secondAction.name || - firstAction.icon != secondAction.icon) { + firstAction.icon != secondAction.icon + ) { return false } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt new file mode 100644 index 000000000000..8f3f0548230f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt @@ -0,0 +1,112 @@ +/* + * 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.media.controls.pipeline + +import android.media.session.PlaybackState +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.dagger.MediaTimeoutListenerLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import javax.inject.Inject + +private const val TAG = "MediaTimeout" + +/** A buffered log for [MediaTimeoutListener] events */ +@SysUISingleton +class MediaTimeoutLogger +@Inject +constructor(@MediaTimeoutListenerLog private val buffer: LogBuffer) { + fun logReuseListener(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "reuse listener: $str1" }) + + fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) = + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = oldKey + str2 = newKey + bool1 = hadListener + }, + { "migrate from $str1 to $str2, had listener? $bool1" } + ) + + fun logUpdateListener(key: String, wasPlaying: Boolean) = + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = key + bool1 = wasPlaying + }, + { "updating $str1, was playing? $bool1" } + ) + + fun logDelayedUpdate(key: String) = + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { "deliver delayed playback state for $str1" } + ) + + fun logSessionDestroyed(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "session destroyed $str1" }) + + fun logPlaybackState(key: String, state: PlaybackState?) = + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = key + str2 = state?.toString() + }, + { "state update: key=$str1 state=$str2" } + ) + + fun logStateCallback(key: String) = + buffer.log(TAG, LogLevel.VERBOSE, { str1 = key }, { "dispatching state update for $key" }) + + fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) = + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = key + bool1 = playing + bool2 = resumption + }, + { "schedule timeout $str1, playing=$bool1 resumption=$bool2" } + ) + + fun logCancelIgnored(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "cancellation already exists for $str1" }) + + fun logTimeout(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "execute timeout for $str1" }) + + fun logTimeoutCancelled(key: String, reason: String) = + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = key + str2 = reason + }, + { "media timeout cancelled for $str1, reason: $str2" } + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaBrowserFactory.java index aca033e99623..00620b5b2575 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaBrowserFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.resume; import android.content.ComponentName; import android.content.Context; diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt index cc06b6c67879..4891297dbcf9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.resume import android.content.BroadcastReceiver import android.content.ComponentName @@ -33,6 +33,9 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT import com.android.systemui.tuner.TunerService import com.android.systemui.util.Utils import com.android.systemui.util.time.SystemClock @@ -47,7 +50,9 @@ private const val MEDIA_PREFERENCES = "media_control_prefs" private const val MEDIA_PREFERENCE_KEY = "browser_components_" @SysUISingleton -class MediaResumeListener @Inject constructor( +class MediaResumeListener +@Inject +constructor( private val context: Context, private val broadcastDispatcher: BroadcastDispatcher, @Background private val backgroundExecutor: Executor, @@ -59,7 +64,7 @@ class MediaResumeListener @Inject constructor( private var useMediaResumption: Boolean = Utils.useMediaResumption(context) private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> = - ConcurrentLinkedQueue() + ConcurrentLinkedQueue() private lateinit var mediaDataManager: MediaDataManager @@ -72,40 +77,49 @@ class MediaResumeListener @Inject constructor( private var currentUserId: Int = context.userId @VisibleForTesting - val userChangeReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (Intent.ACTION_USER_UNLOCKED == intent.action) { - loadMediaResumptionControls() - } else if (Intent.ACTION_USER_SWITCHED == intent.action) { - currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) - loadSavedComponents() + val userChangeReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (Intent.ACTION_USER_UNLOCKED == intent.action) { + loadMediaResumptionControls() + } else if (Intent.ACTION_USER_SWITCHED == intent.action) { + currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) + loadSavedComponents() + } } } - } - private val mediaBrowserCallback = object : ResumeMediaBrowser.Callback() { - override fun addTrack( - desc: MediaDescription, - component: ComponentName, - browser: ResumeMediaBrowser - ) { - val token = browser.token - val appIntent = browser.appIntent - val pm = context.getPackageManager() - var appName: CharSequence = component.packageName - val resumeAction = getResumeAction(component) - try { - appName = pm.getApplicationLabel( - pm.getApplicationInfo(component.packageName, 0)) - } catch (e: PackageManager.NameNotFoundException) { - Log.e(TAG, "Error getting package information", e) - } + private val mediaBrowserCallback = + object : ResumeMediaBrowser.Callback() { + override fun addTrack( + desc: MediaDescription, + component: ComponentName, + browser: ResumeMediaBrowser + ) { + val token = browser.token + val appIntent = browser.appIntent + val pm = context.getPackageManager() + var appName: CharSequence = component.packageName + val resumeAction = getResumeAction(component) + try { + appName = + pm.getApplicationLabel(pm.getApplicationInfo(component.packageName, 0)) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(TAG, "Error getting package information", e) + } - Log.d(TAG, "Adding resume controls $desc") - mediaDataManager.addResumptionControls(currentUserId, desc, resumeAction, token, - appName.toString(), appIntent, component.packageName) + Log.d(TAG, "Adding resume controls $desc") + mediaDataManager.addResumptionControls( + currentUserId, + desc, + resumeAction, + token, + appName.toString(), + appIntent, + component.packageName + ) + } } - } init { if (useMediaResumption) { @@ -113,8 +127,12 @@ class MediaResumeListener @Inject constructor( val unlockFilter = IntentFilter() unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED) unlockFilter.addAction(Intent.ACTION_USER_SWITCHED) - broadcastDispatcher.registerReceiver(userChangeReceiver, unlockFilter, null, - UserHandle.ALL) + broadcastDispatcher.registerReceiver( + userChangeReceiver, + unlockFilter, + null, + UserHandle.ALL + ) loadSavedComponents() } } @@ -123,12 +141,15 @@ class MediaResumeListener @Inject constructor( mediaDataManager = manager // Add listener for resumption setting changes - tunerService.addTunable(object : TunerService.Tunable { - override fun onTuningChanged(key: String?, newValue: String?) { - useMediaResumption = Utils.useMediaResumption(context) - mediaDataManager.setMediaResumptionEnabled(useMediaResumption) - } - }, Settings.Secure.MEDIA_CONTROLS_RESUME) + tunerService.addTunable( + object : TunerService.Tunable { + override fun onTuningChanged(key: String?, newValue: String?) { + useMediaResumption = Utils.useMediaResumption(context) + mediaDataManager.setMediaResumptionEnabled(useMediaResumption) + } + }, + Settings.Secure.MEDIA_CONTROLS_RESUME + ) } private fun loadSavedComponents() { @@ -136,8 +157,10 @@ class MediaResumeListener @Inject constructor( resumeComponents.clear() val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE) val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null) - val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex()) - ?.dropLastWhile { it.isEmpty() } + val components = + listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())?.dropLastWhile { + it.isEmpty() + } var needsUpdate = false components?.forEach { val info = it.split("/") @@ -145,17 +168,18 @@ class MediaResumeListener @Inject constructor( val className = info[1] val component = ComponentName(packageName, className) - val lastPlayed = if (info.size == 3) { - try { - info[2].toLong() - } catch (e: NumberFormatException) { + val lastPlayed = + if (info.size == 3) { + try { + info[2].toLong() + } catch (e: NumberFormatException) { + needsUpdate = true + systemClock.currentTimeMillis() + } + } else { needsUpdate = true systemClock.currentTimeMillis() } - } else { - needsUpdate = true - systemClock.currentTimeMillis() - } resumeComponents.add(component to lastPlayed) } Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}") @@ -166,9 +190,7 @@ class MediaResumeListener @Inject constructor( } } - /** - * Load controls for resuming media, if available - */ + /** Load controls for resuming media, if available */ private fun loadMediaResumptionControls() { if (!useMediaResumption) { return @@ -204,9 +226,7 @@ class MediaResumeListener @Inject constructor( val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE) val resumeInfo = pm.queryIntentServices(serviceIntent, 0) - val inf = resumeInfo?.filter { - it.serviceInfo.packageName == data.packageName - } + val inf = resumeInfo?.filter { it.serviceInfo.packageName == data.packageName } if (inf != null && inf.size > 0) { backgroundExecutor.execute { tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName) @@ -227,7 +247,8 @@ class MediaResumeListener @Inject constructor( Log.d(TAG, "Testing if we can connect to $componentName") // Set null action to prevent additional attempts to connect mediaDataManager.setResumeAction(key, null) - mediaBrowser = mediaBrowserFactory.create( + mediaBrowser = + mediaBrowserFactory.create( object : ResumeMediaBrowser.Callback() { override fun onConnected() { Log.d(TAG, "Connected to $componentName") @@ -250,7 +271,8 @@ class MediaResumeListener @Inject constructor( mediaBrowser = null } }, - componentName) + componentName + ) mediaBrowser?.testConnection() } @@ -285,9 +307,7 @@ class MediaResumeListener @Inject constructor( prefs.edit().putString(MEDIA_PREFERENCE_KEY + currentUserId, sb.toString()).apply() } - /** - * Get a runnable which will resume media playback - */ + /** Get a runnable which will resume media playback */ private fun getResumeAction(componentName: ComponentName): Runnable { return Runnable { mediaBrowser = mediaBrowserFactory.create(null, componentName) @@ -296,8 +316,6 @@ class MediaResumeListener @Inject constructor( } override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.apply { - println("resumeComponents: $resumeComponents") - } + pw.apply { println("resumeComponents: $resumeComponents") } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java index 40a5653a15a0..3493b2453fd6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.resume; import android.annotation.Nullable; import android.app.PendingIntent; @@ -293,7 +293,7 @@ public class ResumeMediaBrowser { public PendingIntent getAppIntent() { PackageManager pm = mContext.getPackageManager(); Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName()); - return PendingIntent.getActivity(mContext, 0, launchIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED); + return PendingIntent.getActivity(mContext, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE); } /** diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java index 3d1380b6bd24..c558227df0b5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.resume; import android.content.ComponentName; import android.content.Context; diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt index 41f735486c7e..335ce1d3d694 100644 --- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt @@ -14,61 +14,60 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.resume import android.content.ComponentName import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaBrowserLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** A logger for events in [ResumeMediaBrowser]. */ @SysUISingleton -class ResumeMediaBrowserLogger @Inject constructor( - @MediaBrowserLog private val buffer: LogBuffer -) { +class ResumeMediaBrowserLogger @Inject constructor(@MediaBrowserLog private val buffer: LogBuffer) { /** Logs that we've initiated a connection to a [android.media.browse.MediaBrowser]. */ - fun logConnection(componentName: ComponentName, reason: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = componentName.toShortString() - str2 = reason - }, - { "Connecting browser for component $str1 due to $str2" } - ) + fun logConnection(componentName: ComponentName, reason: String) = + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = componentName.toShortString() + str2 = reason + }, + { "Connecting browser for component $str1 due to $str2" } + ) /** Logs that we've disconnected from a [android.media.browse.MediaBrowser]. */ - fun logDisconnect(componentName: ComponentName) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = componentName.toShortString() - }, - { "Disconnecting browser for component $str1" } - ) + fun logDisconnect(componentName: ComponentName) = + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = componentName.toShortString() }, + { "Disconnecting browser for component $str1" } + ) /** * Logs that we received a [android.media.session.MediaController.Callback.onSessionDestroyed] * event. * * @param isBrowserConnected true if there's a currently connected + * ``` * [android.media.browse.MediaBrowser] and false otherwise. - * @param componentName the component name for the [ResumeMediaBrowser] that triggered this log. + * @param componentName + * ``` + * the component name for the [ResumeMediaBrowser] that triggered this log. */ - fun logSessionDestroyed( - isBrowserConnected: Boolean, - componentName: ComponentName - ) = buffer.log( - TAG, - LogLevel.DEBUG, - { - bool1 = isBrowserConnected - str1 = componentName.toShortString() - }, - { "Session destroyed. Active browser = $bool1. Browser component = $str1." } - ) + fun logSessionDestroyed(isBrowserConnected: Boolean, componentName: ComponentName) = + buffer.log( + TAG, + LogLevel.DEBUG, + { + bool1 = isBrowserConnected + str1 = componentName.toShortString() + }, + { "Session destroyed. Active browser = $bool1. Browser component = $str1." } + ) } private const val TAG = "MediaBrowser" diff --git a/packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/AnimationBindHandler.kt index 013683e962a4..d2793bca867b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/AnimationBindHandler.kt @@ -14,19 +14,23 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.graphics.drawable.Animatable2 import android.graphics.drawable.Drawable /** - * AnimationBindHandler is responsible for tracking the bound animation state and preventing - * jank and conflicts due to media notifications arriving at any time during an animation. It - * does this in two parts. - * - Exit animations fired as a result of user input are tracked. When these are running, any + * AnimationBindHandler is responsible for tracking the bound animation state and preventing jank + * and conflicts due to media notifications arriving at any time during an animation. It does this + * in two parts. + * - Exit animations fired as a result of user input are tracked. When these are running, any + * ``` * bind actions are delayed until the animation completes (and then fired in sequence). - * - Continuous animations are tracked using their rebind id. Later calls using the same + * ``` + * - Continuous animations are tracked using their rebind id. Later calls using the same + * ``` * rebind id will be totally ignored to prevent the continuous animation from restarting. + * ``` */ internal class AnimationBindHandler : Animatable2.AnimationCallback() { private val onAnimationsComplete = mutableListOf<() -> Unit>() @@ -37,10 +41,10 @@ internal class AnimationBindHandler : Animatable2.AnimationCallback() { get() = registrations.any { it.isRunning } /** - * This check prevents rebinding to the action button if the identifier has not changed. A - * null value is always considered to be changed. This is used to prevent the connecting - * animation from rebinding (and restarting) if multiple buffer PlaybackStates are pushed by - * an application in a row. + * This check prevents rebinding to the action button if the identifier has not changed. A null + * value is always considered to be changed. This is used to prevent the connecting animation + * from rebinding (and restarting) if multiple buffer PlaybackStates are pushed by an + * application in a row. */ fun updateRebindId(newRebindId: Int?): Boolean { if (rebindId == null || newRebindId == null || rebindId != newRebindId) { @@ -78,4 +82,4 @@ internal class AnimationBindHandler : Animatable2.AnimationCallback() { onAnimationsComplete.clear() } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt new file mode 100644 index 000000000000..61ef2f1838e7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt @@ -0,0 +1,223 @@ +/* + * 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.media.controls.ui + +import android.animation.ArgbEvaluator +import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.graphics.drawable.RippleDrawable +import com.android.internal.R +import com.android.internal.annotations.VisibleForTesting +import com.android.settingslib.Utils +import com.android.systemui.media.controls.models.player.MediaViewHolder +import com.android.systemui.monet.ColorScheme + +/** + * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme] + * is triggered. + */ +interface ColorTransition { + fun updateColorScheme(scheme: ColorScheme?): Boolean +} + +/** + * A [ColorTransition] that animates between two specific colors. It uses a ValueAnimator to execute + * the animation and interpolate between the source color and the target color. + * + * Selection of the target color from the scheme, and application of the interpolated color are + * delegated to callbacks. + */ +open class AnimatingColorTransition( + private val defaultColor: Int, + private val extractColor: (ColorScheme) -> Int, + private val applyColor: (Int) -> Unit +) : AnimatorUpdateListener, ColorTransition { + + private val argbEvaluator = ArgbEvaluator() + private val valueAnimator = buildAnimator() + var sourceColor: Int = defaultColor + var currentColor: Int = defaultColor + var targetColor: Int = defaultColor + + override fun onAnimationUpdate(animation: ValueAnimator) { + currentColor = + argbEvaluator.evaluate(animation.animatedFraction, sourceColor, targetColor) as Int + applyColor(currentColor) + } + + override fun updateColorScheme(scheme: ColorScheme?): Boolean { + val newTargetColor = if (scheme == null) defaultColor else extractColor(scheme) + if (newTargetColor != targetColor) { + sourceColor = currentColor + targetColor = newTargetColor + valueAnimator.cancel() + valueAnimator.start() + return true + } + return false + } + + init { + applyColor(defaultColor) + } + + @VisibleForTesting + open fun buildAnimator(): ValueAnimator { + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = 333 + animator.addUpdateListener(this) + return animator + } +} + +typealias AnimatingColorTransitionFactory = + (Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition + +/** + * ColorSchemeTransition constructs a ColorTransition for each color in the scheme that needs to be + * transitioned when changed. It also sets up the assignment functions for sending the sending the + * interpolated colors to the appropriate views. + */ +class ColorSchemeTransition +internal constructor( + private val context: Context, + private val mediaViewHolder: MediaViewHolder, + animatingColorTransitionFactory: AnimatingColorTransitionFactory +) { + constructor( + context: Context, + mediaViewHolder: MediaViewHolder + ) : this(context, mediaViewHolder, ::AnimatingColorTransition) + + val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95) + val surfaceColor = + animatingColorTransitionFactory(bgColor, ::surfaceFromScheme) { surfaceColor -> + val colorList = ColorStateList.valueOf(surfaceColor) + mediaViewHolder.seamlessIcon.imageTintList = colorList + mediaViewHolder.seamlessText.setTextColor(surfaceColor) + mediaViewHolder.albumView.backgroundTintList = colorList + mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor) + } + + val accentPrimary = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorPrimary), + ::accentPrimaryFromScheme + ) { accentPrimary -> + val accentColorList = ColorStateList.valueOf(accentPrimary) + mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList + mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary) + } + + val accentSecondary = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorPrimary), + ::accentSecondaryFromScheme + ) { accentSecondary -> + val colorList = ColorStateList.valueOf(accentSecondary) + (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let { + it.setColor(colorList) + it.effectColor = colorList + } + } + + val colorSeamless = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorPrimary), + { colorScheme: ColorScheme -> + // A1-100 dark in dark theme, A1-200 in light theme + if ( + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + UI_MODE_NIGHT_YES + ) + colorScheme.accent1[2] + else colorScheme.accent1[3] + }, + { seamlessColor: Int -> + val accentColorList = ColorStateList.valueOf(seamlessColor) + mediaViewHolder.seamlessButton.backgroundTintList = accentColorList + } + ) + + val textPrimary = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorPrimary), + ::textPrimaryFromScheme + ) { textPrimary -> + mediaViewHolder.titleText.setTextColor(textPrimary) + val textColorList = ColorStateList.valueOf(textPrimary) + mediaViewHolder.seekBar.thumb.setTintList(textColorList) + mediaViewHolder.seekBar.progressTintList = textColorList + mediaViewHolder.scrubbingElapsedTimeView.setTextColor(textColorList) + mediaViewHolder.scrubbingTotalTimeView.setTextColor(textColorList) + for (button in mediaViewHolder.getTransparentActionButtons()) { + button.imageTintList = textColorList + } + mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary) + } + + val textPrimaryInverse = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorPrimaryInverse), + ::textPrimaryInverseFromScheme + ) { textPrimaryInverse -> + mediaViewHolder.actionPlayPause.imageTintList = + ColorStateList.valueOf(textPrimaryInverse) + } + + val textSecondary = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorSecondary), + ::textSecondaryFromScheme + ) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) } + + val textTertiary = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorTertiary), + ::textTertiaryFromScheme + ) { textTertiary -> + mediaViewHolder.seekBar.progressBackgroundTintList = + ColorStateList.valueOf(textTertiary) + } + + val colorTransitions = + arrayOf( + surfaceColor, + colorSeamless, + accentPrimary, + accentSecondary, + textPrimary, + textPrimaryInverse, + textSecondary, + textTertiary, + ) + + private fun loadDefaultColor(id: Int): Int { + return Utils.getColorAttr(context, id).defaultColor + } + + fun updateColorScheme(colorScheme: ColorScheme?): Boolean { + var anyChanged = false + colorTransitions.forEach { anyChanged = it.updateColorScheme(colorScheme) || anyChanged } + colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme } + return anyChanged + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt index 121ddd46976d..9f86cd88788b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -42,22 +42,20 @@ import org.xmlpull.v1.XmlPullParser private const val BACKGROUND_ANIM_DURATION = 370L -/** - * Drawable that can draw an animated gradient when tapped. - */ +/** Drawable that can draw an animated gradient when tapped. */ @Keep class IlluminationDrawable : Drawable() { private var themeAttrs: IntArray? = null private var cornerRadiusOverride = -1f var cornerRadius = 0f - get() { - return if (cornerRadiusOverride >= 0) { - cornerRadiusOverride - } else { - field + get() { + return if (cornerRadiusOverride >= 0) { + cornerRadiusOverride + } else { + field + } } - } private var highlightColor = Color.TRANSPARENT private var tmpHsl = floatArrayOf(0f, 0f, 0f) private var paint = Paint() @@ -65,22 +63,27 @@ class IlluminationDrawable : Drawable() { private val lightSources = arrayListOf<LightSourceDrawable>() private var backgroundColor = Color.TRANSPARENT - set(value) { - if (value == field) { - return + set(value) { + if (value == field) { + return + } + field = value + animateBackground() } - field = value - animateBackground() - } private var backgroundAnimation: ValueAnimator? = null - /** - * Draw background and gradient. - */ + /** Draw background and gradient. */ override fun draw(canvas: Canvas) { - canvas.drawRoundRect(0f, 0f, bounds.width().toFloat(), bounds.height().toFloat(), - cornerRadius, cornerRadius, paint) + canvas.drawRoundRect( + 0f, + 0f, + bounds.width().toFloat(), + bounds.height().toFloat(), + cornerRadius, + cornerRadius, + paint + ) } override fun getOutline(outline: Outline) { @@ -105,12 +108,11 @@ class IlluminationDrawable : Drawable() { private fun updateStateFromTypedArray(a: TypedArray) { if (a.hasValue(R.styleable.IlluminationDrawable_cornerRadius)) { - cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, - cornerRadius) + cornerRadius = + a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, cornerRadius) } if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) { - highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / - 100f + highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f } } @@ -163,34 +165,42 @@ class IlluminationDrawable : Drawable() { private fun animateBackground() { ColorUtils.colorToHSL(backgroundColor, tmpHsl) val L = tmpHsl[2] - tmpHsl[2] = MathUtils.constrain(if (L < 1f - highlight) { - L + highlight - } else { - L - highlight - }, 0f, 1f) + tmpHsl[2] = + MathUtils.constrain( + if (L < 1f - highlight) { + L + highlight + } else { + L - highlight + }, + 0f, + 1f + ) val initialBackground = paint.color val initialHighlight = highlightColor val finalHighlight = ColorUtils.HSLToColor(tmpHsl) backgroundAnimation?.cancel() - backgroundAnimation = ValueAnimator.ofFloat(0f, 1f).apply { - duration = BACKGROUND_ANIM_DURATION - interpolator = Interpolators.FAST_OUT_LINEAR_IN - addUpdateListener { - val progress = it.animatedValue as Float - paint.color = blendARGB(initialBackground, backgroundColor, progress) - highlightColor = blendARGB(initialHighlight, finalHighlight, progress) - lightSources.forEach { it.highlightColor = highlightColor } - invalidateSelf() - } - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - backgroundAnimation = null + backgroundAnimation = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = BACKGROUND_ANIM_DURATION + interpolator = Interpolators.FAST_OUT_LINEAR_IN + addUpdateListener { + val progress = it.animatedValue as Float + paint.color = blendARGB(initialBackground, backgroundColor, progress) + highlightColor = blendARGB(initialHighlight, finalHighlight, progress) + lightSources.forEach { it.highlightColor = highlightColor } + invalidateSelf() } - }) - start() - } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + backgroundAnimation = null + } + } + ) + start() + } } override fun setTintList(tint: ColorStateList?) { @@ -215,4 +225,4 @@ class IlluminationDrawable : Drawable() { fun setCornerRadiusOverride(cornerRadius: Float?) { cornerRadiusOverride = cornerRadius ?: -1f } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt index 32600fba61a4..899148b0014c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.content.Context import android.content.res.Configuration @@ -45,7 +45,9 @@ import javax.inject.Named * switches media player positioning between split pane container vs single pane container */ @SysUISingleton -class KeyguardMediaController @Inject constructor( +class KeyguardMediaController +@Inject +constructor( @param:Named(KEYGUARD) private val mediaHost: MediaHost, private val bypassController: KeyguardBypassController, private val statusBarStateController: SysuiStatusBarStateController, @@ -56,34 +58,40 @@ class KeyguardMediaController @Inject constructor( ) { init { - statusBarStateController.addCallback(object : StatusBarStateController.StateListener { - override fun onStateChanged(newState: Int) { - refreshMediaPosition() + statusBarStateController.addCallback( + object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) { + refreshMediaPosition() + } } - }) - configurationController.addCallback(object : ConfigurationController.ConfigurationListener { - override fun onConfigChanged(newConfig: Configuration?) { - updateResources() + ) + configurationController.addCallback( + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration?) { + updateResources() + } } - }) + ) - val settingsObserver: ContentObserver = object : ContentObserver(handler) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - if (uri == lockScreenMediaPlayerUri) { - allowMediaPlayerOnLockScreen = + val settingsObserver: ContentObserver = + object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + if (uri == lockScreenMediaPlayerUri) { + allowMediaPlayerOnLockScreen = secureSettings.getBoolForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - true, - UserHandle.USER_CURRENT + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + true, + UserHandle.USER_CURRENT ) - refreshMediaPosition() + refreshMediaPosition() + } } } - } secureSettings.registerContentObserverForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - settingsObserver, - UserHandle.USER_ALL) + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + settingsObserver, + UserHandle.USER_ALL + ) // First let's set the desired state that we want for this host mediaHost.expansion = MediaHostState.EXPANDED @@ -110,27 +118,21 @@ class KeyguardMediaController @Inject constructor( refreshMediaPosition() } - /** - * Is the media player visible? - */ + /** Is the media player visible? */ var visible = false private set var visibilityChangedListener: ((Boolean) -> Unit)? = null - /** - * single pane media container placed at the top of the notifications list - */ + /** single pane media container placed at the top of the notifications list */ var singlePaneContainer: MediaContainerView? = null private set private var splitShadeContainer: ViewGroup? = null - /** - * Track the media player setting status on lock screen. - */ + /** Track the media player setting status on lock screen. */ private var allowMediaPlayerOnLockScreen: Boolean = true private val lockScreenMediaPlayerUri = - secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) + secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) /** * Attaches media container in single pane mode, situated at the top of the notifications list @@ -146,9 +148,7 @@ class KeyguardMediaController @Inject constructor( onMediaHostVisibilityChanged(mediaHost.visible) } - /** - * Called whenever the media hosts visibility changes - */ + /** Called whenever the media hosts visibility changes */ private fun onMediaHostVisibilityChanged(visible: Boolean) { refreshMediaPosition() if (visible) { @@ -159,9 +159,7 @@ class KeyguardMediaController @Inject constructor( } } - /** - * Attaches media container in split shade mode, situated to the left of notifications - */ + /** Attaches media container in split shade mode, situated to the left of notifications */ fun attachSplitShadeContainer(container: ViewGroup) { splitShadeContainer = container reattachHostView() @@ -183,9 +181,7 @@ class KeyguardMediaController @Inject constructor( } if (activeContainer?.childCount == 0) { // Detach the hostView from its parent view if exists - mediaHost.hostView.parent?.let { - (it as? ViewGroup)?.removeView(mediaHost.hostView) - } + mediaHost.hostView.parent?.let { (it as? ViewGroup)?.removeView(mediaHost.hostView) } activeContainer.addView(mediaHost.hostView) } } @@ -193,7 +189,8 @@ class KeyguardMediaController @Inject constructor( fun refreshMediaPosition() { val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD) // mediaHost.visible required for proper animations handling - visible = mediaHost.visible && + visible = + mediaHost.visible && !bypassController.bypassEnabled && keyguardOrUserSwitcher && allowMediaPlayerOnLockScreen diff --git a/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt index 711cb361f437..dd5c2bf497cb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -55,9 +55,7 @@ private data class RippleData( var highlight: Float ) -/** - * Drawable that can draw an animated gradient when tapped. - */ +/** Drawable that can draw an animated gradient when tapped. */ @Keep class LightSourceDrawable : Drawable() { @@ -67,17 +65,15 @@ class LightSourceDrawable : Drawable() { private var paint = Paint() var highlightColor = Color.WHITE - set(value) { - if (field == value) { - return + set(value) { + if (field == value) { + return + } + field = value + invalidateSelf() } - field = value - invalidateSelf() - } - /** - * Draw a small highlight under the finger before expanding (or cancelling) it. - */ + /** Draw a small highlight under the finger before expanding (or cancelling) it. */ private var active: Boolean = false set(value) { if (value == field) { @@ -91,46 +87,54 @@ class LightSourceDrawable : Drawable() { rippleData.progress = RIPPLE_DOWN_PROGRESS } else { rippleAnimation?.cancel() - rippleAnimation = ValueAnimator.ofFloat(rippleData.alpha, 0f).apply { - duration = RIPPLE_CANCEL_DURATION - interpolator = Interpolators.LINEAR_OUT_SLOW_IN - addUpdateListener { - rippleData.alpha = it.animatedValue as Float - invalidateSelf() - } - addListener(object : AnimatorListenerAdapter() { - var cancelled = false - override fun onAnimationCancel(animation: Animator?) { - cancelled = true - } - - override fun onAnimationEnd(animation: Animator?) { - if (cancelled) { - return - } - rippleData.progress = 0f - rippleData.alpha = 0f - rippleAnimation = null + rippleAnimation = + ValueAnimator.ofFloat(rippleData.alpha, 0f).apply { + duration = RIPPLE_CANCEL_DURATION + interpolator = Interpolators.LINEAR_OUT_SLOW_IN + addUpdateListener { + rippleData.alpha = it.animatedValue as Float invalidateSelf() } - }) - start() - } + addListener( + object : AnimatorListenerAdapter() { + var cancelled = false + override fun onAnimationCancel(animation: Animator?) { + cancelled = true + } + + override fun onAnimationEnd(animation: Animator?) { + if (cancelled) { + return + } + rippleData.progress = 0f + rippleData.alpha = 0f + rippleAnimation = null + invalidateSelf() + } + } + ) + start() + } } invalidateSelf() } private var rippleAnimation: Animator? = null - /** - * Draw background and gradient. - */ + /** Draw background and gradient. */ override fun draw(canvas: Canvas) { val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress) val centerColor = - ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt()) - paint.shader = RadialGradient(rippleData.x, rippleData.y, radius, - intArrayOf(centerColor, Color.TRANSPARENT), GRADIENT_STOPS, Shader.TileMode.CLAMP) + ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt()) + paint.shader = + RadialGradient( + rippleData.x, + rippleData.y, + radius, + intArrayOf(centerColor, Color.TRANSPARENT), + GRADIENT_STOPS, + Shader.TileMode.CLAMP + ) canvas.drawCircle(rippleData.x, rippleData.y, radius, paint) } @@ -162,8 +166,8 @@ class LightSourceDrawable : Drawable() { rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f) } if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) { - rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / - 100f + rippleData.highlight = + a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f } } @@ -193,40 +197,44 @@ class LightSourceDrawable : Drawable() { invalidateSelf() } - /** - * Draws an animated ripple that expands fading away. - */ + /** Draws an animated ripple that expands fading away. */ private fun illuminate() { rippleData.alpha = 1f invalidateSelf() rippleAnimation?.cancel() - rippleAnimation = AnimatorSet().apply { - playTogether(ValueAnimator.ofFloat(1f, 0f).apply { - startDelay = 133 - duration = RIPPLE_ANIM_DURATION - startDelay - interpolator = Interpolators.LINEAR_OUT_SLOW_IN - addUpdateListener { - rippleData.alpha = it.animatedValue as Float - invalidateSelf() - } - }, ValueAnimator.ofFloat(rippleData.progress, 1f).apply { - duration = RIPPLE_ANIM_DURATION - interpolator = Interpolators.LINEAR_OUT_SLOW_IN - addUpdateListener { - rippleData.progress = it.animatedValue as Float - invalidateSelf() - } - }) - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - rippleData.progress = 0f - rippleAnimation = null - invalidateSelf() - } - }) - start() - } + rippleAnimation = + AnimatorSet().apply { + playTogether( + ValueAnimator.ofFloat(1f, 0f).apply { + startDelay = 133 + duration = RIPPLE_ANIM_DURATION - startDelay + interpolator = Interpolators.LINEAR_OUT_SLOW_IN + addUpdateListener { + rippleData.alpha = it.animatedValue as Float + invalidateSelf() + } + }, + ValueAnimator.ofFloat(rippleData.progress, 1f).apply { + duration = RIPPLE_ANIM_DURATION + interpolator = Interpolators.LINEAR_OUT_SLOW_IN + addUpdateListener { + rippleData.progress = it.animatedValue as Float + invalidateSelf() + } + } + ) + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + rippleData.progress = 0f + rippleAnimation = null + invalidateSelf() + } + } + ) + start() + } } override fun setHotspot(x: Float, y: Float) { @@ -251,8 +259,13 @@ class LightSourceDrawable : Drawable() { override fun getDirtyBounds(): Rect { val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress) - val bounds = Rect((rippleData.x - radius).toInt(), (rippleData.y - radius).toInt(), - (rippleData.x + radius).toInt(), (rippleData.y + radius).toInt()) + val bounds = + Rect( + (rippleData.x - radius).toInt(), + (rippleData.y - radius).toInt(), + (rippleData.x + radius).toInt(), + (rippleData.y + radius).toInt() + ) bounds.union(super.getDirtyBounds()) return bounds } @@ -293,4 +306,4 @@ class LightSourceDrawable : Drawable() { return changed } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt new file mode 100644 index 000000000000..e38c1baaeae9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt @@ -0,0 +1,1327 @@ +/* + * 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.media.controls.ui + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS +import android.util.Log +import android.util.MathUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.PathInterpolator +import android.widget.LinearLayout +import androidx.annotation.VisibleForTesting +import com.android.internal.logging.InstanceId +import com.android.systemui.Dumpable +import com.android.systemui.R +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaViewHolder +import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.ui.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.media.controls.util.SmallHash +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.PageIndicator +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener +import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.Utils +import com.android.systemui.util.animation.UniqueObjectHostView +import com.android.systemui.util.animation.requiresRemeasuring +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock +import com.android.systemui.util.traceSection +import java.io.PrintWriter +import java.util.TreeMap +import javax.inject.Inject +import javax.inject.Provider + +private const val TAG = "MediaCarouselController" +private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS) +private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + +/** + * Class that is responsible for keeping the view carousel up to date. This also handles changes in + * state and applies them to the media carousel like the expansion. + */ +@SysUISingleton +class MediaCarouselController +@Inject +constructor( + private val context: Context, + private val mediaControlPanelFactory: Provider<MediaControlPanel>, + private val visualStabilityProvider: VisualStabilityProvider, + private val mediaHostStatesManager: MediaHostStatesManager, + private val activityStarter: ActivityStarter, + private val systemClock: SystemClock, + @Main executor: DelayableExecutor, + private val mediaManager: MediaDataManager, + configurationController: ConfigurationController, + falsingCollector: FalsingCollector, + falsingManager: FalsingManager, + dumpManager: DumpManager, + private val logger: MediaUiEventLogger, + private val debugLogger: MediaCarouselControllerLogger +) : Dumpable { + /** The current width of the carousel */ + private var currentCarouselWidth: Int = 0 + + /** The current height of the carousel */ + private var currentCarouselHeight: Int = 0 + + /** Are we currently showing only active players */ + private var currentlyShowingOnlyActive: Boolean = false + + /** Is the player currently visible (at the end of the transformation */ + private var playersVisible: Boolean = false + /** + * The desired location where we'll be at the end of the transformation. Usually this matches + * the end location, except when we're still waiting on a state update call. + */ + @MediaLocation private var desiredLocation: Int = -1 + + /** + * The ending location of the view where it ends when all animations and transitions have + * finished + */ + @MediaLocation @VisibleForTesting var currentEndLocation: Int = -1 + + /** + * The ending location of the view where it ends when all animations and transitions have + * finished + */ + @MediaLocation private var currentStartLocation: Int = -1 + + /** The progress of the transition or 1.0 if there is no transition happening */ + private var currentTransitionProgress: Float = 1.0f + + /** The measured width of the carousel */ + private var carouselMeasureWidth: Int = 0 + + /** The measured height of the carousel */ + private var carouselMeasureHeight: Int = 0 + private var desiredHostState: MediaHostState? = null + private val mediaCarousel: MediaScrollView + val mediaCarouselScrollHandler: MediaCarouselScrollHandler + val mediaFrame: ViewGroup + @VisibleForTesting + lateinit var settingsButton: View + private set + private val mediaContent: ViewGroup + @VisibleForTesting val pageIndicator: PageIndicator + private val visualStabilityCallback: OnReorderingAllowedListener + private var needsReordering: Boolean = false + private var keysNeedRemoval = mutableSetOf<String>() + var shouldScrollToKey: Boolean = false + private var isRtl: Boolean = false + set(value) { + if (value != field) { + field = value + mediaFrame.layoutDirection = + if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR + mediaCarouselScrollHandler.scrollToStart() + } + } + private var currentlyExpanded = true + set(value) { + if (field != value) { + field = value + for (player in MediaPlayerData.players()) { + player.setListening(field) + } + } + } + + companion object { + const val ANIMATION_BASE_DURATION = 2200f + const val DURATION = 167f + const val DETAILS_DELAY = 1067f + const val CONTROLS_DELAY = 1400f + const val PAGINATION_DELAY = 1900f + const val MEDIATITLES_DELAY = 1000f + const val MEDIACONTAINERS_DELAY = 967f + val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F) + val REVERSE_BEZIER = PathInterpolator(0F, 0.68F, 1F, 0F) + + fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float { + val transformStartFraction = delay / ANIMATION_BASE_DURATION + val transformDurationFraction = duration / ANIMATION_BASE_DURATION + val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction) + return MathUtils.constrain( + (squishinessToTime - transformStartFraction) / transformDurationFraction, + 0F, + 1F + ) + } + } + + private val configListener = + object : ConfigurationController.ConfigurationListener { + override fun onDensityOrFontScaleChanged() { + // System font changes should only happen when UMO is offscreen or a flicker may + // occur + updatePlayers(recreateMedia = true) + inflateSettingsButton() + } + + override fun onThemeChanged() { + updatePlayers(recreateMedia = false) + inflateSettingsButton() + } + + override fun onConfigChanged(newConfig: Configuration?) { + if (newConfig == null) return + isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL + } + + override fun onUiModeChanged() { + updatePlayers(recreateMedia = false) + inflateSettingsButton() + } + } + + /** + * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility. + * It will be called when the container is out of view. + */ + lateinit var updateUserVisibility: () -> Unit + lateinit var updateHostVisibility: () -> Unit + + private val isReorderingAllowed: Boolean + get() = visualStabilityProvider.isReorderingAllowed + + init { + dumpManager.registerDumpable(TAG, this) + mediaFrame = inflateMediaCarousel() + mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) + pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) + mediaCarouselScrollHandler = + MediaCarouselScrollHandler( + mediaCarousel, + pageIndicator, + executor, + this::onSwipeToDismiss, + this::updatePageIndicatorLocation, + this::closeGuts, + falsingCollector, + falsingManager, + this::logSmartspaceImpression, + logger + ) + isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL + inflateSettingsButton() + mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) + configurationController.addCallback(configListener) + visualStabilityCallback = OnReorderingAllowedListener { + if (needsReordering) { + needsReordering = false + reorderAllPlayers(previousVisiblePlayerKey = null) + } + + keysNeedRemoval.forEach { removePlayer(it) } + if (keysNeedRemoval.size > 0) { + // Carousel visibility may need to be updated after late removals + updateHostVisibility() + } + keysNeedRemoval.clear() + + // Update user visibility so that no extra impression will be logged when + // activeMediaIndex resets to 0 + if (this::updateUserVisibility.isInitialized) { + updateUserVisibility() + } + + // Let's reset our scroll position + mediaCarouselScrollHandler.scrollToStart() + } + visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback) + mediaManager.addListener( + object : MediaDataManager.Listener { + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean, + receivedSmartspaceCardLatency: Int, + isSsReactivated: Boolean + ) { + debugLogger.logMediaLoaded(key) + if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) { + // Log card received if a new resumable media card is added + MediaPlayerData.getMediaPlayer(key)?.let { + /* ktlint-disable max-line-length */ + logSmartspaceCardReported( + 759, // SMARTSPACE_CARD_RECEIVED + it.mSmartspaceId, + it.mUid, + surfaces = + intArrayOf( + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY + ), + rank = MediaPlayerData.getMediaPlayerIndex(key) + ) + /* ktlint-disable max-line-length */ + } + if ( + mediaCarouselScrollHandler.visibleToUser && + mediaCarouselScrollHandler.visibleMediaIndex == + MediaPlayerData.getMediaPlayerIndex(key) + ) { + logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) + } + } else if (receivedSmartspaceCardLatency != 0) { + // Log resume card received if resumable media card is reactivated and + // resume card is ranked first + MediaPlayerData.players().forEachIndexed { index, it -> + if (it.recommendationViewHolder == null) { + it.mSmartspaceId = + SmallHash.hash( + it.mUid + systemClock.currentTimeMillis().toInt() + ) + it.mIsImpressed = false + /* ktlint-disable max-line-length */ + logSmartspaceCardReported( + 759, // SMARTSPACE_CARD_RECEIVED + it.mSmartspaceId, + it.mUid, + surfaces = + intArrayOf( + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY + ), + rank = index, + receivedLatencyMillis = receivedSmartspaceCardLatency + ) + /* ktlint-disable max-line-length */ + } + } + // If media container area already visible to the user, log impression for + // reactivated card. + if ( + mediaCarouselScrollHandler.visibleToUser && + !mediaCarouselScrollHandler.qsExpanded + ) { + logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) + } + } + + val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active + if (canRemove && !Utils.useMediaResumption(context)) { + // This view isn't playing, let's remove this! This happens e.g. when + // dismissing/timing out a view. We still have the data around because + // resumption could be on, but we should save the resources and release + // this. + if (isReorderingAllowed) { + onMediaDataRemoved(key) + } else { + keysNeedRemoval.add(key) + } + } else { + keysNeedRemoval.remove(key) + } + } + + override fun onSmartspaceMediaDataLoaded( + key: String, + data: SmartspaceMediaData, + shouldPrioritize: Boolean + ) { + debugLogger.logRecommendationLoaded(key) + // Log the case where the hidden media carousel with the existed inactive resume + // media is shown by the Smartspace signal. + if (data.isActive) { + val hasActivatedExistedResumeMedia = + !mediaManager.hasActiveMedia() && + mediaManager.hasAnyMedia() && + shouldPrioritize + if (hasActivatedExistedResumeMedia) { + // Log resume card received if resumable media card is reactivated and + // recommendation card is valid and ranked first + MediaPlayerData.players().forEachIndexed { index, it -> + if (it.recommendationViewHolder == null) { + it.mSmartspaceId = + SmallHash.hash( + it.mUid + systemClock.currentTimeMillis().toInt() + ) + it.mIsImpressed = false + /* ktlint-disable max-line-length */ + logSmartspaceCardReported( + 759, // SMARTSPACE_CARD_RECEIVED + it.mSmartspaceId, + it.mUid, + surfaces = + intArrayOf( + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY + ), + rank = index, + receivedLatencyMillis = + (systemClock.currentTimeMillis() - + data.headphoneConnectionTimeMillis) + .toInt() + ) + /* ktlint-disable max-line-length */ + } + } + } + addSmartspaceMediaRecommendations(key, data, shouldPrioritize) + MediaPlayerData.getMediaPlayer(key)?.let { + /* ktlint-disable max-line-length */ + logSmartspaceCardReported( + 759, // SMARTSPACE_CARD_RECEIVED + it.mSmartspaceId, + it.mUid, + surfaces = + intArrayOf( + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY + ), + rank = MediaPlayerData.getMediaPlayerIndex(key), + receivedLatencyMillis = + (systemClock.currentTimeMillis() - + data.headphoneConnectionTimeMillis) + .toInt() + ) + /* ktlint-disable max-line-length */ + } + if ( + mediaCarouselScrollHandler.visibleToUser && + mediaCarouselScrollHandler.visibleMediaIndex == + MediaPlayerData.getMediaPlayerIndex(key) + ) { + logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) + } + } else { + onSmartspaceMediaDataRemoved(data.targetId, immediately = true) + } + } + + override fun onMediaDataRemoved(key: String) { + debugLogger.logMediaRemoved(key) + removePlayer(key) + } + + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + debugLogger.logRecommendationRemoved(key, immediately) + if (immediately || isReorderingAllowed) { + removePlayer(key) + if (!immediately) { + // Although it wasn't requested, we were able to process the removal + // immediately since reordering is allowed. So, notify hosts to update + if (this@MediaCarouselController::updateHostVisibility.isInitialized) { + updateHostVisibility() + } + } + } else { + keysNeedRemoval.add(key) + } + } + } + ) + mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + // The pageIndicator is not laid out yet when we get the current state update, + // Lets make sure we have the right dimensions + updatePageIndicatorLocation() + } + mediaHostStatesManager.addCallback( + object : MediaHostStatesManager.Callback { + override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) { + if (location == desiredLocation) { + onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false) + } + } + } + ) + } + + private fun inflateSettingsButton() { + val settings = + LayoutInflater.from(context) + .inflate(R.layout.media_carousel_settings_button, mediaFrame, false) as View + if (this::settingsButton.isInitialized) { + mediaFrame.removeView(settingsButton) + } + settingsButton = settings + mediaFrame.addView(settingsButton) + mediaCarouselScrollHandler.onSettingsButtonUpdated(settings) + settingsButton.setOnClickListener { + logger.logCarouselSettings() + activityStarter.startActivity(settingsIntent, true /* dismissShade */) + } + } + + private fun inflateMediaCarousel(): ViewGroup { + val mediaCarousel = + LayoutInflater.from(context) + .inflate(R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup + // Because this is inflated when not attached to the true view hierarchy, it resolves some + // potential issues to force that the layout direction is defined by the locale + // (rather than inherited from the parent, which would resolve to LTR when unattached). + mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE + return mediaCarousel + } + + private fun reorderAllPlayers( + previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?, + key: String? = null + ) { + mediaContent.removeAllViews() + for (mediaPlayer in MediaPlayerData.players()) { + mediaPlayer.mediaViewHolder?.let { mediaContent.addView(it.player) } + ?: mediaPlayer.recommendationViewHolder?.let { + mediaContent.addView(it.recommendations) + } + } + mediaCarouselScrollHandler.onPlayersChanged() + MediaPlayerData.updateVisibleMediaPlayers() + // Automatically scroll to the active player if needed + if (shouldScrollToKey) { + shouldScrollToKey = false + val mediaIndex = key?.let { MediaPlayerData.getMediaPlayerIndex(it) } ?: -1 + if (mediaIndex != -1) { + previousVisiblePlayerKey?.let { + val previousVisibleIndex = + MediaPlayerData.playerKeys().indexOfFirst { key -> it == key } + mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex) + } + ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex) + } + } + } + + // Returns true if new player is added + private fun addOrUpdatePlayer( + key: String, + oldKey: String?, + data: MediaData, + isSsReactivated: Boolean + ): Boolean = + traceSection("MediaCarouselController#addOrUpdatePlayer") { + MediaPlayerData.moveIfExists(oldKey, key) + val existingPlayer = MediaPlayerData.getMediaPlayer(key) + val curVisibleMediaKey = + MediaPlayerData.visiblePlayerKeys() + .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) + if (existingPlayer == null) { + val newPlayer = mediaControlPanelFactory.get() + newPlayer.attachPlayer( + MediaViewHolder.create(LayoutInflater.from(context), mediaContent) + ) + newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions + val lp = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + newPlayer.mediaViewHolder?.player?.setLayoutParams(lp) + newPlayer.bindPlayer(data, key) + newPlayer.setListening(currentlyExpanded) + MediaPlayerData.addMediaPlayer( + key, + data, + newPlayer, + systemClock, + isSsReactivated, + debugLogger + ) + updatePlayerToState(newPlayer, noAnimation = true) + // Media data added from a recommendation card should starts playing. + if ( + (shouldScrollToKey && data.isPlaying == true) || + (!shouldScrollToKey && data.active) + ) { + reorderAllPlayers(curVisibleMediaKey, key) + } else { + needsReordering = true + } + } else { + existingPlayer.bindPlayer(data, key) + MediaPlayerData.addMediaPlayer( + key, + data, + existingPlayer, + systemClock, + isSsReactivated, + debugLogger + ) + val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String() + // In case of recommendations hits. + // Check the playing status of media player and the package name. + // To make sure we scroll to the right app's media player. + if ( + isReorderingAllowed || + shouldScrollToKey && + data.isPlaying == true && + packageName == data.packageName + ) { + reorderAllPlayers(curVisibleMediaKey, key) + } else { + needsReordering = true + } + } + updatePageIndicator() + mediaCarouselScrollHandler.onPlayersChanged() + mediaFrame.requiresRemeasuring = true + // Check postcondition: mediaContent should have the same number of children as there + // are + // elements in mediaPlayers. + if (MediaPlayerData.players().size != mediaContent.childCount) { + Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") + } + return existingPlayer == null + } + + private fun addSmartspaceMediaRecommendations( + key: String, + data: SmartspaceMediaData, + shouldPrioritize: Boolean + ) = + traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") { + if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel") + if (MediaPlayerData.getMediaPlayer(key) != null) { + Log.w(TAG, "Skip adding smartspace target in carousel") + return + } + + val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey() + existingSmartspaceMediaKey?.let { + val removedPlayer = + MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey, true) + removedPlayer?.run { + debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey) + } + } + + val newRecs = mediaControlPanelFactory.get() + newRecs.attachRecommendation( + RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent) + ) + newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions + val lp = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp) + newRecs.bindRecommendation(data) + val curVisibleMediaKey = + MediaPlayerData.visiblePlayerKeys() + .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) + MediaPlayerData.addMediaRecommendation( + key, + data, + newRecs, + shouldPrioritize, + systemClock, + debugLogger + ) + updatePlayerToState(newRecs, noAnimation = true) + reorderAllPlayers(curVisibleMediaKey) + updatePageIndicator() + mediaFrame.requiresRemeasuring = true + // Check postcondition: mediaContent should have the same number of children as there + // are + // elements in mediaPlayers. + if (MediaPlayerData.players().size != mediaContent.childCount) { + Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") + } + } + + fun removePlayer( + key: String, + dismissMediaData: Boolean = true, + dismissRecommendation: Boolean = true + ) { + if (key == MediaPlayerData.smartspaceMediaKey()) { + MediaPlayerData.smartspaceMediaData?.let { + logger.logRecommendationRemoved(it.packageName, it.instanceId) + } + } + val removed = + MediaPlayerData.removeMediaPlayer(key, dismissMediaData || dismissRecommendation) + removed?.apply { + mediaCarouselScrollHandler.onPrePlayerRemoved(removed) + mediaContent.removeView(removed.mediaViewHolder?.player) + mediaContent.removeView(removed.recommendationViewHolder?.recommendations) + removed.onDestroy() + mediaCarouselScrollHandler.onPlayersChanged() + updatePageIndicator() + + if (dismissMediaData) { + // Inform the media manager of a potentially late dismissal + mediaManager.dismissMediaData(key, delay = 0L) + } + if (dismissRecommendation) { + // Inform the media manager of a potentially late dismissal + mediaManager.dismissSmartspaceRecommendation(key, delay = 0L) + } + } + } + + private fun updatePlayers(recreateMedia: Boolean) { + pageIndicator.tintList = + ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) + + MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) -> + if (isSsMediaRec) { + val smartspaceMediaData = MediaPlayerData.smartspaceMediaData + removePlayer(key, dismissMediaData = false, dismissRecommendation = false) + smartspaceMediaData?.let { + addSmartspaceMediaRecommendations( + it.targetId, + it, + MediaPlayerData.shouldPrioritizeSs + ) + } + } else { + val isSsReactivated = MediaPlayerData.isSsReactivated(key) + if (recreateMedia) { + removePlayer(key, dismissMediaData = false, dismissRecommendation = false) + } + addOrUpdatePlayer( + key = key, + oldKey = null, + data = data, + isSsReactivated = isSsReactivated + ) + } + } + } + + private fun updatePageIndicator() { + val numPages = mediaContent.getChildCount() + pageIndicator.setNumPages(numPages) + if (numPages == 1) { + pageIndicator.setLocation(0f) + } + updatePageIndicatorAlpha() + } + + /** + * Set a new interpolated state for all players. This is a state that is usually controlled by a + * finger movement where the user drags from one state to the next. + * + * @param startLocation the start location of our state or -1 if this is directly set + * @param endLocation the ending location of our state. + * @param progress the progress of the transition between startLocation and endlocation. If + * ``` + * this is not a guided transformation, this will be 1.0f + * @param immediately + * ``` + * should this state be applied immediately, canceling all animations? + */ + fun setCurrentState( + @MediaLocation startLocation: Int, + @MediaLocation endLocation: Int, + progress: Float, + immediately: Boolean + ) { + if ( + startLocation != currentStartLocation || + endLocation != currentEndLocation || + progress != currentTransitionProgress || + immediately + ) { + currentStartLocation = startLocation + currentEndLocation = endLocation + currentTransitionProgress = progress + for (mediaPlayer in MediaPlayerData.players()) { + updatePlayerToState(mediaPlayer, immediately) + } + maybeResetSettingsCog() + updatePageIndicatorAlpha() + } + } + + @VisibleForTesting + fun updatePageIndicatorAlpha() { + val hostStates = mediaHostStatesManager.mediaHostStates + val endIsVisible = hostStates[currentEndLocation]?.visible ?: false + val startIsVisible = hostStates[currentStartLocation]?.visible ?: false + val startAlpha = if (startIsVisible) 1.0f else 0.0f + // when squishing in split shade, only use endState, which keeps changing + // to provide squishFraction + val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F + val endAlpha = + (if (endIsVisible) 1.0f else 0.0f) * + calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION) + var alpha = 1.0f + if (!endIsVisible || !startIsVisible) { + var progress = currentTransitionProgress + if (!endIsVisible) { + progress = 1.0f - progress + } + // Let's fade in quickly at the end where the view is visible + progress = + MathUtils.constrain(MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress), 0.0f, 1.0f) + alpha = MathUtils.lerp(startAlpha, endAlpha, progress) + } + pageIndicator.alpha = alpha + } + + private fun updatePageIndicatorLocation() { + // Update the location of the page indicator, carousel clipping + val translationX = + if (isRtl) { + (pageIndicator.width - currentCarouselWidth) / 2.0f + } else { + (currentCarouselWidth - pageIndicator.width) / 2.0f + } + pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation + val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams + pageIndicator.translationY = + (currentCarouselHeight - pageIndicator.height - layoutParams.bottomMargin).toFloat() + } + + /** Update the dimension of this carousel. */ + private fun updateCarouselDimensions() { + var width = 0 + var height = 0 + for (mediaPlayer in MediaPlayerData.players()) { + val controller = mediaPlayer.mediaViewController + // When transitioning the view to gone, the view gets smaller, but the translation + // Doesn't, let's add the translation + width = Math.max(width, controller.currentWidth + controller.translationX.toInt()) + height = Math.max(height, controller.currentHeight + controller.translationY.toInt()) + } + if (width != currentCarouselWidth || height != currentCarouselHeight) { + currentCarouselWidth = width + currentCarouselHeight = height + mediaCarouselScrollHandler.setCarouselBounds( + currentCarouselWidth, + currentCarouselHeight + ) + updatePageIndicatorLocation() + updatePageIndicatorAlpha() + } + } + + private fun maybeResetSettingsCog() { + val hostStates = mediaHostStatesManager.mediaHostStates + val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia ?: true + val startShowsActive = + hostStates[currentStartLocation]?.showsOnlyActiveMedia ?: endShowsActive + if ( + currentlyShowingOnlyActive != endShowsActive || + ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) && + startShowsActive != endShowsActive) + ) { + // Whenever we're transitioning from between differing states or the endstate differs + // we reset the translation + currentlyShowingOnlyActive = endShowsActive + mediaCarouselScrollHandler.resetTranslation(animate = true) + } + } + + private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) { + mediaPlayer.mediaViewController.setCurrentState( + startLocation = currentStartLocation, + endLocation = currentEndLocation, + transitionProgress = currentTransitionProgress, + applyImmediately = noAnimation + ) + } + + /** + * The desired location of this view has changed. We should remeasure the view to match the new + * bounds and kick off bounds animations if necessary. If an animation is happening, an + * animation is kicked of externally, which sets a new current state until we reach the + * targetState. + * + * @param desiredLocation the location we're going to + * @param desiredHostState the target state we're transitioning to + * @param animate should this be animated + */ + fun onDesiredLocationChanged( + desiredLocation: Int, + desiredHostState: MediaHostState?, + animate: Boolean, + duration: Long = 200, + startDelay: Long = 0 + ) = + traceSection("MediaCarouselController#onDesiredLocationChanged") { + desiredHostState?.let { + if (this.desiredLocation != desiredLocation) { + // Only log an event when location changes + logger.logCarouselPosition(desiredLocation) + } + + // This is a hosting view, let's remeasure our players + this.desiredLocation = desiredLocation + this.desiredHostState = it + currentlyExpanded = it.expansion > 0 + + val shouldCloseGuts = + !currentlyExpanded && + !mediaManager.hasActiveMediaOrRecommendation() && + desiredHostState.showsOnlyActiveMedia + + for (mediaPlayer in MediaPlayerData.players()) { + if (animate) { + mediaPlayer.mediaViewController.animatePendingStateChange( + duration = duration, + delay = startDelay + ) + } + if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) { + mediaPlayer.closeGuts(!animate) + } + + mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation) + } + mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia + mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded + val nowVisible = it.visible + if (nowVisible != playersVisible) { + playersVisible = nowVisible + if (nowVisible) { + mediaCarouselScrollHandler.resetTranslation() + } + } + updateCarouselSize() + } + } + + fun closeGuts(immediate: Boolean = true) { + MediaPlayerData.players().forEach { it.closeGuts(immediate) } + } + + /** Update the size of the carousel, remeasuring it if necessary. */ + private fun updateCarouselSize() { + val width = desiredHostState?.measurementInput?.width ?: 0 + val height = desiredHostState?.measurementInput?.height ?: 0 + if ( + width != carouselMeasureWidth && width != 0 || + height != carouselMeasureHeight && height != 0 + ) { + carouselMeasureWidth = width + carouselMeasureHeight = height + val playerWidthPlusPadding = + carouselMeasureWidth + + context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) + // Let's remeasure the carousel + val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0 + val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0 + mediaCarousel.measure(widthSpec, heightSpec) + mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight) + // Update the padding after layout; view widths are used in RTL to calculate scrollX + mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding + } + } + + /** Log the user impression for media card at visibleMediaIndex. */ + fun logSmartspaceImpression(qsExpanded: Boolean) { + val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex + if (MediaPlayerData.players().size > visibleMediaIndex) { + val mediaControlPanel = MediaPlayerData.getMediaControlPanel(visibleMediaIndex) + val hasActiveMediaOrRecommendationCard = + MediaPlayerData.hasActiveMediaOrRecommendationCard() + if (!hasActiveMediaOrRecommendationCard && !qsExpanded) { + // Skip logging if on LS or QQS, and there is no active media card + return + } + mediaControlPanel?.let { + logSmartspaceCardReported( + 800, // SMARTSPACE_CARD_SEEN + it.mSmartspaceId, + it.mUid, + intArrayOf(it.surfaceForSmartspaceLogging) + ) + it.mIsImpressed = true + } + } + } + + @JvmOverloads + /** + * Log Smartspace events + * + * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN) + * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new + * instanceId + * @param uid uid for the application that media comes from + * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when + * the event happened + * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1 + * for tapping on card but not on any media item, 0 for first media item, 1 for second, etc. + * @param interactedSubcardCardinality how many media items were shown to the user when there is + * user interaction + * @param rank the rank for media card in the media carousel, starting from 0 + * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency + * between headphone connection to sysUI displays media recommendation card + * @param isSwipeToDismiss whether is to log swipe-to-dismiss event + */ + fun logSmartspaceCardReported( + eventId: Int, + instanceId: Int, + uid: Int, + surfaces: IntArray, + interactedSubcardRank: Int = 0, + interactedSubcardCardinality: Int = 0, + rank: Int = mediaCarouselScrollHandler.visibleMediaIndex, + receivedLatencyMillis: Int = 0, + isSwipeToDismiss: Boolean = false + ) { + if (MediaPlayerData.players().size <= rank) { + return + } + + val mediaControlKey = MediaPlayerData.visiblePlayerKeys().elementAt(rank) + // Only log media resume card when Smartspace data is available + if ( + !mediaControlKey.isSsMediaRec && + !mediaManager.smartspaceMediaData.isActive && + MediaPlayerData.smartspaceMediaData == null + ) { + return + } + + val cardinality = mediaContent.getChildCount() + surfaces.forEach { surface -> + /* ktlint-disable max-line-length */ + SysUiStatsLog.write( + SysUiStatsLog.SMARTSPACE_CARD_REPORTED, + eventId, + instanceId, + // Deprecated, replaced with AiAi feature type so we don't need to create logging + // card type for each new feature. + SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD, + surface, + // Use -1 as rank value to indicate user swipe to dismiss the card + if (isSwipeToDismiss) -1 else rank, + cardinality, + if (mediaControlKey.isSsMediaRec) 15 // MEDIA_RECOMMENDATION + else if (mediaControlKey.isSsReactivated) 43 // MEDIA_RESUME_SS_ACTIVATED + else 31, // MEDIA_RESUME + uid, + interactedSubcardRank, + interactedSubcardCardinality, + receivedLatencyMillis, + null, // Media cards cannot have subcards. + null // Media cards don't have dimensions today. + ) + /* ktlint-disable max-line-length */ + if (DEBUG) { + Log.d( + TAG, + "Log Smartspace card event id: $eventId instance id: $instanceId" + + " surface: $surface rank: $rank cardinality: $cardinality " + + "isRecommendationCard: ${mediaControlKey.isSsMediaRec} " + + "isSsReactivated: ${mediaControlKey.isSsReactivated}" + + "uid: $uid " + + "interactedSubcardRank: $interactedSubcardRank " + + "interactedSubcardCardinality: $interactedSubcardCardinality " + + "received_latency_millis: $receivedLatencyMillis" + ) + } + } + } + + private fun onSwipeToDismiss() { + MediaPlayerData.players().forEachIndexed { index, it -> + if (it.mIsImpressed) { + logSmartspaceCardReported( + SMARTSPACE_CARD_DISMISS_EVENT, + it.mSmartspaceId, + it.mUid, + intArrayOf(it.surfaceForSmartspaceLogging), + rank = index, + isSwipeToDismiss = true + ) + // Reset card impressed state when swipe to dismissed + it.mIsImpressed = false + } + } + logger.logSwipeDismiss() + mediaManager.onSwipeToDismiss() + } + + fun getCurrentVisibleMediaContentIntent(): PendingIntent? { + return MediaPlayerData.playerKeys() + .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) + ?.data + ?.clickIntent + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.apply { + println("keysNeedRemoval: $keysNeedRemoval") + println("dataKeys: ${MediaPlayerData.dataKeys()}") + println("orderedPlayerSortKeys: ${MediaPlayerData.playerKeys()}") + println("visiblePlayerSortKeys: ${MediaPlayerData.visiblePlayerKeys()}") + println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}") + println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}") + println("current size: $currentCarouselWidth x $currentCarouselHeight") + println("location: $desiredLocation") + println( + "state: ${desiredHostState?.expansion}, " + + "only active ${desiredHostState?.showsOnlyActiveMedia}" + ) + } + } +} + +@VisibleForTesting +internal object MediaPlayerData { + private val EMPTY = + MediaData( + userId = -1, + initialized = false, + app = null, + appIcon = null, + artist = null, + song = null, + artwork = null, + actions = emptyList(), + actionsToShowInCompact = emptyList(), + packageName = "INVALID", + token = null, + clickIntent = null, + device = null, + active = true, + resumeAction = null, + instanceId = InstanceId.fakeInstanceId(-1), + appUid = -1 + ) + // Whether should prioritize Smartspace card. + internal var shouldPrioritizeSs: Boolean = false + private set + internal var smartspaceMediaData: SmartspaceMediaData? = null + private set + + data class MediaSortKey( + val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation. + val data: MediaData, + val key: String, + val updateTime: Long = 0, + val isSsReactivated: Boolean = false + ) + + private val comparator = + compareByDescending<MediaSortKey> { + it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL + } + .thenByDescending { + it.data.isPlaying == true && + it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL + } + .thenByDescending { it.data.active } + .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec } + .thenByDescending { !it.data.resumption } + .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE } + .thenByDescending { it.data.lastActive } + .thenByDescending { it.updateTime } + .thenByDescending { it.data.notificationKey } + + private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) + private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() + // A map that tracks order of visible media players before they get reordered. + private val visibleMediaPlayers = LinkedHashMap<String, MediaSortKey>() + + fun addMediaPlayer( + key: String, + data: MediaData, + player: MediaControlPanel, + clock: SystemClock, + isSsReactivated: Boolean, + debugLogger: MediaCarouselControllerLogger? = null + ) { + val removedPlayer = removeMediaPlayer(key) + if (removedPlayer != null && removedPlayer != player) { + debugLogger?.logPotentialMemoryLeak(key) + } + val sortKey = + MediaSortKey( + isSsMediaRec = false, + data, + key, + clock.currentTimeMillis(), + isSsReactivated = isSsReactivated + ) + mediaData.put(key, sortKey) + mediaPlayers.put(sortKey, player) + visibleMediaPlayers.put(key, sortKey) + } + + fun addMediaRecommendation( + key: String, + data: SmartspaceMediaData, + player: MediaControlPanel, + shouldPrioritize: Boolean, + clock: SystemClock, + debugLogger: MediaCarouselControllerLogger? = null + ) { + shouldPrioritizeSs = shouldPrioritize + val removedPlayer = removeMediaPlayer(key) + if (removedPlayer != null && removedPlayer != player) { + debugLogger?.logPotentialMemoryLeak(key) + } + val sortKey = + MediaSortKey( + isSsMediaRec = true, + EMPTY.copy(isPlaying = false), + key, + clock.currentTimeMillis(), + isSsReactivated = true + ) + mediaData.put(key, sortKey) + mediaPlayers.put(sortKey, player) + visibleMediaPlayers.put(key, sortKey) + smartspaceMediaData = data + } + + fun moveIfExists( + oldKey: String?, + newKey: String, + debugLogger: MediaCarouselControllerLogger? = null + ) { + if (oldKey == null || oldKey == newKey) { + return + } + + mediaData.remove(oldKey)?.let { + // MediaPlayer should not be visible + // no need to set isDismissed flag. + val removedPlayer = removeMediaPlayer(newKey) + removedPlayer?.run { debugLogger?.logPotentialMemoryLeak(newKey) } + mediaData.put(newKey, it) + } + } + + fun getMediaControlPanel(visibleIndex: Int): MediaControlPanel? { + return mediaPlayers.get(visiblePlayerKeys().elementAt(visibleIndex)) + } + + fun getMediaPlayer(key: String): MediaControlPanel? { + return mediaData.get(key)?.let { mediaPlayers.get(it) } + } + + fun getMediaPlayerIndex(key: String): Int { + val sortKey = mediaData.get(key) + mediaPlayers.entries.forEachIndexed { index, e -> + if (e.key == sortKey) { + return index + } + } + return -1 + } + + /** + * Removes media player given the key. + * @param isDismissed determines whether the media player is removed from the carousel. + */ + fun removeMediaPlayer(key: String, isDismissed: Boolean = false) = + mediaData.remove(key)?.let { + if (it.isSsMediaRec) { + smartspaceMediaData = null + } + if (isDismissed) { + visibleMediaPlayers.remove(key) + } + mediaPlayers.remove(it) + } + + fun mediaData() = + mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) } + + fun dataKeys() = mediaData.keys + + fun players() = mediaPlayers.values + + fun playerKeys() = mediaPlayers.keys + + fun visiblePlayerKeys() = visibleMediaPlayers.values + + /** Returns the index of the first non-timeout media. */ + fun firstActiveMediaIndex(): Int { + mediaPlayers.entries.forEachIndexed { index, e -> + if (!e.key.isSsMediaRec && e.key.data.active) { + return index + } + } + return -1 + } + + /** Returns the existing Smartspace target id. */ + fun smartspaceMediaKey(): String? { + mediaData.entries.forEach { e -> + if (e.value.isSsMediaRec) { + return e.key + } + } + return null + } + + @VisibleForTesting + fun clear() { + mediaData.clear() + mediaPlayers.clear() + visibleMediaPlayers.clear() + } + + /* Returns true if there is active media player card or recommendation card */ + fun hasActiveMediaOrRecommendationCard(): Boolean { + if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) { + return true + } + if (firstActiveMediaIndex() != -1) { + return true + } + return false + } + + fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false + + /** + * This method is called when media players are reordered. To make sure we have the new version + * of the order of media players visible to user. + */ + fun updateVisibleMediaPlayers() { + visibleMediaPlayers.clear() + playerKeys().forEach { visibleMediaPlayers.put(it.key, it) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt index b1018f9544c0..eed1bd743938 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt @@ -14,63 +14,53 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaCarouselControllerLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** A debug logger for [MediaCarouselController]. */ @SysUISingleton -class MediaCarouselControllerLogger @Inject constructor( - @MediaCarouselControllerLog private val buffer: LogBuffer -) { +class MediaCarouselControllerLogger +@Inject +constructor(@MediaCarouselControllerLog private val buffer: LogBuffer) { /** * Log that there might be a potential memory leak for the [MediaControlPanel] and/or * [MediaViewController] related to [key]. */ - fun logPotentialMemoryLeak(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { str1 = key }, - { - "Potential memory leak: " + + fun logPotentialMemoryLeak(key: String) = + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { + "Potential memory leak: " + "Removing control panel for $str1 from map without calling #onDestroy" - } - ) + } + ) - fun logMediaLoaded(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { str1 = key }, - { "add player $str1" } - ) + fun logMediaLoaded(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add player $str1" }) - fun logMediaRemoved(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { str1 = key }, - { "removing player $str1" } - ) + fun logMediaRemoved(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "removing player $str1" }) - fun logRecommendationLoaded(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { str1 = key }, - { "add recommendation $str1" } - ) + fun logRecommendationLoaded(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add recommendation $str1" }) - fun logRecommendationRemoved(key: String, immediately: Boolean) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - bool1 = immediately - }, - { "removing recommendation $str1, immediate=$bool1" } - ) + fun logRecommendationRemoved(key: String, immediately: Boolean) = + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = key + bool1 = immediately + }, + { "removing recommendation $str1, immediate=$bool1" } + ) } private const val TAG = "MediaCarouselCtlrLog" diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt index a776897b2fd5..36b2eda65fab 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.graphics.Outline import android.util.MathUtils @@ -31,6 +31,7 @@ import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PageIndicator import com.android.systemui.util.concurrency.DelayableExecutor @@ -43,16 +44,13 @@ private const val RUBBERBAND_FACTOR = 0.2f private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f /** - * Default spring configuration to use for animations where stiffness and/or damping ratio - * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig]. + * Default spring configuration to use for animations where stiffness and/or damping ratio were not + * provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig]. */ -private val translationConfig = PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_LOW, - SpringForce.DAMPING_RATIO_LOW_BOUNCY) +private val translationConfig = + PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY) -/** - * A controller class for the media scrollview, responsible for touch handling - */ +/** A controller class for the media scrollview, responsible for touch handling */ class MediaCarouselScrollHandler( private val scrollView: MediaScrollView, private val pageIndicator: PageIndicator, @@ -65,57 +63,36 @@ class MediaCarouselScrollHandler( private val logSmartspaceImpression: (Boolean) -> Unit, private val logger: MediaUiEventLogger ) { - /** - * Is the view in RTL - */ - val isRtl: Boolean get() = scrollView.isLayoutRtl - /** - * Do we need falsing protection? - */ + /** Is the view in RTL */ + val isRtl: Boolean + get() = scrollView.isLayoutRtl + /** Do we need falsing protection? */ var falsingProtectionNeeded: Boolean = false - /** - * The width of the carousel - */ + /** The width of the carousel */ private var carouselWidth: Int = 0 - /** - * The height of the carousel - */ + /** The height of the carousel */ private var carouselHeight: Int = 0 - /** - * How much are we scrolled into the current media? - */ + /** How much are we scrolled into the current media? */ private var cornerRadius: Int = 0 - /** - * The content where the players are added - */ + /** The content where the players are added */ private var mediaContent: ViewGroup - /** - * The gesture detector to detect touch gestures - */ + /** The gesture detector to detect touch gestures */ private val gestureDetector: GestureDetectorCompat - /** - * The settings button view - */ + /** The settings button view */ private lateinit var settingsButton: View - /** - * What's the currently visible player index? - */ + /** What's the currently visible player index? */ var visibleMediaIndex: Int = 0 private set - /** - * How much are we scrolled into the current media? - */ + /** How much are we scrolled into the current media? */ private var scrollIntoCurrentMedia: Int = 0 - /** - * how much is the content translated in X - */ + /** how much is the content translated in X */ var contentTranslation = 0.0f private set(value) { field = value @@ -125,9 +102,7 @@ class MediaCarouselScrollHandler( updateClipToOutline() } - /** - * The width of a player including padding - */ + /** The width of a player including padding */ var playerWidthPlusPadding: Int = 0 set(value) { field = value @@ -135,82 +110,75 @@ class MediaCarouselScrollHandler( // it's still at the same place var newRelativeScroll = visibleMediaIndex * playerWidthPlusPadding if (scrollIntoCurrentMedia > playerWidthPlusPadding) { - newRelativeScroll += playerWidthPlusPadding - - (scrollIntoCurrentMedia - playerWidthPlusPadding) + newRelativeScroll += + playerWidthPlusPadding - (scrollIntoCurrentMedia - playerWidthPlusPadding) } else { newRelativeScroll += scrollIntoCurrentMedia } scrollView.relativeScrollX = newRelativeScroll } - /** - * Does the dismiss currently show the setting cog? - */ + /** Does the dismiss currently show the setting cog? */ var showsSettingsButton: Boolean = false - /** - * A utility to detect gestures, used in the touch listener - */ - private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { - override fun onFling( - eStart: MotionEvent?, - eCurrent: MotionEvent?, - vX: Float, - vY: Float - ) = onFling(vX, vY) - - override fun onScroll( - down: MotionEvent?, - lastMotion: MotionEvent?, - distanceX: Float, - distanceY: Float - ) = onScroll(down!!, lastMotion!!, distanceX) - - override fun onDown(e: MotionEvent?): Boolean { - if (falsingProtectionNeeded) { - falsingCollector.onNotificationStartDismissing() + /** A utility to detect gestures, used in the touch listener */ + private val gestureListener = + object : GestureDetector.SimpleOnGestureListener() { + override fun onFling( + eStart: MotionEvent?, + eCurrent: MotionEvent?, + vX: Float, + vY: Float + ) = onFling(vX, vY) + + override fun onScroll( + down: MotionEvent?, + lastMotion: MotionEvent?, + distanceX: Float, + distanceY: Float + ) = onScroll(down!!, lastMotion!!, distanceX) + + override fun onDown(e: MotionEvent?): Boolean { + if (falsingProtectionNeeded) { + falsingCollector.onNotificationStartDismissing() + } + return false } - return false } - } - /** - * The touch listener for the scroll view - */ - private val touchListener = object : Gefingerpoken { - override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!) - override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!) - } + /** The touch listener for the scroll view */ + private val touchListener = + object : Gefingerpoken { + override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!) + override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!) + } - /** - * A listener that is invoked when the scrolling changes to update player visibilities - */ - private val scrollChangedListener = object : View.OnScrollChangeListener { - override fun onScrollChange( - v: View?, - scrollX: Int, - scrollY: Int, - oldScrollX: Int, - oldScrollY: Int - ) { - if (playerWidthPlusPadding == 0) { - return - } + /** A listener that is invoked when the scrolling changes to update player visibilities */ + private val scrollChangedListener = + object : View.OnScrollChangeListener { + override fun onScrollChange( + v: View?, + scrollX: Int, + scrollY: Int, + oldScrollX: Int, + oldScrollY: Int + ) { + if (playerWidthPlusPadding == 0) { + return + } - val relativeScrollX = scrollView.relativeScrollX - onMediaScrollingChanged(relativeScrollX / playerWidthPlusPadding, - relativeScrollX % playerWidthPlusPadding) + val relativeScrollX = scrollView.relativeScrollX + onMediaScrollingChanged( + relativeScrollX / playerWidthPlusPadding, + relativeScrollX % playerWidthPlusPadding + ) + } } - } - /** - * Whether the media card is visible to user if any - */ + /** Whether the media card is visible to user if any */ var visibleToUser: Boolean = false - /** - * Whether the quick setting is expanded or not - */ + /** Whether the quick setting is expanded or not */ var qsExpanded: Boolean = false init { @@ -219,47 +187,61 @@ class MediaCarouselScrollHandler( scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER) mediaContent = scrollView.contentContainer scrollView.setOnScrollChangeListener(scrollChangedListener) - scrollView.outlineProvider = object : ViewOutlineProvider() { - override fun getOutline(view: View?, outline: Outline?) { - outline?.setRoundRect(0, 0, carouselWidth, carouselHeight, cornerRadius.toFloat()) + scrollView.outlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View?, outline: Outline?) { + outline?.setRoundRect( + 0, + 0, + carouselWidth, + carouselHeight, + cornerRadius.toFloat() + ) + } } - } } fun onSettingsButtonUpdated(button: View) { settingsButton = button // We don't have a context to resolve, lets use the settingsbuttons one since that is // reinflated appropriately - cornerRadius = settingsButton.resources.getDimensionPixelSize( - Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius)) + cornerRadius = + settingsButton.resources.getDimensionPixelSize( + Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius) + ) updateSettingsPresentation() scrollView.invalidateOutline() } private fun updateSettingsPresentation() { if (showsSettingsButton && settingsButton.width > 0) { - val settingsOffset = MathUtils.map( + val settingsOffset = + MathUtils.map( 0.0f, getMaxTranslation().toFloat(), 0.0f, 1.0f, - Math.abs(contentTranslation)) - val settingsTranslation = (1.0f - settingsOffset) * -settingsButton.width * + Math.abs(contentTranslation) + ) + val settingsTranslation = + (1.0f - settingsOffset) * + -settingsButton.width * SETTINGS_BUTTON_TRANSLATION_FRACTION - val newTranslationX = if (isRtl) { - // In RTL, the 0-placement is on the right side of the view, not the left... - if (contentTranslation > 0) { - -(scrollView.width - settingsTranslation - settingsButton.width) - } else { - -settingsTranslation - } - } else { - if (contentTranslation > 0) { - settingsTranslation + val newTranslationX = + if (isRtl) { + // In RTL, the 0-placement is on the right side of the view, not the left... + if (contentTranslation > 0) { + -(scrollView.width - settingsTranslation - settingsButton.width) + } else { + -settingsTranslation + } } else { - scrollView.width - settingsTranslation - settingsButton.width + if (contentTranslation > 0) { + settingsTranslation + } else { + scrollView.width - settingsTranslation - settingsButton.width + } } - } val rotation = (1.0f - settingsOffset) * 50 settingsButton.rotation = rotation * -Math.signum(contentTranslation) val alpha = MathUtils.saturate(MathUtils.map(0.5f, 1.0f, 0.0f, 1.0f, settingsOffset)) @@ -306,16 +288,14 @@ class MediaCarouselScrollHandler( val newScrollX = scrollView.relativeScrollX + dx // Delay the scrolling since scrollView calls springback which cancels // the animation again.. - mainExecutor.execute { - scrollView.smoothScrollTo(newScrollX, scrollView.scrollY) - } + mainExecutor.execute { scrollView.smoothScrollTo(newScrollX, scrollView.scrollY) } } val currentTranslation = scrollView.getContentTranslation() if (currentTranslation != 0.0f) { // We started a Swipe but didn't end up with a fling. Let's either go to the // dismissed position or go back. - val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2 || - isFalseTouch() + val springBack = + Math.abs(currentTranslation) < getMaxTranslation() / 2 || isFalseTouch() val newTranslation: Float if (springBack) { newTranslation = 0.0f @@ -324,13 +304,17 @@ class MediaCarouselScrollHandler( if (!showsSettingsButton) { // Delay the dismiss a bit to avoid too much overlap. Waiting until the // animation has finished also feels a bit too slow here. - mainExecutor.executeDelayed({ - dismissCallback.invoke() - }, DISMISS_DELAY) + mainExecutor.executeDelayed({ dismissCallback.invoke() }, DISMISS_DELAY) } } - PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION, - newTranslation, startVelocity = 0.0f, config = translationConfig).start() + PhysicsAnimator.getInstance(this) + .spring( + CONTENT_TRANSLATION, + newTranslation, + startVelocity = 0.0f, + config = translationConfig + ) + .start() scrollView.animationTargetX = newTranslation } } @@ -338,10 +322,11 @@ class MediaCarouselScrollHandler( return false } - private fun isFalseTouch() = falsingProtectionNeeded && - falsingManager.isFalseTouch(NOTIFICATION_DISMISS) + private fun isFalseTouch() = + falsingProtectionNeeded && falsingManager.isFalseTouch(NOTIFICATION_DISMISS) - private fun getMaxTranslation() = if (showsSettingsButton) { + private fun getMaxTranslation() = + if (showsSettingsButton) { settingsButton.width } else { playerWidthPlusPadding @@ -351,15 +336,10 @@ class MediaCarouselScrollHandler( return gestureDetector.onTouchEvent(motionEvent) } - fun onScroll( - down: MotionEvent, - lastMotion: MotionEvent, - distanceX: Float - ): Boolean { + fun onScroll(down: MotionEvent, lastMotion: MotionEvent, distanceX: Float): Boolean { val totalX = lastMotion.x - down.x val currentTranslation = scrollView.getContentTranslation() - if (currentTranslation != 0.0f || - !scrollView.canScrollHorizontally((-totalX).toInt())) { + if (currentTranslation != 0.0f || !scrollView.canScrollHorizontally((-totalX).toInt())) { var newTranslation = currentTranslation - distanceX val absTranslation = Math.abs(newTranslation) if (absTranslation > getMaxTranslation()) { @@ -373,14 +353,18 @@ class MediaCarouselScrollHandler( newTranslation = currentTranslation - distanceX * RUBBERBAND_FACTOR } else { // We just crossed the boundary, let's rubberband it all - newTranslation = Math.signum(newTranslation) * (getMaxTranslation() + - (absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR) + newTranslation = + Math.signum(newTranslation) * + (getMaxTranslation() + + (absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR) } } // Otherwise we don't have do do anything, and will remove the unrubberbanded // translation } - if (Math.signum(newTranslation) != Math.signum(currentTranslation) && - currentTranslation != 0.0f) { + if ( + Math.signum(newTranslation) != Math.signum(currentTranslation) && + currentTranslation != 0.0f + ) { // We crossed the 0.0 threshold of the translation. Let's see if we're allowed // to scroll into the new direction if (scrollView.canScrollHorizontally(-newTranslation.toInt())) { @@ -391,8 +375,14 @@ class MediaCarouselScrollHandler( } val physicsAnimator = PhysicsAnimator.getInstance(this) if (physicsAnimator.isRunning()) { - physicsAnimator.spring(CONTENT_TRANSLATION, - newTranslation, startVelocity = 0.0f, config = translationConfig).start() + physicsAnimator + .spring( + CONTENT_TRANSLATION, + newTranslation, + startVelocity = 0.0f, + config = translationConfig + ) + .start() } else { contentTranslation = newTranslation } @@ -402,10 +392,7 @@ class MediaCarouselScrollHandler( return false } - private fun onFling( - vX: Float, - vY: Float - ): Boolean { + private fun onFling(vX: Float, vY: Float): Boolean { if (vX * vX < 0.5 * vY * vY) { return false } @@ -424,13 +411,17 @@ class MediaCarouselScrollHandler( // Delay the dismiss a bit to avoid too much overlap. Waiting until the animation // has finished also feels a bit too slow here. if (!showsSettingsButton) { - mainExecutor.executeDelayed({ - dismissCallback.invoke() - }, DISMISS_DELAY) + mainExecutor.executeDelayed({ dismissCallback.invoke() }, DISMISS_DELAY) } } - PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION, - newTranslation, startVelocity = vX, config = translationConfig).start() + PhysicsAnimator.getInstance(this) + .spring( + CONTENT_TRANSLATION, + newTranslation, + startVelocity = vX, + config = translationConfig + ) + .start() scrollView.animationTargetX = newTranslation } else { // We're flinging the player! Let's go either to the previous or to the next player @@ -443,21 +434,18 @@ class MediaCarouselScrollHandler( val view = mediaContent.getChildAt(destIndex) // We need to post this since we're dispatching a touch to the underlying view to cancel // but canceling will actually abort the animation. - mainExecutor.execute { - scrollView.smoothScrollTo(view.left, scrollView.scrollY) - } + mainExecutor.execute { scrollView.smoothScrollTo(view.left, scrollView.scrollY) } } return true } - /** - * Reset the translation of the players when swiped - */ + /** Reset the translation of the players when swiped */ fun resetTranslation(animate: Boolean = false) { if (scrollView.getContentTranslation() != 0.0f) { if (animate) { - PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION, - 0.0f, config = translationConfig).start() + PhysicsAnimator.getInstance(this) + .spring(CONTENT_TRANSLATION, 0.0f, config = translationConfig) + .start() scrollView.animationTargetX = 0.0f } else { PhysicsAnimator.getInstance(this).cancel() @@ -485,21 +473,22 @@ class MediaCarouselScrollHandler( closeGuts(false) updatePlayerVisibilities() } - val relativeLocation = visibleMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) - scrollInAmount.toFloat() / playerWidthPlusPadding else 0f + val relativeLocation = + visibleMediaIndex.toFloat() + + if (playerWidthPlusPadding > 0) scrollInAmount.toFloat() / playerWidthPlusPadding + else 0f // Fix the location, because PageIndicator does not handle RTL internally - val location = if (isRtl) { - mediaContent.childCount - relativeLocation - 1 - } else { - relativeLocation - } + val location = + if (isRtl) { + mediaContent.childCount - relativeLocation - 1 + } else { + relativeLocation + } pageIndicator.setLocation(location) updateClipToOutline() } - /** - * Notified whenever the players or their order has changed - */ + /** Notified whenever the players or their order has changed */ fun onPlayersChanged() { updatePlayerVisibilities() updateMediaPaddings() @@ -529,8 +518,8 @@ class MediaCarouselScrollHandler( } /** - * Notify that a player will be removed right away. This gives us the opporunity to look - * where it was and update our scroll position. + * Notify that a player will be removed right away. This gives us the opporunity to look where + * it was and update our scroll position. */ fun onPrePlayerRemoved(removed: MediaControlPanel) { val removedIndex = mediaContent.indexOfChild(removed.mediaViewHolder?.player) @@ -550,9 +539,7 @@ class MediaCarouselScrollHandler( } } - /** - * Update the bounds of the carousel - */ + /** Update the bounds of the carousel */ fun setCarouselBounds(currentCarouselWidth: Int, currentCarouselHeight: Int) { if (currentCarouselHeight != carouselHeight || currentCarouselWidth != carouselHeight) { carouselWidth = currentCarouselWidth @@ -561,9 +548,7 @@ class MediaCarouselScrollHandler( } } - /** - * Reset the MediaScrollView to the start. - */ + /** Reset the MediaScrollView to the start. */ fun scrollToStart() { scrollView.relativeScrollX = 0 } @@ -581,21 +566,22 @@ class MediaCarouselScrollHandler( val destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex) val view = mediaContent.getChildAt(destIndex) // We need to post this to wait for the active player becomes visible. - mainExecutor.executeDelayed({ - scrollView.smoothScrollTo(view.left, scrollView.scrollY) - }, SCROLL_DELAY) + mainExecutor.executeDelayed( + { scrollView.smoothScrollTo(view.left, scrollView.scrollY) }, + SCROLL_DELAY + ) } companion object { - private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>( - "contentTranslation") { - override fun getValue(handler: MediaCarouselScrollHandler): Float { - return handler.contentTranslation - } + private val CONTENT_TRANSLATION = + object : FloatPropertyCompat<MediaCarouselScrollHandler>("contentTranslation") { + override fun getValue(handler: MediaCarouselScrollHandler): Float { + return handler.contentTranslation + } - override fun setValue(handler: MediaCarouselScrollHandler, value: Float) { - handler.contentTranslation = value + override fun setValue(handler: MediaCarouselScrollHandler, value: Float) { + handler.contentTranslation = value + } } - } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaColorSchemes.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt index 208766d7f5e6..82abf9bfcc80 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaColorSchemes.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import com.android.systemui.monet.ColorScheme diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index 759795f84963..18ecadb28cf3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.ui; import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS; -import static com.android.systemui.media.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS; +import static com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS; import android.animation.Animator; import android.animation.AnimatorInflater; @@ -76,6 +76,20 @@ import com.android.systemui.bluetooth.BroadcastDialogController; import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.media.controls.models.GutsViewHolder; +import com.android.systemui.media.controls.models.player.MediaAction; +import com.android.systemui.media.controls.models.player.MediaButton; +import com.android.systemui.media.controls.models.player.MediaData; +import com.android.systemui.media.controls.models.player.MediaDeviceData; +import com.android.systemui.media.controls.models.player.MediaViewHolder; +import com.android.systemui.media.controls.models.player.SeekBarObserver; +import com.android.systemui.media.controls.models.player.SeekBarViewModel; +import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder; +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData; +import com.android.systemui.media.controls.pipeline.MediaDataManager; +import com.android.systemui.media.controls.util.MediaDataUtils; +import com.android.systemui.media.controls.util.MediaUiEventLogger; +import com.android.systemui.media.controls.util.SmallHash; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.monet.ColorScheme; import com.android.systemui.monet.Style; @@ -111,7 +125,6 @@ public class MediaControlPanel { "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT"; private static final String KEY_SMARTSPACE_ARTIST_NAME = "artist_name"; private static final String KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND"; - protected static final String KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME"; // Event types logged by smartspace private static final int SMARTSPACE_CARD_CLICK_EVENT = 760; @@ -251,6 +264,9 @@ public class MediaControlPanel { }); } + /** + * Clean up seekbar and controller when panel is destroyed + */ public void onDestroy() { if (mSeekBarObserver != null) { mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver); @@ -651,7 +667,7 @@ public class MediaControlPanel { return; } - CharSequence contentDescription; + CharSequence contentDescription; if (mMediaViewController.isGutsVisible()) { contentDescription = mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText(); @@ -1441,7 +1457,7 @@ public class MediaControlPanel { } // Automatically scroll to the active player once the media is loaded. - mMediaCarouselController.setShouldScrollToActivePlayer(true); + mMediaCarouselController.setShouldScrollToKey(true); }); } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index e0b6d1f9de7b..6b46d8f30cad 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -59,9 +59,7 @@ import javax.inject.Inject private val TAG: String = MediaHierarchyManager::class.java.simpleName -/** - * Similarly to isShown but also excludes views that have 0 alpha - */ +/** Similarly to isShown but also excludes views that have 0 alpha */ val View.isShownNotFaded: Boolean get() { var current: View = this @@ -86,7 +84,9 @@ val View.isShownNotFaded: Boolean * and animate the positions of the views to achieve seamless transitions. */ @SysUISingleton -class MediaHierarchyManager @Inject constructor( +class MediaHierarchyManager +@Inject +constructor( private val context: Context, private val statusBarStateController: SysuiStatusBarStateController, private val keyguardStateController: KeyguardStateController, @@ -101,12 +101,10 @@ class MediaHierarchyManager @Inject constructor( @Main private val handler: Handler, ) { - /** - * Track the media player setting status on lock screen. - */ + /** Track the media player setting status on lock screen. */ private var allowMediaPlayerOnLockScreen: Boolean = true private val lockScreenMediaPlayerUri = - secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) + secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) /** * Whether we "skip" QQS during panel expansion. @@ -118,8 +116,8 @@ class MediaHierarchyManager @Inject constructor( /** * The root overlay of the hierarchy. This is where the media notification is attached to - * whenever the view is transitioning from one host to another. It also make sure that the - * view is always in its final state when it is attached to a view host. + * whenever the view is transitioning from one host to another. It also make sure that the view + * is always in its final state when it is attached to a view host. */ private var rootOverlay: ViewGroupOverlay? = null @@ -138,69 +136,68 @@ class MediaHierarchyManager @Inject constructor( */ private var animationStartCrossFadeProgress = 0.0f - /** - * The starting alpha of the animation - */ + /** The starting alpha of the animation */ private var animationStartAlpha = 0.0f - /** - * The starting location of the cross fade if an animation is running right now. - */ - @MediaLocation - private var crossFadeAnimationStartLocation = -1 + /** The starting location of the cross fade if an animation is running right now. */ + @MediaLocation private var crossFadeAnimationStartLocation = -1 - /** - * The end location of the cross fade if an animation is running right now. - */ - @MediaLocation - private var crossFadeAnimationEndLocation = -1 + /** The end location of the cross fade if an animation is running right now. */ + @MediaLocation private var crossFadeAnimationEndLocation = -1 private var targetBounds: Rect = Rect() private val mediaFrame get() = mediaCarouselController.mediaFrame private var statusbarState: Int = statusBarStateController.state - private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { - interpolator = Interpolators.FAST_OUT_SLOW_IN - addUpdateListener { - updateTargetState() - val currentAlpha: Float - var boundsProgress = animatedFraction - if (isCrossFadeAnimatorRunning) { - animationCrossFadeProgress = MathUtils.lerp(animationStartCrossFadeProgress, 1.0f, - animatedFraction) - // When crossfading, let's keep the bounds at the right location during fading - boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f - currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress) - } else { - // If we're not crossfading, let's interpolate from the start alpha to 1.0f - currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction) + private var animator = + ValueAnimator.ofFloat(0.0f, 1.0f).apply { + interpolator = Interpolators.FAST_OUT_SLOW_IN + addUpdateListener { + updateTargetState() + val currentAlpha: Float + var boundsProgress = animatedFraction + if (isCrossFadeAnimatorRunning) { + animationCrossFadeProgress = + MathUtils.lerp(animationStartCrossFadeProgress, 1.0f, animatedFraction) + // When crossfading, let's keep the bounds at the right location during fading + boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f + currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress) + } else { + // If we're not crossfading, let's interpolate from the start alpha to 1.0f + currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction) + } + interpolateBounds( + animationStartBounds, + targetBounds, + boundsProgress, + result = currentBounds + ) + resolveClipping(currentClipping) + applyState(currentBounds, currentAlpha, clipBounds = currentClipping) } - interpolateBounds(animationStartBounds, targetBounds, boundsProgress, - result = currentBounds) - resolveClipping(currentClipping) - applyState(currentBounds, currentAlpha, clipBounds = currentClipping) - } - addListener(object : AnimatorListenerAdapter() { - private var cancelled: Boolean = false + addListener( + object : AnimatorListenerAdapter() { + private var cancelled: Boolean = false + + override fun onAnimationCancel(animation: Animator?) { + cancelled = true + animationPending = false + rootView?.removeCallbacks(startAnimation) + } - override fun onAnimationCancel(animation: Animator?) { - cancelled = true - animationPending = false - rootView?.removeCallbacks(startAnimation) - } + override fun onAnimationEnd(animation: Animator?) { + isCrossFadeAnimatorRunning = false + if (!cancelled) { + applyTargetStateIfNotAnimating() + } + } - override fun onAnimationEnd(animation: Animator?) { - isCrossFadeAnimatorRunning = false - if (!cancelled) { - applyTargetStateIfNotAnimating() + override fun onAnimationStart(animation: Animator?) { + cancelled = false + animationPending = false + } } - } - - override fun onAnimationStart(animation: Animator?) { - cancelled = false - animationPending = false - } - }) - } + ) + } private fun resolveClipping(result: Rect) { if (animationStartClipping.isEmpty) result.set(targetClipping) @@ -210,42 +207,31 @@ class MediaHierarchyManager @Inject constructor( private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_DREAM_OVERLAY + 1) /** - * The last location where this view was at before going to the desired location. This is - * useful for guided transitions. + * The last location where this view was at before going to the desired location. This is useful + * for guided transitions. */ - @MediaLocation - private var previousLocation = -1 - /** - * The desired location where the view will be at the end of the transition. - */ - @MediaLocation - private var desiredLocation = -1 + @MediaLocation private var previousLocation = -1 + /** The desired location where the view will be at the end of the transition. */ + @MediaLocation private var desiredLocation = -1 /** - * The current attachment location where the view is currently attached. - * Usually this matches the desired location except for animations whenever a view moves - * to the new desired location, during which it is in [IN_OVERLAY]. + * The current attachment location where the view is currently attached. Usually this matches + * the desired location except for animations whenever a view moves to the new desired location, + * during which it is in [IN_OVERLAY]. */ - @MediaLocation - private var currentAttachmentLocation = -1 + @MediaLocation private var currentAttachmentLocation = -1 private var inSplitShade = false - /** - * Is there any active media in the carousel? - */ + /** Is there any active media in the carousel? */ private var hasActiveMedia: Boolean = false get() = mediaHosts.get(LOCATION_QQS)?.visible == true - /** - * Are we currently waiting on an animation to start? - */ + /** Are we currently waiting on an animation to start? */ private var animationPending: Boolean = false private val startAnimation: Runnable = Runnable { animator.start() } - /** - * The expansion of quick settings - */ + /** The expansion of quick settings */ var qsExpansion: Float = 0.0f set(value) { if (field != value) { @@ -258,9 +244,7 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Is quick setting expanded? - */ + /** Is quick setting expanded? */ var qsExpanded: Boolean = false set(value) { if (field != value) { @@ -281,9 +265,8 @@ class MediaHierarchyManager @Inject constructor( private var distanceForFullShadeTransition = 0 /** - * The amount of progress we are currently in if we're transitioning to the full shade. - * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full - * shade. + * The amount of progress we are currently in if we're transitioning to the full shade. 0.0f + * means we're not transitioning yet, while 1 means we're all the way in the full shade. */ private var fullShadeTransitionProgress = 0f set(value) { @@ -305,9 +288,7 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Is there currently a cross-fade animation running driven by an animator? - */ + /** Is there currently a cross-fade animation running driven by an animator? */ private var isCrossFadeAnimatorRunning = false /** @@ -316,8 +297,10 @@ class MediaHierarchyManager @Inject constructor( * the transition starts, this will no longer return true. */ private val isTransitioningToFullShade: Boolean - get() = fullShadeTransitionProgress != 0f && !bypassController.bypassEnabled && - statusbarState == StatusBarState.KEYGUARD + get() = + fullShadeTransitionProgress != 0f && + !bypassController.bypassEnabled && + statusbarState == StatusBarState.KEYGUARD /** * Set the amount of pixels we have currently dragged down if we're transitioning to the full @@ -354,17 +337,13 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Are location changes currently blocked? - */ + /** Are location changes currently blocked? */ private val blockLocationChanges: Boolean get() { return goingToSleep || dozeAnimationRunning } - /** - * Are we currently going to sleep - */ + /** Are we currently going to sleep */ private var goingToSleep: Boolean = false set(value) { if (field != value) { @@ -375,9 +354,7 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Are we currently fullyAwake - */ + /** Are we currently fullyAwake */ private var fullyAwake: Boolean = false set(value) { if (field != value) { @@ -388,9 +365,7 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Is the doze animation currently Running - */ + /** Is the doze animation currently Running */ private var dozeAnimationRunning: Boolean = false private set(value) { if (field != value) { @@ -401,9 +376,7 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Is the dream overlay currently active - */ + /** Is the dream overlay currently active */ private var dreamOverlayActive: Boolean = false private set(value) { if (field != value) { @@ -412,9 +385,7 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Is the dream media complication currently active - */ + /** Is the dream media complication currently active */ private var dreamMediaComplicationActive: Boolean = false private set(value) { if (field != value) { @@ -424,16 +395,13 @@ class MediaHierarchyManager @Inject constructor( } /** - * The current cross fade progress. 0.5f means it's just switching - * between the start and the end location and the content is fully faded, while 0.75f means - * that we're halfway faded in again in the target state. - * This is only valid while [isCrossFadeAnimatorRunning] is true. + * The current cross fade progress. 0.5f means it's just switching between the start and the end + * location and the content is fully faded, while 0.75f means that we're halfway faded in again + * in the target state. This is only valid while [isCrossFadeAnimatorRunning] is true. */ private var animationCrossFadeProgress = 1.0f - /** - * The current carousel Alpha. - */ + /** The current carousel Alpha. */ private var carouselAlpha: Float = 1.0f set(value) { if (field == value) { @@ -447,8 +415,8 @@ class MediaHierarchyManager @Inject constructor( * Calculate the alpha of the view when given a cross-fade progress. * * @param crossFadeProgress The current cross fade progress. 0.5f means it's just switching - * between the start and the end location and the content is fully faded, while 0.75f means - * that we're halfway faded in again in the target state. + * between the start and the end location and the content is fully faded, while 0.75f means that + * we're halfway faded in again in the target state. */ private fun calculateAlphaFromCrossFade(crossFadeProgress: Float): Float { if (crossFadeProgress <= 0.5f) { @@ -460,132 +428,152 @@ class MediaHierarchyManager @Inject constructor( init { updateConfiguration() - configurationController.addCallback(object : ConfigurationController.ConfigurationListener { - override fun onConfigChanged(newConfig: Configuration?) { - updateConfiguration() - updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = true) - } - }) - statusBarStateController.addCallback(object : StatusBarStateController.StateListener { - override fun onStatePreChange(oldState: Int, newState: Int) { - // We're updating the location before the state change happens, since we want the - // location of the previous state to still be up to date when the animation starts - statusbarState = newState - updateDesiredLocation() - } - - override fun onStateChanged(newState: Int) { - updateTargetState() - // Enters shade from lock screen - if (newState == StatusBarState.SHADE_LOCKED && isLockScreenShadeVisibleToUser()) { - mediaCarouselController.logSmartspaceImpression(qsExpanded) + configurationController.addCallback( + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration?) { + updateConfiguration() + updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = true) } - mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser() - } - - override fun onDozeAmountChanged(linear: Float, eased: Float) { - dozeAnimationRunning = linear != 0.0f && linear != 1.0f } + ) + statusBarStateController.addCallback( + object : StatusBarStateController.StateListener { + override fun onStatePreChange(oldState: Int, newState: Int) { + // We're updating the location before the state change happens, since we want + // the + // location of the previous state to still be up to date when the animation + // starts + statusbarState = newState + updateDesiredLocation() + } - override fun onDozingChanged(isDozing: Boolean) { - if (!isDozing) { - dozeAnimationRunning = false - // Enters lock screen from screen off - if (isLockScreenVisibleToUser()) { + override fun onStateChanged(newState: Int) { + updateTargetState() + // Enters shade from lock screen + if ( + newState == StatusBarState.SHADE_LOCKED && isLockScreenShadeVisibleToUser() + ) { mediaCarouselController.logSmartspaceImpression(qsExpanded) } - } else { - updateDesiredLocation() - qsExpanded = false - closeGuts() + mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = + isVisibleToUser() } - mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser() - } - override fun onExpandedChanged(isExpanded: Boolean) { - // Enters shade from home screen - if (isHomeScreenShadeVisibleToUser()) { - mediaCarouselController.logSmartspaceImpression(qsExpanded) + override fun onDozeAmountChanged(linear: Float, eased: Float) { + dozeAnimationRunning = linear != 0.0f && linear != 1.0f } - mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser() - } - }) - dreamOverlayStateController.addCallback(object : DreamOverlayStateController.Callback { - override fun onComplicationsChanged() { - dreamMediaComplicationActive = dreamOverlayStateController.complications.any { - it is MediaDreamComplication + override fun onDozingChanged(isDozing: Boolean) { + if (!isDozing) { + dozeAnimationRunning = false + // Enters lock screen from screen off + if (isLockScreenVisibleToUser()) { + mediaCarouselController.logSmartspaceImpression(qsExpanded) + } + } else { + updateDesiredLocation() + qsExpanded = false + closeGuts() + } + mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = + isVisibleToUser() } - } - override fun onStateChanged() { - dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it } + override fun onExpandedChanged(isExpanded: Boolean) { + // Enters shade from home screen + if (isHomeScreenShadeVisibleToUser()) { + mediaCarouselController.logSmartspaceImpression(qsExpanded) + } + mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = + isVisibleToUser() + } } - }) + ) + + dreamOverlayStateController.addCallback( + object : DreamOverlayStateController.Callback { + override fun onComplicationsChanged() { + dreamMediaComplicationActive = + dreamOverlayStateController.complications.any { + it is MediaDreamComplication + } + } - wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer { - override fun onFinishedGoingToSleep() { - goingToSleep = false + override fun onStateChanged() { + dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it } + } } + ) - override fun onStartedGoingToSleep() { - goingToSleep = true - fullyAwake = false - } + wakefulnessLifecycle.addObserver( + object : WakefulnessLifecycle.Observer { + override fun onFinishedGoingToSleep() { + goingToSleep = false + } - override fun onFinishedWakingUp() { - goingToSleep = false - fullyAwake = true - } + override fun onStartedGoingToSleep() { + goingToSleep = true + fullyAwake = false + } + + override fun onFinishedWakingUp() { + goingToSleep = false + fullyAwake = true + } - override fun onStartedWakingUp() { - goingToSleep = false + override fun onStartedWakingUp() { + goingToSleep = false + } } - }) + ) mediaCarouselController.updateUserVisibility = { mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser() } mediaCarouselController.updateHostVisibility = { - mediaHosts.forEach { - it?.updateViewVisibility() - } + mediaHosts.forEach { it?.updateViewVisibility() } } - panelEventsEvents.registerListener(object : NotifPanelEvents.Listener { - override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) { - skipQqsOnExpansion = isExpandImmediateEnabled - updateDesiredLocation() + panelEventsEvents.registerListener( + object : NotifPanelEvents.Listener { + override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) { + skipQqsOnExpansion = isExpandImmediateEnabled + updateDesiredLocation() + } } - }) + ) - val settingsObserver: ContentObserver = object : ContentObserver(handler) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - if (uri == lockScreenMediaPlayerUri) { - allowMediaPlayerOnLockScreen = + val settingsObserver: ContentObserver = + object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + if (uri == lockScreenMediaPlayerUri) { + allowMediaPlayerOnLockScreen = secureSettings.getBoolForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - true, - UserHandle.USER_CURRENT + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + true, + UserHandle.USER_CURRENT ) + } } } - } secureSettings.registerContentObserverForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - settingsObserver, - UserHandle.USER_ALL) + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + settingsObserver, + UserHandle.USER_ALL + ) } private fun updateConfiguration() { - distanceForFullShadeTransition = context.resources.getDimensionPixelSize( - R.dimen.lockscreen_shade_media_transition_distance) + distanceForFullShadeTransition = + context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_media_transition_distance + ) inSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(context.resources) } /** - * Register a media host and create a view can be attached to a view hierarchy - * and where the players will be placed in when the host is the currently desired state. + * Register a media host and create a view can be attached to a view hierarchy and where the + * players will be placed in when the host is the currently desired state. * * @return the hostView associated with this location */ @@ -613,27 +601,26 @@ class MediaHierarchyManager @Inject constructor( return viewHost } - /** - * Close the guts in all players in [MediaCarouselController]. - */ + /** Close the guts in all players in [MediaCarouselController]. */ fun closeGuts() { mediaCarouselController.closeGuts() } private fun createUniqueObjectHost(): UniqueObjectHostView { val viewHost = UniqueObjectHostView(context) - viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { - override fun onViewAttachedToWindow(p0: View?) { - if (rootOverlay == null) { - rootView = viewHost.viewRootImpl.view - rootOverlay = (rootView!!.overlay as ViewGroupOverlay) + viewHost.addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(p0: View?) { + if (rootOverlay == null) { + rootView = viewHost.viewRootImpl.view + rootOverlay = (rootView!!.overlay as ViewGroupOverlay) + } + viewHost.removeOnAttachStateChangeListener(this) } - viewHost.removeOnAttachStateChangeListener(this) - } - override fun onViewDetachedFromWindow(p0: View?) { + override fun onViewDetachedFromWindow(p0: View?) {} } - }) + ) return viewHost } @@ -643,127 +630,141 @@ class MediaHierarchyManager @Inject constructor( * * @param forceNoAnimation optional parameter telling the system not to animate * @param forceStateUpdate optional parameter telling the system to update transition state + * ``` * even if location did not change + * ``` */ private fun updateDesiredLocation( forceNoAnimation: Boolean = false, forceStateUpdate: Boolean = false - ) = traceSection("MediaHierarchyManager#updateDesiredLocation") { - val desiredLocation = calculateLocation() - if (desiredLocation != this.desiredLocation || forceStateUpdate) { - if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) { - // Only update previous location when it actually changes - previousLocation = this.desiredLocation - } else if (forceStateUpdate) { - val onLockscreen = (!bypassController.bypassEnabled && - (statusbarState == StatusBarState.KEYGUARD)) - if (desiredLocation == LOCATION_QS && previousLocation == LOCATION_LOCKSCREEN && - !onLockscreen) { - // If media active state changed and the device is now unlocked, update the - // previous location so we animate between the correct hosts - previousLocation = LOCATION_QQS + ) = + traceSection("MediaHierarchyManager#updateDesiredLocation") { + val desiredLocation = calculateLocation() + if (desiredLocation != this.desiredLocation || forceStateUpdate) { + if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) { + // Only update previous location when it actually changes + previousLocation = this.desiredLocation + } else if (forceStateUpdate) { + val onLockscreen = + (!bypassController.bypassEnabled && + (statusbarState == StatusBarState.KEYGUARD)) + if ( + desiredLocation == LOCATION_QS && + previousLocation == LOCATION_LOCKSCREEN && + !onLockscreen + ) { + // If media active state changed and the device is now unlocked, update the + // previous location so we animate between the correct hosts + previousLocation = LOCATION_QQS + } } + val isNewView = this.desiredLocation == -1 + this.desiredLocation = desiredLocation + // Let's perform a transition + val animate = + !forceNoAnimation && shouldAnimateTransition(desiredLocation, previousLocation) + val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) + val host = getHost(desiredLocation) + val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE + if (!willFade || isCurrentlyInGuidedTransformation() || !animate) { + // if we're fading, we want the desired location / measurement only to change + // once fully faded. This is happening in the host attachment + mediaCarouselController.onDesiredLocationChanged( + desiredLocation, + host, + animate, + animDuration, + delay + ) + } + performTransitionToNewLocation(isNewView, animate) } - val isNewView = this.desiredLocation == -1 - this.desiredLocation = desiredLocation - // Let's perform a transition - val animate = !forceNoAnimation && - shouldAnimateTransition(desiredLocation, previousLocation) - val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) - val host = getHost(desiredLocation) - val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE - if (!willFade || isCurrentlyInGuidedTransformation() || !animate) { - // if we're fading, we want the desired location / measurement only to change - // once fully faded. This is happening in the host attachment - mediaCarouselController.onDesiredLocationChanged(desiredLocation, host, - animate, animDuration, delay) - } - performTransitionToNewLocation(isNewView, animate) } - } - private fun performTransitionToNewLocation( - isNewView: Boolean, - animate: Boolean - ) = traceSection("MediaHierarchyManager#performTransitionToNewLocation") { - if (previousLocation < 0 || isNewView) { - cancelAnimationAndApplyDesiredState() - return - } - val currentHost = getHost(desiredLocation) - val previousHost = getHost(previousLocation) - if (currentHost == null || previousHost == null) { - cancelAnimationAndApplyDesiredState() - return - } - updateTargetState() - if (isCurrentlyInGuidedTransformation()) { - applyTargetStateIfNotAnimating() - } else if (animate) { - val wasCrossFading = isCrossFadeAnimatorRunning - val previewsCrossFadeProgress = animationCrossFadeProgress - animator.cancel() - if (currentAttachmentLocation != previousLocation || - !previousHost.hostView.isAttachedToWindow) { - // Let's animate to the new position, starting from the current position - // We also go in here in case the view was detached, since the bounds wouldn't - // be correct anymore - animationStartBounds.set(currentBounds) - animationStartClipping.set(currentClipping) - } else { - // otherwise, let's take the freshest state, since the current one could - // be outdated - animationStartBounds.set(previousHost.currentBounds) - animationStartClipping.set(previousHost.currentClipping) + private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) = + traceSection("MediaHierarchyManager#performTransitionToNewLocation") { + if (previousLocation < 0 || isNewView) { + cancelAnimationAndApplyDesiredState() + return } - val transformationType = calculateTransformationType() - var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE - var crossFadeStartProgress = 0.0f - // The alpha is only relevant when not cross fading - var newCrossFadeStartLocation = previousLocation - if (wasCrossFading) { - if (currentAttachmentLocation == crossFadeAnimationEndLocation) { - if (needsCrossFade) { - // We were previously crossFading and we've already reached - // the end view, Let's start crossfading from the same position there - crossFadeStartProgress = 1.0f - previewsCrossFadeProgress - } - // Otherwise let's fade in from the current alpha, but not cross fade + val currentHost = getHost(desiredLocation) + val previousHost = getHost(previousLocation) + if (currentHost == null || previousHost == null) { + cancelAnimationAndApplyDesiredState() + return + } + updateTargetState() + if (isCurrentlyInGuidedTransformation()) { + applyTargetStateIfNotAnimating() + } else if (animate) { + val wasCrossFading = isCrossFadeAnimatorRunning + val previewsCrossFadeProgress = animationCrossFadeProgress + animator.cancel() + if ( + currentAttachmentLocation != previousLocation || + !previousHost.hostView.isAttachedToWindow + ) { + // Let's animate to the new position, starting from the current position + // We also go in here in case the view was detached, since the bounds wouldn't + // be correct anymore + animationStartBounds.set(currentBounds) + animationStartClipping.set(currentClipping) } else { - // We haven't reached the previous location yet, let's still cross fade from - // where we were. - newCrossFadeStartLocation = crossFadeAnimationStartLocation - if (newCrossFadeStartLocation == desiredLocation) { - // we're crossFading back to where we were, let's start at the end position - crossFadeStartProgress = 1.0f - previewsCrossFadeProgress + // otherwise, let's take the freshest state, since the current one could + // be outdated + animationStartBounds.set(previousHost.currentBounds) + animationStartClipping.set(previousHost.currentClipping) + } + val transformationType = calculateTransformationType() + var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE + var crossFadeStartProgress = 0.0f + // The alpha is only relevant when not cross fading + var newCrossFadeStartLocation = previousLocation + if (wasCrossFading) { + if (currentAttachmentLocation == crossFadeAnimationEndLocation) { + if (needsCrossFade) { + // We were previously crossFading and we've already reached + // the end view, Let's start crossfading from the same position there + crossFadeStartProgress = 1.0f - previewsCrossFadeProgress + } + // Otherwise let's fade in from the current alpha, but not cross fade } else { - // Let's start from where we are right now - crossFadeStartProgress = previewsCrossFadeProgress - // We need to force cross fading as we haven't reached the end location yet - needsCrossFade = true + // We haven't reached the previous location yet, let's still cross fade from + // where we were. + newCrossFadeStartLocation = crossFadeAnimationStartLocation + if (newCrossFadeStartLocation == desiredLocation) { + // we're crossFading back to where we were, let's start at the end + // position + crossFadeStartProgress = 1.0f - previewsCrossFadeProgress + } else { + // Let's start from where we are right now + crossFadeStartProgress = previewsCrossFadeProgress + // We need to force cross fading as we haven't reached the end location + // yet + needsCrossFade = true + } } + } else if (needsCrossFade) { + // let's not flicker and start with the same alpha + crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f } - } else if (needsCrossFade) { - // let's not flicker and start with the same alpha - crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f - } - isCrossFadeAnimatorRunning = needsCrossFade - crossFadeAnimationStartLocation = newCrossFadeStartLocation - crossFadeAnimationEndLocation = desiredLocation - animationStartAlpha = carouselAlpha - animationStartCrossFadeProgress = crossFadeStartProgress - adjustAnimatorForTransition(desiredLocation, previousLocation) - if (!animationPending) { - rootView?.let { - // Let's delay the animation start until we finished laying out - animationPending = true - it.postOnAnimation(startAnimation) + isCrossFadeAnimatorRunning = needsCrossFade + crossFadeAnimationStartLocation = newCrossFadeStartLocation + crossFadeAnimationEndLocation = desiredLocation + animationStartAlpha = carouselAlpha + animationStartCrossFadeProgress = crossFadeStartProgress + adjustAnimatorForTransition(desiredLocation, previousLocation) + if (!animationPending) { + rootView?.let { + // Let's delay the animation start until we finished laying out + animationPending = true + it.postOnAnimation(startAnimation) + } } + } else { + cancelAnimationAndApplyDesiredState() } - } else { - cancelAnimationAndApplyDesiredState() } - } private fun shouldAnimateTransition( @MediaLocation currentLocation: Int, @@ -777,23 +778,29 @@ class MediaHierarchyManager @Inject constructor( } // This is an invalid transition, and can happen when using the camera gesture from the // lock screen. Disallow. - if (previousLocation == LOCATION_LOCKSCREEN && - desiredLocation == LOCATION_QQS && - statusbarState == StatusBarState.SHADE) { + if ( + previousLocation == LOCATION_LOCKSCREEN && + desiredLocation == LOCATION_QQS && + statusbarState == StatusBarState.SHADE + ) { return false } - if (currentLocation == LOCATION_QQS && + if ( + currentLocation == LOCATION_QQS && previousLocation == LOCATION_LOCKSCREEN && (statusBarStateController.leaveOpenOnKeyguardHide() || - statusbarState == StatusBarState.SHADE_LOCKED)) { + statusbarState == StatusBarState.SHADE_LOCKED) + ) { // Usually listening to the isShown is enough to determine this, but there is some // non-trivial reattaching logic happening that will make the view not-shown earlier return true } - if (statusbarState == StatusBarState.KEYGUARD && (currentLocation == LOCATION_LOCKSCREEN || - previousLocation == LOCATION_LOCKSCREEN)) { + if ( + statusbarState == StatusBarState.KEYGUARD && + (currentLocation == LOCATION_LOCKSCREEN || previousLocation == LOCATION_LOCKSCREEN) + ) { // We're always fading from lockscreen to keyguard in situations where the player // is already fully hidden return false @@ -814,8 +821,10 @@ class MediaHierarchyManager @Inject constructor( var delay = 0L if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) { // Going to the full shade, let's adjust the animation duration - if (statusbarState == StatusBarState.SHADE && - keyguardStateController.isKeyguardFadingAway) { + if ( + statusbarState == StatusBarState.SHADE && + keyguardStateController.isKeyguardFadingAway + ) { delay = keyguardStateController.keyguardFadingAwayDelay } animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong() @@ -834,14 +843,16 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Updates the bounds that the view wants to be in at the end of the animation. - */ + /** Updates the bounds that the view wants to be in at the end of the animation. */ private fun updateTargetState() { var starthost = getHost(previousLocation) var endHost = getHost(desiredLocation) - if (isCurrentlyInGuidedTransformation() && !isCurrentlyFading() && starthost != null && - endHost != null) { + if ( + isCurrentlyInGuidedTransformation() && + !isCurrentlyFading() && + starthost != null && + endHost != null + ) { val progress = getTransformationProgress() // If either of the hosts are invisible, let's keep them at the other host location to // have a nicer disappear animation. Otherwise the currentBounds of the state might @@ -868,14 +879,15 @@ class MediaHierarchyManager @Inject constructor( progress: Float, result: Rect? = null ): Rect { - val left = MathUtils.lerp(startBounds.left.toFloat(), - endBounds.left.toFloat(), progress).toInt() - val top = MathUtils.lerp(startBounds.top.toFloat(), - endBounds.top.toFloat(), progress).toInt() - val right = MathUtils.lerp(startBounds.right.toFloat(), - endBounds.right.toFloat(), progress).toInt() - val bottom = MathUtils.lerp(startBounds.bottom.toFloat(), - endBounds.bottom.toFloat(), progress).toInt() + val left = + MathUtils.lerp(startBounds.left.toFloat(), endBounds.left.toFloat(), progress).toInt() + val top = + MathUtils.lerp(startBounds.top.toFloat(), endBounds.top.toFloat(), progress).toInt() + val right = + MathUtils.lerp(startBounds.right.toFloat(), endBounds.right.toFloat(), progress).toInt() + val bottom = + MathUtils.lerp(startBounds.bottom.toFloat(), endBounds.bottom.toFloat(), progress) + .toInt() val resultBounds = result ?: Rect() resultBounds.set(left, top, right, bottom) return resultBounds @@ -884,17 +896,15 @@ class MediaHierarchyManager @Inject constructor( /** @return true if this transformation is guided by an external progress like a finger */ fun isCurrentlyInGuidedTransformation(): Boolean { return hasValidStartAndEndLocations() && - getTransformationProgress() >= 0 && - areGuidedTransitionHostsVisible() + getTransformationProgress() >= 0 && + areGuidedTransitionHostsVisible() } private fun hasValidStartAndEndLocations(): Boolean { return previousLocation != -1 && desiredLocation != -1 } - /** - * Calculate the transformation type for the current animation - */ + /** Calculate the transformation type for the current animation */ @VisibleForTesting @TransformationType fun calculateTransformationType(): Int { @@ -904,8 +914,10 @@ class MediaHierarchyManager @Inject constructor( } return TRANSFORMATION_TYPE_FADE } - if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS || - previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN) { + if ( + previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS || + previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN + ) { // animating between ls and qs should fade, as QS is clipped. return TRANSFORMATION_TYPE_FADE } @@ -918,7 +930,7 @@ class MediaHierarchyManager @Inject constructor( private fun areGuidedTransitionHostsVisible(): Boolean { return getHost(previousLocation)?.visible == true && - getHost(desiredLocation)?.visible == true + getHost(desiredLocation)?.visible == true } /** @@ -966,103 +978,115 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Apply the current state to the view, updating it's bounds and desired state - */ + /** Apply the current state to the view, updating it's bounds and desired state */ private fun applyState( bounds: Rect, alpha: Float, immediately: Boolean = false, clipBounds: Rect = EMPTY_RECT - ) = traceSection("MediaHierarchyManager#applyState") { - currentBounds.set(bounds) - currentClipping = clipBounds - carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f - val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading() - val startLocation = if (onlyUseEndState) -1 else previousLocation - val progress = if (onlyUseEndState) 1.0f else getTransformationProgress() - val endLocation = resolveLocationForFading() - mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately) - updateHostAttachment() - if (currentAttachmentLocation == IN_OVERLAY) { - // Setting the clipping on the hierarchy of `mediaFrame` does not work - if (!currentClipping.isEmpty) { - currentBounds.intersect(currentClipping) - } - mediaFrame.setLeftTopRightBottom( + ) = + traceSection("MediaHierarchyManager#applyState") { + currentBounds.set(bounds) + currentClipping = clipBounds + carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f + val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading() + val startLocation = if (onlyUseEndState) -1 else previousLocation + val progress = if (onlyUseEndState) 1.0f else getTransformationProgress() + val endLocation = resolveLocationForFading() + mediaCarouselController.setCurrentState( + startLocation, + endLocation, + progress, + immediately + ) + updateHostAttachment() + if (currentAttachmentLocation == IN_OVERLAY) { + // Setting the clipping on the hierarchy of `mediaFrame` does not work + if (!currentClipping.isEmpty) { + currentBounds.intersect(currentClipping) + } + mediaFrame.setLeftTopRightBottom( currentBounds.left, currentBounds.top, currentBounds.right, - currentBounds.bottom) + currentBounds.bottom + ) + } } - } - private fun updateHostAttachment() = traceSection( - "MediaHierarchyManager#updateHostAttachment" - ) { - var newLocation = resolveLocationForFading() - var canUseOverlay = !isCurrentlyFading() - if (isCrossFadeAnimatorRunning) { - if (getHost(newLocation)?.visible == true && - getHost(newLocation)?.hostView?.isShown == false && - newLocation != desiredLocation) { - // We're crossfading but the view is already hidden. Let's move to the overlay - // instead. This happens when animating to the full shade using a button click. - canUseOverlay = true + private fun updateHostAttachment() = + traceSection("MediaHierarchyManager#updateHostAttachment") { + var newLocation = resolveLocationForFading() + var canUseOverlay = !isCurrentlyFading() + if (isCrossFadeAnimatorRunning) { + if ( + getHost(newLocation)?.visible == true && + getHost(newLocation)?.hostView?.isShown == false && + newLocation != desiredLocation + ) { + // We're crossfading but the view is already hidden. Let's move to the overlay + // instead. This happens when animating to the full shade using a button click. + canUseOverlay = true + } } - } - val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay - newLocation = if (inOverlay) IN_OVERLAY else newLocation - if (currentAttachmentLocation != newLocation) { - currentAttachmentLocation = newLocation + val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay + newLocation = if (inOverlay) IN_OVERLAY else newLocation + if (currentAttachmentLocation != newLocation) { + currentAttachmentLocation = newLocation - // Remove the carousel from the old host - (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame) + // Remove the carousel from the old host + (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame) - // Add it to the new one - if (inOverlay) { - rootOverlay!!.add(mediaFrame) - } else { - val targetHost = getHost(newLocation)!!.hostView - // When adding back to the host, let's make sure to reset the bounds. - // Usually adding the view will trigger a layout that does this automatically, - // but we sometimes suppress this. - targetHost.addView(mediaFrame) - val left = targetHost.paddingLeft - val top = targetHost.paddingTop - mediaFrame.setLeftTopRightBottom( + // Add it to the new one + if (inOverlay) { + rootOverlay!!.add(mediaFrame) + } else { + val targetHost = getHost(newLocation)!!.hostView + // When adding back to the host, let's make sure to reset the bounds. + // Usually adding the view will trigger a layout that does this automatically, + // but we sometimes suppress this. + targetHost.addView(mediaFrame) + val left = targetHost.paddingLeft + val top = targetHost.paddingTop + mediaFrame.setLeftTopRightBottom( left, top, left + currentBounds.width(), - top + currentBounds.height()) - - if (mediaFrame.childCount > 0) { - val child = mediaFrame.getChildAt(0) - if (mediaFrame.height < child.height) { - Log.wtf(TAG, "mediaFrame height is too small for child: " + - "${mediaFrame.height} vs ${child.height}") + top + currentBounds.height() + ) + + if (mediaFrame.childCount > 0) { + val child = mediaFrame.getChildAt(0) + if (mediaFrame.height < child.height) { + Log.wtf( + TAG, + "mediaFrame height is too small for child: " + + "${mediaFrame.height} vs ${child.height}" + ) + } } } - } - if (isCrossFadeAnimatorRunning) { - // When cross-fading with an animation, we only notify the media carousel of the - // location change, once the view is reattached to the new place and not immediately - // when the desired location changes. This callback will update the measurement - // of the carousel, only once we've faded out at the old location and then reattach - // to fade it in at the new location. - mediaCarouselController.onDesiredLocationChanged( - newLocation, - getHost(newLocation), - animate = false - ) + if (isCrossFadeAnimatorRunning) { + // When cross-fading with an animation, we only notify the media carousel of the + // location change, once the view is reattached to the new place and not + // immediately + // when the desired location changes. This callback will update the measurement + // of the carousel, only once we've faded out at the old location and then + // reattach + // to fade it in at the new location. + mediaCarouselController.onDesiredLocationChanged( + newLocation, + getHost(newLocation), + animate = false + ) + } } } - } /** - * Calculate the location when cross fading between locations. While fading out, - * the content should remain in the previous location, while after the switch it should - * be at the desired location. + * Calculate the location when cross fading between locations. While fading out, the content + * should remain in the previous location, while after the switch it should be at the desired + * location. */ private fun resolveLocationForFading(): Int { if (isCrossFadeAnimatorRunning) { @@ -1079,7 +1103,8 @@ class MediaHierarchyManager @Inject constructor( private fun isTransitionRunning(): Boolean { return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f || - animator.isRunning || animationPending + animator.isRunning || + animationPending } @MediaLocation @@ -1088,31 +1113,39 @@ class MediaHierarchyManager @Inject constructor( // Keep the current location until we're allowed to again return desiredLocation } - val onLockscreen = (!bypassController.bypassEnabled && - (statusbarState == StatusBarState.KEYGUARD)) - val location = when { - dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY - (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS - qsExpansion > 0.4f && onLockscreen -> LOCATION_QS - !hasActiveMedia -> LOCATION_QS - onLockscreen && isSplitShadeExpanding() -> LOCATION_QS - onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS - onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN - else -> LOCATION_QQS - } + val onLockscreen = + (!bypassController.bypassEnabled && (statusbarState == StatusBarState.KEYGUARD)) + val location = + when { + dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY + (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS + qsExpansion > 0.4f && onLockscreen -> LOCATION_QS + !hasActiveMedia -> LOCATION_QS + onLockscreen && isSplitShadeExpanding() -> LOCATION_QS + onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS + onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN + else -> LOCATION_QQS + } // When we're on lock screen and the player is not active, we should keep it in QS. // Otherwise it will try to animate a transition that doesn't make sense. - if (location == LOCATION_LOCKSCREEN && getHost(location)?.visible != true && - !statusBarStateController.isDozing) { + if ( + location == LOCATION_LOCKSCREEN && + getHost(location)?.visible != true && + !statusBarStateController.isDozing + ) { return LOCATION_QS } - if (location == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS && - collapsingShadeFromQS) { + if ( + location == LOCATION_LOCKSCREEN && + desiredLocation == LOCATION_QS && + collapsingShadeFromQS + ) { // When collapsing on the lockscreen, we want to remain in QS return LOCATION_QS } - if (location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN && - !fullyAwake) { + if ( + location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN && !fullyAwake + ) { // When unlocking from dozing / while waking up, the media shouldn't be transitioning // in an animated way. Let's keep it in the lockscreen until we're fully awake and // reattach it without an animation @@ -1129,9 +1162,7 @@ class MediaHierarchyManager @Inject constructor( return inSplitShade && isTransitioningToFullShade } - /** - * Are we currently transforming to the full shade and already in QQS - */ + /** Are we currently transforming to the full shade and already in QQS */ private fun isTransformingToFullShadeAndInQQS(): Boolean { if (!isTransitioningToFullShade) { return false @@ -1143,9 +1174,7 @@ class MediaHierarchyManager @Inject constructor( return fullShadeTransitionProgress > 0.5f } - /** - * Is the current transformationType fading - */ + /** Is the current transformationType fading */ private fun isCurrentlyFading(): Boolean { if (isSplitShadeExpanding()) { // Split shade always uses transition instead of fade. @@ -1157,60 +1186,49 @@ class MediaHierarchyManager @Inject constructor( return isCrossFadeAnimatorRunning } - /** - * Returns true when the media card could be visible to the user if existed. - */ + /** Returns true when the media card could be visible to the user if existed. */ private fun isVisibleToUser(): Boolean { - return isLockScreenVisibleToUser() || isLockScreenShadeVisibleToUser() || - isHomeScreenShadeVisibleToUser() + return isLockScreenVisibleToUser() || + isLockScreenShadeVisibleToUser() || + isHomeScreenShadeVisibleToUser() } private fun isLockScreenVisibleToUser(): Boolean { return !statusBarStateController.isDozing && - !keyguardViewController.isBouncerShowing && - statusBarStateController.state == StatusBarState.KEYGUARD && - allowMediaPlayerOnLockScreen && - statusBarStateController.isExpanded && - !qsExpanded + !keyguardViewController.isBouncerShowing && + statusBarStateController.state == StatusBarState.KEYGUARD && + allowMediaPlayerOnLockScreen && + statusBarStateController.isExpanded && + !qsExpanded } private fun isLockScreenShadeVisibleToUser(): Boolean { return !statusBarStateController.isDozing && - !keyguardViewController.isBouncerShowing && - (statusBarStateController.state == StatusBarState.SHADE_LOCKED || - (statusBarStateController.state == StatusBarState.KEYGUARD && qsExpanded)) + !keyguardViewController.isBouncerShowing && + (statusBarStateController.state == StatusBarState.SHADE_LOCKED || + (statusBarStateController.state == StatusBarState.KEYGUARD && qsExpanded)) } private fun isHomeScreenShadeVisibleToUser(): Boolean { return !statusBarStateController.isDozing && - statusBarStateController.state == StatusBarState.SHADE && - statusBarStateController.isExpanded + statusBarStateController.state == StatusBarState.SHADE && + statusBarStateController.isExpanded } companion object { - /** - * Attached in expanded quick settings - */ + /** Attached in expanded quick settings */ const val LOCATION_QS = 0 - /** - * Attached in the collapsed QS - */ + /** Attached in the collapsed QS */ const val LOCATION_QQS = 1 - /** - * Attached on the lock screen - */ + /** Attached on the lock screen */ const val LOCATION_LOCKSCREEN = 2 - /** - * Attached on the dream overlay - */ + /** Attached on the dream overlay */ const val LOCATION_DREAM_OVERLAY = 3 - /** - * Attached at the root of the hierarchy in an overlay - */ + /** Attached at the root of the hierarchy in an overlay */ const val IN_OVERLAY = -1000 /** @@ -1226,18 +1244,29 @@ class MediaHierarchyManager @Inject constructor( const val TRANSFORMATION_TYPE_FADE = 1 } } + private val EMPTY_RECT = Rect() -@IntDef(prefix = ["TRANSFORMATION_TYPE_"], value = [ - MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION, - MediaHierarchyManager.TRANSFORMATION_TYPE_FADE]) +@IntDef( + prefix = ["TRANSFORMATION_TYPE_"], + value = + [ + MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION, + MediaHierarchyManager.TRANSFORMATION_TYPE_FADE + ] +) @Retention(AnnotationRetention.SOURCE) private annotation class TransformationType -@IntDef(prefix = ["LOCATION_"], value = [ - MediaHierarchyManager.LOCATION_QS, - MediaHierarchyManager.LOCATION_QQS, - MediaHierarchyManager.LOCATION_LOCKSCREEN, - MediaHierarchyManager.LOCATION_DREAM_OVERLAY]) +@IntDef( + prefix = ["LOCATION_"], + value = + [ + MediaHierarchyManager.LOCATION_QS, + MediaHierarchyManager.LOCATION_QQS, + MediaHierarchyManager.LOCATION_LOCKSCREEN, + MediaHierarchyManager.LOCATION_DREAM_OVERLAY + ] +) @Retention(AnnotationRetention.SOURCE) annotation class MediaLocation diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt index 864592238b73..455b7de3dc0c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt @@ -1,9 +1,28 @@ -package com.android.systemui.media +/* + * 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.media.controls.ui import android.graphics.Rect import android.util.ArraySet import android.view.View import android.view.View.OnAttachStateChangeListener +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.util.animation.DisappearParameters import com.android.systemui.util.animation.MeasurementInput import com.android.systemui.util.animation.MeasurementOutput @@ -11,7 +30,8 @@ import com.android.systemui.util.animation.UniqueObjectHostView import java.util.Objects import javax.inject.Inject -class MediaHost constructor( +class MediaHost +constructor( private val state: MediaHostStateHolder, private val mediaHierarchyManager: MediaHierarchyManager, private val mediaDataManager: MediaDataManager, @@ -26,14 +46,10 @@ class MediaHost constructor( private var inited: Boolean = false - /** - * Are we listening to media data changes? - */ + /** Are we listening to media data changes? */ private var listeningToMediaData = false - /** - * Get the current bounds on the screen. This makes sure the state is fresh and up to date - */ + /** Get the current bounds on the screen. This makes sure the state is fresh and up to date */ val currentBounds: Rect = Rect() get() { hostView.getLocationOnScreen(tmpLocationOnScreen) @@ -62,38 +78,39 @@ class MediaHost constructor( */ val currentClipping = Rect() - private val listener = object : MediaDataManager.Listener { - override fun onMediaDataLoaded( - key: String, - oldKey: String?, - data: MediaData, - immediately: Boolean, - receivedSmartspaceCardLatency: Int, - isSsReactivated: Boolean - ) { - if (immediately) { - updateViewVisibility() + private val listener = + object : MediaDataManager.Listener { + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean, + receivedSmartspaceCardLatency: Int, + isSsReactivated: Boolean + ) { + if (immediately) { + updateViewVisibility() + } } - } - override fun onSmartspaceMediaDataLoaded( - key: String, - data: SmartspaceMediaData, - shouldPrioritize: Boolean - ) { - updateViewVisibility() - } - - override fun onMediaDataRemoved(key: String) { - updateViewVisibility() - } + override fun onSmartspaceMediaDataLoaded( + key: String, + data: SmartspaceMediaData, + shouldPrioritize: Boolean + ) { + updateViewVisibility() + } - override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { - if (immediately) { + override fun onMediaDataRemoved(key: String) { updateViewVisibility() } + + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + if (immediately) { + updateViewVisibility() + } + } } - } fun addVisibilityChangeListener(listener: (Boolean) -> Unit) { visibleChangedListeners.add(listener) @@ -104,12 +121,14 @@ class MediaHost constructor( } /** - * Initialize this MediaObject and create a host view. - * All state should already be set on this host before calling this method in order to avoid - * unnecessary state changes which lead to remeasurings later on. + * Initialize this MediaObject and create a host view. All state should already be set on this + * host before calling this method in order to avoid unnecessary state changes which lead to + * remeasurings later on. * * @param location the location this host name has. Used to identify the host during + * ``` * transitions. + * ``` */ fun init(@MediaLocation location: Int) { if (inited) { @@ -122,36 +141,42 @@ class MediaHost constructor( // Listen by default, as the host might not be attached by our clients, until // they get a visibility change. We still want to stay up to date in that case! setListeningToMediaData(true) - hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener { - override fun onViewAttachedToWindow(v: View?) { - setListeningToMediaData(true) - updateViewVisibility() - } + hostView.addOnAttachStateChangeListener( + object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View?) { + setListeningToMediaData(true) + updateViewVisibility() + } - override fun onViewDetachedFromWindow(v: View?) { - setListeningToMediaData(false) + override fun onViewDetachedFromWindow(v: View?) { + setListeningToMediaData(false) + } } - }) + ) // Listen to measurement updates and update our state with it - hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager { - override fun onMeasure(input: MeasurementInput): MeasurementOutput { - // Modify the measurement to exactly match the dimensions - if (View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST) { - input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec( - View.MeasureSpec.getSize(input.widthMeasureSpec), - View.MeasureSpec.EXACTLY) + hostView.measurementManager = + object : UniqueObjectHostView.MeasurementManager { + override fun onMeasure(input: MeasurementInput): MeasurementOutput { + // Modify the measurement to exactly match the dimensions + if ( + View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST + ) { + input.widthMeasureSpec = + View.MeasureSpec.makeMeasureSpec( + View.MeasureSpec.getSize(input.widthMeasureSpec), + View.MeasureSpec.EXACTLY + ) + } + // This will trigger a state change that ensures that we now have a state + // available + state.measurementInput = input + return mediaHostStatesManager.updateCarouselDimensions(location, state) } - // This will trigger a state change that ensures that we now have a state available - state.measurementInput = input - return mediaHostStatesManager.updateCarouselDimensions(location, state) } - } // Whenever the state changes, let our state manager know - state.changedListener = { - mediaHostStatesManager.updateHostState(location, state) - } + state.changedListener = { mediaHostStatesManager.updateHostState(location, state) } updateViewVisibility() } @@ -172,17 +197,16 @@ class MediaHost constructor( * the visibility has changed */ fun updateViewVisibility() { - state.visible = if (showsOnlyActiveMedia) { - mediaDataManager.hasActiveMediaOrRecommendation() - } else { - mediaDataManager.hasAnyMediaOrRecommendation() - } + state.visible = + if (showsOnlyActiveMedia) { + mediaDataManager.hasActiveMediaOrRecommendation() + } else { + mediaDataManager.hasAnyMediaOrRecommendation() + } val newVisibility = if (visible) View.VISIBLE else View.GONE if (newVisibility != hostView.visibility) { hostView.visibility = newVisibility - visibleChangedListeners.forEach { - it.invoke(visible) - } + visibleChangedListeners.forEach { it.invoke(visible) } } } @@ -250,14 +274,10 @@ class MediaHost constructor( private var lastDisappearHash = disappearParameters.hashCode() - /** - * A listener for all changes. This won't be copied over when invoking [copy] - */ + /** A listener for all changes. This won't be copied over when invoking [copy] */ var changedListener: (() -> Unit)? = null - /** - * Get a copy of this state. This won't copy any listeners it may have set - */ + /** Get a copy of this state. This won't copy any listeners it may have set */ override fun copy(): MediaHostState { val mediaHostState = MediaHostStateHolder() mediaHostState.expansion = expansion @@ -312,15 +332,13 @@ class MediaHost constructor( } /** - * A description of a media host state that describes the behavior whenever the media carousel - * is hosted. The HostState notifies the media players of changes to their properties, who - * in turn will create view states from it. - * When adding a new property to this, make sure to update the listener and notify them - * about the changes. - * In case you need to have a different rendering based on the state, you can add a new - * constraintState to the [MediaViewController]. Otherwise, similar host states will resolve - * to the same viewstate, a behavior that is described in [CacheKey]. Make sure to only update - * that key if the underlying view needs to have a different measurement. + * A description of a media host state that describes the behavior whenever the media carousel is + * hosted. The HostState notifies the media players of changes to their properties, who in turn will + * create view states from it. When adding a new property to this, make sure to update the listener + * and notify them about the changes. In case you need to have a different rendering based on the + * state, you can add a new constraintState to the [MediaViewController]. Otherwise, similar host + * states will resolve to the same viewstate, a behavior that is described in [CacheKey]. Make sure + * to only update that key if the underlying view needs to have a different measurement. */ interface MediaHostState { @@ -330,46 +348,36 @@ interface MediaHostState { } /** - * The last measurement input that this state was measured with. Infers width and height of - * the players. + * The last measurement input that this state was measured with. Infers width and height of the + * players. */ var measurementInput: MeasurementInput? /** - * The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions), - * [EXPANDED] for fully expanded (up to 5 actions). + * The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions), [EXPANDED] + * for fully expanded (up to 5 actions). */ var expansion: Float - /** - * Fraction of the height animation. - */ + /** Fraction of the height animation. */ var squishFraction: Float - /** - * Is this host only showing active media or is it showing all of them including resumption? - */ + /** Is this host only showing active media or is it showing all of them including resumption? */ var showsOnlyActiveMedia: Boolean - /** - * If the view should be VISIBLE or GONE. - */ + /** If the view should be VISIBLE or GONE. */ val visible: Boolean - /** - * Does this host need any falsing protection? - */ + /** Does this host need any falsing protection? */ var falsingProtectionNeeded: Boolean /** * The parameters how the view disappears from this location when going to a host that's not - * visible. If modified, make sure to set this value again on the host to ensure the values - * are propagated + * visible. If modified, make sure to set this value again on the host to ensure the values are + * propagated */ var disappearParameters: DisappearParameters - /** - * Get a copy of this view state, deepcopying all appropriate members - */ + /** Get a copy of this view state, deepcopying all appropriate members */ fun copy(): MediaHostState } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHostStatesManager.kt index aea2934c46fe..ae3ce333d41d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHostStatesManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import com.android.systemui.dagger.SysUISingleton import com.android.systemui.util.animation.MeasurementOutput @@ -38,85 +38,76 @@ class MediaHostStatesManager @Inject constructor() { */ val carouselSizes: MutableMap<Int, MeasurementOutput> = mutableMapOf() - /** - * A map with all media states of all locations. - */ + /** A map with all media states of all locations. */ val mediaHostStates: MutableMap<Int, MediaHostState> = mutableMapOf() /** - * Notify that a media state for a given location has changed. Should only be called from - * Media hosts themselves. + * Notify that a media state for a given location has changed. Should only be called from Media + * hosts themselves. */ - fun updateHostState( - @MediaLocation location: Int, - hostState: MediaHostState - ) = traceSection("MediaHostStatesManager#updateHostState") { - val currentState = mediaHostStates.get(location) - if (!hostState.equals(currentState)) { - val newState = hostState.copy() - mediaHostStates.put(location, newState) - updateCarouselDimensions(location, hostState) - // First update all the controllers to ensure they get the chance to measure - for (controller in controllers) { - controller.stateCallback.onHostStateChanged(location, newState) - } + fun updateHostState(@MediaLocation location: Int, hostState: MediaHostState) = + traceSection("MediaHostStatesManager#updateHostState") { + val currentState = mediaHostStates.get(location) + if (!hostState.equals(currentState)) { + val newState = hostState.copy() + mediaHostStates.put(location, newState) + updateCarouselDimensions(location, hostState) + // First update all the controllers to ensure they get the chance to measure + for (controller in controllers) { + controller.stateCallback.onHostStateChanged(location, newState) + } - // Then update all other callbacks which may depend on the controllers above - for (callback in callbacks) { - callback.onHostStateChanged(location, newState) + // Then update all other callbacks which may depend on the controllers above + for (callback in callbacks) { + callback.onHostStateChanged(location, newState) + } } } - } /** - * Get the dimensions of all players combined, which determines the overall height of the - * media carousel and the media hosts. + * Get the dimensions of all players combined, which determines the overall height of the media + * carousel and the media hosts. */ fun updateCarouselDimensions( @MediaLocation location: Int, hostState: MediaHostState - ): MeasurementOutput = traceSection("MediaHostStatesManager#updateCarouselDimensions") { - val result = MeasurementOutput(0, 0) - for (controller in controllers) { - val measurement = controller.getMeasurementsForState(hostState) - measurement?.let { - if (it.measuredHeight > result.measuredHeight) { - result.measuredHeight = it.measuredHeight - } - if (it.measuredWidth > result.measuredWidth) { - result.measuredWidth = it.measuredWidth + ): MeasurementOutput = + traceSection("MediaHostStatesManager#updateCarouselDimensions") { + val result = MeasurementOutput(0, 0) + for (controller in controllers) { + val measurement = controller.getMeasurementsForState(hostState) + measurement?.let { + if (it.measuredHeight > result.measuredHeight) { + result.measuredHeight = it.measuredHeight + } + if (it.measuredWidth > result.measuredWidth) { + result.measuredWidth = it.measuredWidth + } } } + carouselSizes[location] = result + return result } - carouselSizes[location] = result - return result - } - /** - * Add a callback to be called when a MediaState has updated - */ + /** Add a callback to be called when a MediaState has updated */ fun addCallback(callback: Callback) { callbacks.add(callback) } - /** - * Remove a callback that listens to media states - */ + /** Remove a callback that listens to media states */ fun removeCallback(callback: Callback) { callbacks.remove(callback) } /** - * Register a controller that listens to media states and is used to determine the size of - * the media carousel + * Register a controller that listens to media states and is used to determine the size of the + * media carousel */ fun addController(controller: MediaViewController) { controllers.add(controller) } - /** - * Notify the manager about the removal of a controller. - */ + /** Notify the manager about the removal of a controller. */ fun removeController(controller: MediaViewController) { controllers.remove(controller) } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt index 00273bc34552..0e0746590776 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt @@ -1,4 +1,20 @@ -package com.android.systemui.media +/* + * 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.media.controls.ui import android.content.Context import android.os.SystemClock @@ -11,15 +27,13 @@ import com.android.systemui.Gefingerpoken import com.android.wm.shell.animation.physicsAnimator /** - * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful - * when only measuring children but not the parent, when trying to apply a new scroll position + * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful when + * only measuring children but not the parent, when trying to apply a new scroll position */ -class MediaScrollView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) - : HorizontalScrollView(context, attrs, defStyleAttr) { +class MediaScrollView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + HorizontalScrollView(context, attrs, defStyleAttr) { lateinit var contentContainer: ViewGroup private set @@ -34,35 +48,33 @@ class MediaScrollView @JvmOverloads constructor( * Get the current content translation. This is usually the normal translationX of the content, * but when animating, it might differ */ - fun getContentTranslation() = if (contentContainer.physicsAnimator.isRunning()) { - animationTargetX - } else { - contentContainer.translationX - } + fun getContentTranslation() = + if (contentContainer.physicsAnimator.isRunning()) { + animationTargetX + } else { + contentContainer.translationX + } /** * Convert between the absolute (left-to-right) and relative (start-to-end) scrollX of the media - * carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX - * is always absolute. This function is its own inverse. + * carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX is + * always absolute. This function is its own inverse. */ - private fun transformScrollX(scrollX: Int): Int = if (isLayoutRtl) { - contentContainer.width - width - scrollX - } else { - scrollX - } + private fun transformScrollX(scrollX: Int): Int = + if (isLayoutRtl) { + contentContainer.width - width - scrollX + } else { + scrollX + } - /** - * Get the layoutDirection-relative (start-to-end) scroll X position of the carousel. - */ + /** Get the layoutDirection-relative (start-to-end) scroll X position of the carousel. */ var relativeScrollX: Int get() = transformScrollX(scrollX) set(value) { scrollX = transformScrollX(value) } - /** - * Allow all scrolls to go through, use base implementation - */ + /** Allow all scrolls to go through, use base implementation */ override fun scrollTo(x: Int, y: Int) { if (mScrollX != x || mScrollY != y) { val oldX: Int = mScrollX @@ -79,17 +91,13 @@ class MediaScrollView @JvmOverloads constructor( override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { var intercept = false - touchListener?.let { - intercept = it.onInterceptTouchEvent(ev) - } + touchListener?.let { intercept = it.onInterceptTouchEvent(ev) } return super.onInterceptTouchEvent(ev) || intercept } override fun onTouchEvent(ev: MotionEvent?): Boolean { var touch = false - touchListener?.let { - touch = it.onTouchEvent(ev) - } + touchListener?.let { touch = it.onTouchEvent(ev) } return super.onTouchEvent(ev) || touch } @@ -113,19 +121,25 @@ class MediaScrollView @JvmOverloads constructor( // When we're dismissing we ignore all the scrolling return false } - return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, - scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent) + return super.overScrollBy( + deltaX, + deltaY, + scrollX, + scrollY, + scrollRangeX, + scrollRangeY, + maxOverScrollX, + maxOverScrollY, + isTouchEvent + ) } - /** - * Cancel the current touch event going on. - */ + /** Cancel the current touch event going on. */ fun cancelCurrentScroll() { val now = SystemClock.uptimeMillis() - val event = MotionEvent.obtain(now, now, - MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0) + val event = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0) event.source = InputDevice.SOURCE_TOUCHSCREEN super.onTouchEvent(event) event.recycle() } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index faa7aaee3c9a..4bf3031c02b4 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -14,19 +14,22 @@ * limitations under the License */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.content.Context import android.content.res.Configuration import androidx.annotation.VisibleForTesting import androidx.constraintlayout.widget.ConstraintSet import com.android.systemui.R -import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY -import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY -import com.android.systemui.media.MediaCarouselController.Companion.DURATION -import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY -import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY -import com.android.systemui.media.MediaCarouselController.Companion.calculateAlpha +import com.android.systemui.media.controls.models.GutsViewHolder +import com.android.systemui.media.controls.models.player.MediaViewHolder +import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.calculateAlpha import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.animation.MeasurementOutput import com.android.systemui.util.animation.TransitionLayout @@ -39,7 +42,9 @@ import javax.inject.Inject * A class responsible for controlling a single instance of a media player handling interactions * with the view instance and keeping the media view states up to date. */ -class MediaViewController @Inject constructor( +class MediaViewController +@Inject +constructor( private val context: Context, private val configurationController: ConfigurationController, private val mediaHostStatesManager: MediaHostStatesManager, @@ -47,17 +52,18 @@ class MediaViewController @Inject constructor( ) { /** - * Indicating that the media view controller is for a notification-based player, - * session-based player, or recommendation + * Indicating that the media view controller is for a notification-based player, session-based + * player, or recommendation */ enum class TYPE { - PLAYER, RECOMMENDATION + PLAYER, + RECOMMENDATION } companion object { - @JvmField - val GUTS_ANIMATION_DURATION = 500L - val controlIds = setOf( + @JvmField val GUTS_ANIMATION_DURATION = 500L + val controlIds = + setOf( R.id.media_progress_bar, R.id.actionNext, R.id.actionPrev, @@ -68,22 +74,20 @@ class MediaViewController @Inject constructor( R.id.action4, R.id.media_scrubbing_elapsed_time, R.id.media_scrubbing_total_time - ) + ) - val detailIds = setOf( + val detailIds = + setOf( R.id.header_title, R.id.header_artist, R.id.actionPlayPause, - ) + ) } - /** - * A listener when the current dimensions of the player change - */ + /** A listener when the current dimensions of the player change */ lateinit var sizeChangedListener: () -> Unit private var firstRefresh: Boolean = true - @VisibleForTesting - private var transitionLayout: TransitionLayout? = null + @VisibleForTesting private var transitionLayout: TransitionLayout? = null private val layoutController = TransitionLayoutController() private var animationDelay: Long = 0 private var animationDuration: Long = 0 @@ -91,116 +95,98 @@ class MediaViewController @Inject constructor( private val measurement = MeasurementOutput(0, 0) private var type: TYPE = TYPE.PLAYER - /** - * A map containing all viewStates for all locations of this mediaState - */ + /** A map containing all viewStates for all locations of this mediaState */ private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf() /** * The ending location of the view where it ends when all animations and transitions have * finished */ - @MediaLocation - var currentEndLocation: Int = -1 + @MediaLocation var currentEndLocation: Int = -1 - /** - * The starting location of the view where it starts for all animations and transitions - */ - @MediaLocation - private var currentStartLocation: Int = -1 + /** The starting location of the view where it starts for all animations and transitions */ + @MediaLocation private var currentStartLocation: Int = -1 - /** - * The progress of the transition or 1.0 if there is no transition happening - */ + /** The progress of the transition or 1.0 if there is no transition happening */ private var currentTransitionProgress: Float = 1.0f - /** - * A temporary state used to store intermediate measurements. - */ + /** A temporary state used to store intermediate measurements. */ private val tmpState = TransitionViewState() - /** - * A temporary state used to store intermediate measurements. - */ + /** A temporary state used to store intermediate measurements. */ private val tmpState2 = TransitionViewState() - /** - * A temporary state used to store intermediate measurements. - */ + /** A temporary state used to store intermediate measurements. */ private val tmpState3 = TransitionViewState() - /** - * A temporary cache key to be used to look up cache entries - */ + /** A temporary cache key to be used to look up cache entries */ private val tmpKey = CacheKey() /** - * The current width of the player. This might not factor in case the player is animating - * to the current state, but represents the end state + * The current width of the player. This might not factor in case the player is animating to the + * current state, but represents the end state */ var currentWidth: Int = 0 /** - * The current height of the player. This might not factor in case the player is animating - * to the current state, but represents the end state + * The current height of the player. This might not factor in case the player is animating to + * the current state, but represents the end state */ var currentHeight: Int = 0 - /** - * Get the translationX of the layout - */ + /** Get the translationX of the layout */ var translationX: Float = 0.0f private set get() { return transitionLayout?.translationX ?: 0.0f } - /** - * Get the translationY of the layout - */ + /** Get the translationY of the layout */ var translationY: Float = 0.0f private set get() { return transitionLayout?.translationY ?: 0.0f } - /** - * A callback for RTL config changes - */ - private val configurationListener = object : ConfigurationController.ConfigurationListener { - override fun onConfigChanged(newConfig: Configuration?) { - // Because the TransitionLayout is not always attached (and calculates/caches layout - // results regardless of attach state), we have to force the layoutDirection of the view - // to the correct value for the user's current locale to ensure correct recalculation - // when/after calling refreshState() - newConfig?.apply { - if (transitionLayout?.rawLayoutDirection != layoutDirection) { - transitionLayout?.layoutDirection = layoutDirection - refreshState() + /** A callback for RTL config changes */ + private val configurationListener = + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration?) { + // Because the TransitionLayout is not always attached (and calculates/caches layout + // results regardless of attach state), we have to force the layoutDirection of the + // view + // to the correct value for the user's current locale to ensure correct + // recalculation + // when/after calling refreshState() + newConfig?.apply { + if (transitionLayout?.rawLayoutDirection != layoutDirection) { + transitionLayout?.layoutDirection = layoutDirection + refreshState() + } } } } - } - /** - * A callback for media state changes - */ - val stateCallback = object : MediaHostStatesManager.Callback { - override fun onHostStateChanged( - @MediaLocation location: Int, - mediaHostState: MediaHostState - ) { - if (location == currentEndLocation || location == currentStartLocation) { - setCurrentState(currentStartLocation, + /** A callback for media state changes */ + val stateCallback = + object : MediaHostStatesManager.Callback { + override fun onHostStateChanged( + @MediaLocation location: Int, + mediaHostState: MediaHostState + ) { + if (location == currentEndLocation || location == currentStartLocation) { + setCurrentState( + currentStartLocation, currentEndLocation, currentTransitionProgress, - applyImmediately = false) + applyImmediately = false + ) + } } } - } /** - * The expanded constraint set used to render a expanded player. If it is modified, make sure - * to call [refreshState] + * The expanded constraint set used to render a expanded player. If it is modified, make sure to + * call [refreshState] */ val collapsedLayout = ConstraintSet() @@ -210,9 +196,7 @@ class MediaViewController @Inject constructor( */ val expandedLayout = ConstraintSet() - /** - * Whether the guts are visible for the associated player. - */ + /** Whether the guts are visible for the associated player. */ var isGutsVisible = false private set @@ -234,17 +218,17 @@ class MediaViewController @Inject constructor( configurationController.removeCallback(configurationListener) } - /** - * Show guts with an animated transition. - */ + /** Show guts with an animated transition. */ fun openGuts() { if (isGutsVisible) return isGutsVisible = true animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) - setCurrentState(currentStartLocation, - currentEndLocation, - currentTransitionProgress, - applyImmediately = false) + setCurrentState( + currentStartLocation, + currentEndLocation, + currentTransitionProgress, + applyImmediately = false + ) } /** @@ -259,10 +243,12 @@ class MediaViewController @Inject constructor( if (!immediate) { animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) } - setCurrentState(currentStartLocation, - currentEndLocation, - currentTransitionProgress, - applyImmediately = immediate) + setCurrentState( + currentStartLocation, + currentEndLocation, + currentTransitionProgress, + applyImmediately = immediate + ) } private fun ensureAllMeasurements() { @@ -272,21 +258,20 @@ class MediaViewController @Inject constructor( } } - /** - * Get the constraintSet for a given expansion - */ + /** Get the constraintSet for a given expansion */ private fun constraintSetForExpansion(expansion: Float): ConstraintSet = - if (expansion > 0) expandedLayout else collapsedLayout + if (expansion > 0) expandedLayout else collapsedLayout /** * Set the views to be showing/hidden based on the [isGutsVisible] for a given * [TransitionViewState]. */ private fun setGutsViewState(viewState: TransitionViewState) { - val controlsIds = when (type) { - TYPE.PLAYER -> MediaViewHolder.controlsIds - TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds - } + val controlsIds = + when (type) { + TYPE.PLAYER -> MediaViewHolder.controlsIds + TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds + } val gutsIds = GutsViewHolder.ids controlsIds.forEach { id -> viewState.widgetStates.get(id)?.let { state -> @@ -304,9 +289,7 @@ class MediaViewController @Inject constructor( } } - /** - * Apply squishFraction to a copy of viewState such that the cached version is untouched. - */ + /** Apply squishFraction to a copy of viewState such that the cached version is untouched. */ internal fun squishViewState( viewState: TransitionViewState, squishFraction: Float @@ -344,8 +327,8 @@ class MediaViewController @Inject constructor( * Obtain a new viewState for a given media state. This usually returns a cached state, but if * it's not available, it will recreate one by measuring, which may be expensive. */ - @VisibleForTesting - fun obtainViewState(state: MediaHostState?): TransitionViewState? { + @VisibleForTesting + fun obtainViewState(state: MediaHostState?): TransitionViewState? { if (state == null || state.measurementInput == null) { return null } @@ -368,10 +351,12 @@ class MediaViewController @Inject constructor( } // Let's create a new measurement if (state.expansion == 0.0f || state.expansion == 1.0f) { - result = transitionLayout!!.calculateViewState( + result = + transitionLayout!!.calculateViewState( state.measurementInput!!, constraintSetForExpansion(state.expansion), - TransitionViewState()) + TransitionViewState() + ) setGutsViewState(result) // We don't want to cache interpolated or null states as this could quickly fill up @@ -387,10 +372,8 @@ class MediaViewController @Inject constructor( val startViewState = obtainViewState(startState) as TransitionViewState val endState = state.copy().also { it.expansion = 1.0f } val endViewState = obtainViewState(endState) as TransitionViewState - result = layoutController.getInterpolatedState( - startViewState, - endViewState, - state.expansion) + result = + layoutController.getInterpolatedState(startViewState, endViewState, state.expansion) } if (state.squishFraction <= 1f) { return squishViewState(result, state.squishFraction) @@ -398,11 +381,7 @@ class MediaViewController @Inject constructor( return result } - private fun getKey( - state: MediaHostState, - guts: Boolean, - result: CacheKey - ): CacheKey { + private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey { result.apply { heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0 widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0 @@ -413,41 +392,39 @@ class MediaViewController @Inject constructor( } /** - * Attach a view to this controller. This may perform measurements if it's not available yet - * and should therefore be done carefully. + * Attach a view to this controller. This may perform measurements if it's not available yet and + * should therefore be done carefully. */ - fun attach( - transitionLayout: TransitionLayout, - type: TYPE - ) = traceSection("MediaViewController#attach") { - updateMediaViewControllerType(type) - logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation) - this.transitionLayout = transitionLayout - layoutController.attach(transitionLayout) - if (currentEndLocation == -1) { - return - } - // Set the previously set state immediately to the view, now that it's finally attached - setCurrentState( + fun attach(transitionLayout: TransitionLayout, type: TYPE) = + traceSection("MediaViewController#attach") { + updateMediaViewControllerType(type) + logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation) + this.transitionLayout = transitionLayout + layoutController.attach(transitionLayout) + if (currentEndLocation == -1) { + return + } + // Set the previously set state immediately to the view, now that it's finally attached + setCurrentState( startLocation = currentStartLocation, endLocation = currentEndLocation, transitionProgress = currentTransitionProgress, - applyImmediately = true) - } + applyImmediately = true + ) + } /** - * Obtain a measurement for a given location. This makes sure that the state is up to date - * and all widgets know their location. Calling this method may create a measurement if we - * don't have a cached value available already. + * Obtain a measurement for a given location. This makes sure that the state is up to date and + * all widgets know their location. Calling this method may create a measurement if we don't + * have a cached value available already. */ - fun getMeasurementsForState( - hostState: MediaHostState - ): MeasurementOutput? = traceSection("MediaViewController#getMeasurementsForState") { - val viewState = obtainViewState(hostState) ?: return null - measurement.measuredWidth = viewState.width - measurement.measuredHeight = viewState.height - return measurement - } + fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? = + traceSection("MediaViewController#getMeasurementsForState") { + val viewState = obtainViewState(hostState) ?: return null + measurement.measuredWidth = viewState.width + measurement.measuredHeight = viewState.height + return measurement + } /** * Set a new state for the controlled view which can be an interpolation between multiple @@ -458,67 +435,85 @@ class MediaViewController @Inject constructor( @MediaLocation endLocation: Int, transitionProgress: Float, applyImmediately: Boolean - ) = traceSection("MediaViewController#setCurrentState") { - currentEndLocation = endLocation - currentStartLocation = startLocation - currentTransitionProgress = transitionProgress - logger.logMediaLocation("setCurrentState", startLocation, endLocation) - - val shouldAnimate = animateNextStateChange && !applyImmediately - - val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return - val startHostState = mediaHostStatesManager.mediaHostStates[startLocation] - - // Obtain the view state that we'd want to be at the end - // The view might not be bound yet or has never been measured and in that case will be - // reset once the state is fully available - var endViewState = obtainViewState(endHostState) ?: return - endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!! - layoutController.setMeasureState(endViewState) - - // If the view isn't bound, we can drop the animation, otherwise we'll execute it - animateNextStateChange = false - if (transitionLayout == null) { - return - } - - val result: TransitionViewState - var startViewState = obtainViewState(startHostState) - startViewState = updateViewStateToCarouselSize(startViewState, startLocation, tmpState3) + ) = + traceSection("MediaViewController#setCurrentState") { + currentEndLocation = endLocation + currentStartLocation = startLocation + currentTransitionProgress = transitionProgress + logger.logMediaLocation("setCurrentState", startLocation, endLocation) + + val shouldAnimate = animateNextStateChange && !applyImmediately + + val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return + val startHostState = mediaHostStatesManager.mediaHostStates[startLocation] + + // Obtain the view state that we'd want to be at the end + // The view might not be bound yet or has never been measured and in that case will be + // reset once the state is fully available + var endViewState = obtainViewState(endHostState) ?: return + endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!! + layoutController.setMeasureState(endViewState) + + // If the view isn't bound, we can drop the animation, otherwise we'll execute it + animateNextStateChange = false + if (transitionLayout == null) { + return + } - if (!endHostState.visible) { - // Let's handle the case where the end is gone first. In this case we take the - // start viewState and will make it gone - if (startViewState == null || startHostState == null || !startHostState.visible) { - // the start isn't a valid state, let's use the endstate directly + val result: TransitionViewState + var startViewState = obtainViewState(startHostState) + startViewState = updateViewStateToCarouselSize(startViewState, startLocation, tmpState3) + + if (!endHostState.visible) { + // Let's handle the case where the end is gone first. In this case we take the + // start viewState and will make it gone + if (startViewState == null || startHostState == null || !startHostState.visible) { + // the start isn't a valid state, let's use the endstate directly + result = endViewState + } else { + // Let's get the gone presentation from the start state + result = + layoutController.getGoneState( + startViewState, + startHostState.disappearParameters, + transitionProgress, + tmpState + ) + } + } else if (startHostState != null && !startHostState.visible) { + // We have a start state and it is gone. + // Let's get presentation from the endState + result = + layoutController.getGoneState( + endViewState, + endHostState.disappearParameters, + 1.0f - transitionProgress, + tmpState + ) + } else if (transitionProgress == 1.0f || startViewState == null) { + // We're at the end. Let's use that state result = endViewState + } else if (transitionProgress == 0.0f) { + // We're at the start. Let's use that state + result = startViewState } else { - // Let's get the gone presentation from the start state - result = layoutController.getGoneState(startViewState, - startHostState.disappearParameters, + result = + layoutController.getInterpolatedState( + startViewState, + endViewState, transitionProgress, - tmpState) + tmpState + ) } - } else if (startHostState != null && !startHostState.visible) { - // We have a start state and it is gone. - // Let's get presentation from the endState - result = layoutController.getGoneState(endViewState, endHostState.disappearParameters, - 1.0f - transitionProgress, - tmpState) - } else if (transitionProgress == 1.0f || startViewState == null) { - // We're at the end. Let's use that state - result = endViewState - } else if (transitionProgress == 0.0f) { - // We're at the start. Let's use that state - result = startViewState - } else { - result = layoutController.getInterpolatedState(startViewState, endViewState, - transitionProgress, tmpState) + logger.logMediaSize("setCurrentState", result.width, result.height) + layoutController.setState( + result, + applyImmediately, + shouldAnimate, + animationDuration, + animationDelay + ) } - logger.logMediaSize("setCurrentState", result.width, result.height) - layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration, - animationDelay) - } private fun updateViewStateToCarouselSize( viewState: TransitionViewState?, @@ -555,8 +550,8 @@ class MediaViewController @Inject constructor( } /** - * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. - * In the event of [location] not being visible, [locationWhenHidden] will be used instead. + * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. In the event + * of [location] not being visible, [locationWhenHidden] will be used instead. * * @param location Target * @param locationWhenHidden Location that will be used when the target is not @@ -573,40 +568,37 @@ class MediaViewController @Inject constructor( * This updates the width the view will me measured with. */ fun onLocationPreChange(@MediaLocation newLocation: Int) { - obtainViewStateForLocation(newLocation)?.let { - layoutController.setMeasureState(it) - } + obtainViewStateForLocation(newLocation)?.let { layoutController.setMeasureState(it) } } - /** - * Request that the next state change should be animated with the given parameters. - */ + /** Request that the next state change should be animated with the given parameters. */ fun animatePendingStateChange(duration: Long, delay: Long) { animateNextStateChange = true animationDuration = duration animationDelay = delay } - /** - * Clear all existing measurements and refresh the state to match the view. - */ - fun refreshState() = traceSection("MediaViewController#refreshState") { - // Let's clear all of our measurements and recreate them! - viewStates.clear() - if (firstRefresh) { - // This is the first bind, let's ensure we pre-cache all measurements. Otherwise - // We'll just load these on demand. - ensureAllMeasurements() - firstRefresh = false + /** Clear all existing measurements and refresh the state to match the view. */ + fun refreshState() = + traceSection("MediaViewController#refreshState") { + // Let's clear all of our measurements and recreate them! + viewStates.clear() + if (firstRefresh) { + // This is the first bind, let's ensure we pre-cache all measurements. Otherwise + // We'll just load these on demand. + ensureAllMeasurements() + firstRefresh = false + } + setCurrentState( + currentStartLocation, + currentEndLocation, + currentTransitionProgress, + applyImmediately = true + ) } - setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress, - applyImmediately = true) - } } -/** - * An internal key for the cache of mediaViewStates. This is a subset of the full host state. - */ +/** An internal key for the cache of mediaViewStates. This is a subset of the full host state. */ private data class CacheKey( var widthMeasureSpec: Int = -1, var heightMeasureSpec: Int = -1, diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt index 73868189b362..fdac33ac20b0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt @@ -14,50 +14,42 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaViewLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject private const val TAG = "MediaView" -/** - * A buffered log for media view events that are too noisy for regular logging - */ +/** A buffered log for media view events that are too noisy for regular logging */ @SysUISingleton -class MediaViewLogger @Inject constructor( - @MediaViewLog private val buffer: LogBuffer -) { +class MediaViewLogger @Inject constructor(@MediaViewLog private val buffer: LogBuffer) { fun logMediaSize(reason: String, width: Int, height: Int) { buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = reason - int1 = width - int2 = height - }, - { - "size ($str1): $int1 x $int2" - } + TAG, + LogLevel.DEBUG, + { + str1 = reason + int1 = width + int2 = height + }, + { "size ($str1): $int1 x $int2" } ) } fun logMediaLocation(reason: String, startLocation: Int, endLocation: Int) { buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = reason - int1 = startLocation - int2 = endLocation - }, - { - "location ($str1): $int1 -> $int2" - } + TAG, + LogLevel.DEBUG, + { + str1 = reason + int1 = startLocation + int2 = endLocation + }, + { "location ($str1): $int1 -> $int2" } ) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MetadataAnimationHandler.kt index 48f4a16cc538..1cdcf5ed2702 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MetadataAnimationHandler.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -73,4 +73,4 @@ internal open class MetadataAnimationHandler( exitAnimator.addListener(this) enterAnimator.addListener(this) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt index 6bc94cd5f525..e9b2cf2b18d1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt @@ -1,4 +1,20 @@ -package com.android.systemui.media +/* + * 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.media.controls.ui import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -23,8 +39,7 @@ import kotlin.math.cos private const val TAG = "Squiggly" private const val TWO_PI = (Math.PI * 2f).toFloat() -@VisibleForTesting -internal const val DISABLED_ALPHA = 77 +@VisibleForTesting internal const val DISABLED_ALPHA = 77 class SquigglyProgress : Drawable() { @@ -86,26 +101,29 @@ class SquigglyProgress : Drawable() { lastFrameTime = SystemClock.uptimeMillis() } heightAnimator?.cancel() - heightAnimator = ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply { - if (animate) { - startDelay = 60 - duration = 800 - interpolator = Interpolators.EMPHASIZED_DECELERATE - } else { - duration = 550 - interpolator = Interpolators.STANDARD_DECELERATE - } - addUpdateListener { - heightFraction = it.animatedValue as Float - invalidateSelf() - } - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - heightAnimator = null + heightAnimator = + ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply { + if (animate) { + startDelay = 60 + duration = 800 + interpolator = Interpolators.EMPHASIZED_DECELERATE + } else { + duration = 550 + interpolator = Interpolators.STANDARD_DECELERATE } - }) - start() - } + addUpdateListener { + heightFraction = it.animatedValue as Float + invalidateSelf() + } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + heightAnimator = null + } + } + ) + start() + } } override fun draw(canvas: Canvas) { @@ -120,9 +138,15 @@ class SquigglyProgress : Drawable() { val progress = level / 10_000f val totalWidth = bounds.width().toFloat() val totalProgressPx = totalWidth * progress - val waveProgressPx = totalWidth * ( - if (!transitionEnabled || progress > matchedWaveEndpoint) progress else - lerp(minWaveEndpoint, matchedWaveEndpoint, lerpInv(0f, matchedWaveEndpoint, progress))) + val waveProgressPx = + totalWidth * + (if (!transitionEnabled || progress > matchedWaveEndpoint) progress + else + lerp( + minWaveEndpoint, + matchedWaveEndpoint, + lerpInv(0f, matchedWaveEndpoint, progress) + )) // Build Wiggly Path val waveStart = -phaseOffset - waveLength / 2f @@ -132,10 +156,8 @@ class SquigglyProgress : Drawable() { val computeAmplitude: (Float, Float) -> Float = { x, sign -> if (transitionEnabled) { val length = transitionPeriods * waveLength - val coeff = lerpInvSat( - waveProgressPx + length / 2f, - waveProgressPx - length / 2f, - x) + val coeff = + lerpInvSat(waveProgressPx + length / 2f, waveProgressPx - length / 2f, x) sign * heightFraction * lineAmplitude * coeff } else { sign * heightFraction * lineAmplitude @@ -156,10 +178,7 @@ class SquigglyProgress : Drawable() { val nextX = currentX + dist val midX = currentX + dist / 2 val nextAmp = computeAmplitude(nextX, waveSign) - path.cubicTo( - midX, currentAmp, - midX, nextAmp, - nextX, nextAmp) + path.cubicTo(midX, currentAmp, midX, nextAmp, nextX, nextAmp) currentAmp = nextAmp currentX = nextX } @@ -229,7 +248,7 @@ class SquigglyProgress : Drawable() { private fun updateColors(tintColor: Int, alpha: Int) { wavePaint.color = ColorUtils.setAlphaComponent(tintColor, alpha) - linePaint.color = ColorUtils.setAlphaComponent(tintColor, - (DISABLED_ALPHA * (alpha / 255f)).toInt()) + linePaint.color = + ColorUtils.setAlphaComponent(tintColor, (DISABLED_ALPHA * (alpha / 255f)).toInt()) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.java index ed3e10939b6a..6caf5c20b81c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.util; import android.annotation.NonNull; import android.content.Context; diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataUtils.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java index b8185b9de7e8..bcfceaa3205e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataUtils.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java @@ -14,15 +14,25 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.util; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.text.TextUtils; +/** + * Utility class with common methods for media controls + */ public class MediaDataUtils { + /** + * Get the application label for a given package + * @param context the context to use + * @param packageName Package to check + * @param unknownName Fallback string if application is not found + * @return The label or fallback string + */ public static String getAppLabel(Context context, String packageName, String unknownName) { if (TextUtils.isEmpty(packageName)) { return null; diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFeatureFlag.kt index 75eb33da64d8..91dac6f1a528 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFeatureFlag.kt @@ -14,15 +14,13 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.util import android.content.Context import com.android.systemui.util.Utils import javax.inject.Inject -/** - * Provides access to the current value of the feature flag. - */ +/** Provides access to the current value of the feature flag. */ class MediaFeatureFlag @Inject constructor(private val context: Context) { val enabled get() = Utils.useQsMediaPlayer(context) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index b85ae4820d49..8d4931a5d08c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.util import android.app.StatusBarManager import android.os.UserHandle @@ -34,9 +34,7 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) { return enabled || featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS) } - /** - * Check whether we support displaying information about mute await connections. - */ + /** Check whether we support displaying information about mute await connections. */ fun areMuteAwaitConnectionsEnabled() = featureFlags.isEnabled(Flags.MEDIA_MUTE_AWAIT) /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt index 0baf01e7476f..3ad8c21e8a1e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.util import com.android.internal.logging.InstanceId import com.android.internal.logging.InstanceIdSequence @@ -22,22 +22,21 @@ import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.ui.MediaHierarchyManager +import com.android.systemui.media.controls.ui.MediaLocation import java.lang.IllegalArgumentException import javax.inject.Inject private const val INSTANCE_ID_MAX = 1 shl 20 -/** - * A helper class to log events related to the media controls - */ +/** A helper class to log events related to the media controls */ @SysUISingleton class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) { private val instanceIdSequence = InstanceIdSequence(INSTANCE_ID_MAX) - /** - * Get a new instance ID for a new media control - */ + /** Get a new instance ID for a new media control */ fun getNewInstanceId(): InstanceId { return instanceIdSequence.newInstanceId() } @@ -48,12 +47,13 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) instanceId: InstanceId, playbackLocation: Int ) { - val event = when (playbackLocation) { - MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED - MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED - MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED - else -> throw IllegalArgumentException("Unknown playback location") - } + val event = + when (playbackLocation) { + MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED + MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED + MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED + else -> throw IllegalArgumentException("Unknown playback location") + } logger.logWithInstanceId(event, uid, packageName, instanceId) } @@ -63,12 +63,13 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) instanceId: InstanceId, playbackLocation: Int ) { - val event = when (playbackLocation) { - MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL - MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST - MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE - else -> throw IllegalArgumentException("Unknown playback location") - } + val event = + when (playbackLocation) { + MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL + MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST + MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE + else -> throw IllegalArgumentException("Unknown playback location") + } logger.logWithInstanceId(event, uid, packageName, instanceId) } @@ -107,8 +108,12 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) } fun logLongPressSettings(uid: Int, packageName: String, instanceId: InstanceId) { - logger.logWithInstanceId(MediaUiEvent.OPEN_SETTINGS_LONG_PRESS, uid, packageName, - instanceId) + logger.logWithInstanceId( + MediaUiEvent.OPEN_SETTINGS_LONG_PRESS, + uid, + packageName, + instanceId + ) } fun logCarouselSettings() { @@ -117,12 +122,13 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) } fun logTapAction(buttonId: Int, uid: Int, packageName: String, instanceId: InstanceId) { - val event = when (buttonId) { - R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE - R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV - R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT - else -> MediaUiEvent.TAP_ACTION_OTHER - } + val event = + when (buttonId) { + R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE + R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV + R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT + else -> MediaUiEvent.TAP_ACTION_OTHER + } logger.logWithInstanceId(event, uid, packageName, instanceId) } @@ -140,148 +146,130 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) } fun logCarouselPosition(@MediaLocation location: Int) { - val event = when (location) { - MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS - MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS - MediaHierarchyManager.LOCATION_LOCKSCREEN -> - MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN - MediaHierarchyManager.LOCATION_DREAM_OVERLAY -> - MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM - else -> throw IllegalArgumentException("Unknown media carousel location $location") - } + val event = + when (location) { + MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS + MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS + MediaHierarchyManager.LOCATION_LOCKSCREEN -> + MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN + MediaHierarchyManager.LOCATION_DREAM_OVERLAY -> + MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM + else -> throw IllegalArgumentException("Unknown media carousel location $location") + } logger.log(event) } fun logRecommendationAdded(packageName: String, instanceId: InstanceId) { - logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ADDED, 0, packageName, - instanceId) + logger.logWithInstanceId( + MediaUiEvent.MEDIA_RECOMMENDATION_ADDED, + 0, + packageName, + instanceId + ) } fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) { - logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED, 0, packageName, - instanceId) + logger.logWithInstanceId( + MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED, + 0, + packageName, + instanceId + ) } fun logRecommendationActivated(uid: Int, packageName: String, instanceId: InstanceId) { - logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED, uid, packageName, - instanceId) + logger.logWithInstanceId( + MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED, + uid, + packageName, + instanceId + ) } fun logRecommendationItemTap(packageName: String, instanceId: InstanceId, position: Int) { - logger.logWithInstanceIdAndPosition(MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP, 0, - packageName, instanceId, position) + logger.logWithInstanceIdAndPosition( + MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP, + 0, + packageName, + instanceId, + position + ) } fun logRecommendationCardTap(packageName: String, instanceId: InstanceId) { - logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP, 0, packageName, - instanceId) + logger.logWithInstanceId( + MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP, + 0, + packageName, + instanceId + ) } fun logOpenBroadcastDialog(uid: Int, packageName: String, instanceId: InstanceId) { - logger.logWithInstanceId(MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG, uid, packageName, - instanceId) + logger.logWithInstanceId( + MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG, + uid, + packageName, + instanceId + ) } } enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum { @UiEvent(doc = "A new media control was added for media playing locally on the device") LOCAL_MEDIA_ADDED(1029), - @UiEvent(doc = "A new media control was added for media cast from the device") CAST_MEDIA_ADDED(1030), - @UiEvent(doc = "A new media control was added for media playing remotely") REMOTE_MEDIA_ADDED(1031), - @UiEvent(doc = "The media for an existing control was transferred to local playback") TRANSFER_TO_LOCAL(1032), - @UiEvent(doc = "The media for an existing control was transferred to a cast device") TRANSFER_TO_CAST(1033), - @UiEvent(doc = "The media for an existing control was transferred to a remote device") TRANSFER_TO_REMOTE(1034), - - @UiEvent(doc = "A new resumable media control was added") - RESUME_MEDIA_ADDED(1013), - + @UiEvent(doc = "A new resumable media control was added") RESUME_MEDIA_ADDED(1013), @UiEvent(doc = "An existing active media control was converted into resumable media") ACTIVE_TO_RESUME(1014), - - @UiEvent(doc = "A media control timed out") - MEDIA_TIMEOUT(1015), - - @UiEvent(doc = "A media control was removed from the carousel") - MEDIA_REMOVED(1016), - - @UiEvent(doc = "User swiped to another control within the media carousel") - CAROUSEL_PAGE(1017), - - @UiEvent(doc = "The user swiped away the media carousel") - DISMISS_SWIPE(1018), - - @UiEvent(doc = "The user long pressed on a media control") - OPEN_LONG_PRESS(1019), - + @UiEvent(doc = "A media control timed out") MEDIA_TIMEOUT(1015), + @UiEvent(doc = "A media control was removed from the carousel") MEDIA_REMOVED(1016), + @UiEvent(doc = "User swiped to another control within the media carousel") CAROUSEL_PAGE(1017), + @UiEvent(doc = "The user swiped away the media carousel") DISMISS_SWIPE(1018), + @UiEvent(doc = "The user long pressed on a media control") OPEN_LONG_PRESS(1019), @UiEvent(doc = "The user dismissed a media control via its long press menu") DISMISS_LONG_PRESS(1020), - @UiEvent(doc = "The user opened media settings from a media control's long press menu") OPEN_SETTINGS_LONG_PRESS(1021), - @UiEvent(doc = "The user opened media settings from the media carousel") OPEN_SETTINGS_CAROUSEL(1022), - @UiEvent(doc = "The play/pause button on a media control was tapped") TAP_ACTION_PLAY_PAUSE(1023), - - @UiEvent(doc = "The previous button on a media control was tapped") - TAP_ACTION_PREV(1024), - - @UiEvent(doc = "The next button on a media control was tapped") - TAP_ACTION_NEXT(1025), - + @UiEvent(doc = "The previous button on a media control was tapped") TAP_ACTION_PREV(1024), + @UiEvent(doc = "The next button on a media control was tapped") TAP_ACTION_NEXT(1025), @UiEvent(doc = "A custom or generic action button on a media control was tapped") TAP_ACTION_OTHER(1026), - - @UiEvent(doc = "The user seeked on a media control using the seekbar") - ACTION_SEEK(1027), - + @UiEvent(doc = "The user seeked on a media control using the seekbar") ACTION_SEEK(1027), @UiEvent(doc = "The user opened the output switcher from a media control") OPEN_OUTPUT_SWITCHER(1028), - - @UiEvent(doc = "The user tapped on a media control view") - MEDIA_TAP_CONTENT_VIEW(1036), - - @UiEvent(doc = "The media carousel moved to QQS") - MEDIA_CAROUSEL_LOCATION_QQS(1037), - - @UiEvent(doc = "THe media carousel moved to QS") - MEDIA_CAROUSEL_LOCATION_QS(1038), - + @UiEvent(doc = "The user tapped on a media control view") MEDIA_TAP_CONTENT_VIEW(1036), + @UiEvent(doc = "The media carousel moved to QQS") MEDIA_CAROUSEL_LOCATION_QQS(1037), + @UiEvent(doc = "THe media carousel moved to QS") MEDIA_CAROUSEL_LOCATION_QS(1038), @UiEvent(doc = "The media carousel moved to the lockscreen") MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039), - @UiEvent(doc = "The media carousel moved to the dream state") MEDIA_CAROUSEL_LOCATION_DREAM(1040), - @UiEvent(doc = "A media recommendation card was added to the media carousel") MEDIA_RECOMMENDATION_ADDED(1041), - @UiEvent(doc = "A media recommendation card was removed from the media carousel") MEDIA_RECOMMENDATION_REMOVED(1042), - @UiEvent(doc = "An existing media control was made active as a recommendation") MEDIA_RECOMMENDATION_ACTIVATED(1043), - @UiEvent(doc = "User tapped on an item in a media recommendation card") MEDIA_RECOMMENDATION_ITEM_TAP(1044), - @UiEvent(doc = "User tapped on a media recommendation card") MEDIA_RECOMMENDATION_CARD_TAP(1045), - @UiEvent(doc = "User opened the broadcast dialog from a media control") MEDIA_OPEN_BROADCAST_DIALOG(1079); override fun getId() = metricId -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/SmallHash.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/SmallHash.java index de7aac609955..97483a61baa4 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SmallHash.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/SmallHash.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.util; import java.util.Objects; diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index a8a84331050d..3e5d337bff9d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -17,14 +17,13 @@ package com.android.systemui.media.dagger; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.log.LogBuffer; import com.android.systemui.log.dagger.MediaTttReceiverLogBuffer; import com.android.systemui.log.dagger.MediaTttSenderLogBuffer; -import com.android.systemui.media.MediaDataManager; -import com.android.systemui.media.MediaFlags; -import com.android.systemui.media.MediaHierarchyManager; -import com.android.systemui.media.MediaHost; -import com.android.systemui.media.MediaHostStatesManager; +import com.android.systemui.media.controls.pipeline.MediaDataManager; +import com.android.systemui.media.controls.ui.MediaHierarchyManager; +import com.android.systemui.media.controls.ui.MediaHost; +import com.android.systemui.media.controls.ui.MediaHostStatesManager; +import com.android.systemui.media.controls.util.MediaFlags; import com.android.systemui.media.dream.dagger.MediaComplicationComponent; import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli; import com.android.systemui.media.nearby.NearbyMediaDevicesManager; @@ -33,6 +32,7 @@ import com.android.systemui.media.taptotransfer.MediaTttFlags; import com.android.systemui.media.taptotransfer.common.MediaTttLogger; import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger; import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger; +import com.android.systemui.plugins.log.LogBuffer; import java.util.Optional; diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java index 65c5bc76f3c5..69b5698b9042 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java @@ -21,9 +21,9 @@ import static com.android.systemui.media.dream.dagger.MediaComplicationComponent import android.widget.FrameLayout; -import com.android.systemui.media.MediaHierarchyManager; -import com.android.systemui.media.MediaHost; -import com.android.systemui.media.MediaHostState; +import com.android.systemui.media.controls.ui.MediaHierarchyManager; +import com.android.systemui.media.controls.ui.MediaHost; +import com.android.systemui.media.controls.ui.MediaHostState; import com.android.systemui.util.ViewController; import javax.inject.Inject; diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java index 91e7b4933096..20e8ae6719f3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java +++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java @@ -27,9 +27,9 @@ import com.android.systemui.CoreStartable; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dreams.complication.DreamMediaEntryComplication; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.media.MediaData; -import com.android.systemui.media.MediaDataManager; -import com.android.systemui.media.SmartspaceMediaData; +import com.android.systemui.media.controls.models.player.MediaData; +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData; +import com.android.systemui.media.controls.pipeline.MediaDataManager; import javax.inject.Inject; diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt index ffcc1f75f077..e26089450c21 100644 --- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt @@ -21,7 +21,7 @@ import com.android.settingslib.media.DeviceIconUtil import com.android.settingslib.media.LocalMediaManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.media.MediaFlags +import com.android.systemui.media.controls.util.MediaFlags import java.util.concurrent.Executor import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt index 78f4e012da03..5ace3ea8a05b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt @@ -1,9 +1,9 @@ package com.android.systemui.media.muteawait import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaMuteAwaitLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** Log messages for [MediaMuteAwaitConnectionManager]. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt index 46b2cc141b3c..78408fce5a36 100644 --- a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt @@ -1,9 +1,9 @@ package com.android.systemui.media.nearby import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NearbyMediaDevicesLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** Log messages for [NearbyMediaDevicesManager]. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt index b565f3c22f24..120f7d673881 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt @@ -16,8 +16,8 @@ package com.android.systemui.media.taptotransfer.common -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.temporarydisplay.TemporaryViewLogger /** @@ -43,6 +43,21 @@ class MediaTttLogger( ) } + /** + * Logs an error in trying to update to [displayState]. + * + * [displayState] is either a [android.app.StatusBarManager.MediaTransferSenderState] or + * a [android.app.StatusBarManager.MediaTransferReceiverState]. + */ + fun logStateChangeError(displayState: Int) { + buffer.log( + tag, + LogLevel.ERROR, + { int1 = displayState }, + { "Cannot display state=$int1; aborting" } + ) + } + /** Logs that we couldn't find information for [packageName]. */ fun logPackageNotFound(packageName: String) { buffer.log( diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt index c3de94f28aea..0a6043793ef6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt @@ -21,6 +21,8 @@ import android.content.pm.PackageManager import android.graphics.drawable.Drawable import com.android.settingslib.Utils import com.android.systemui.R +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon /** Utility methods for media tap-to-transfer. */ class MediaTttUtils { @@ -31,6 +33,23 @@ class MediaTttUtils { const val WAKE_REASON = "MEDIA_TRANSFER_ACTIVATED" /** + * Returns the information needed to display the icon in [Icon] form. + * + * See [getIconInfoFromPackageName]. + */ + fun getIconFromPackageName( + context: Context, + appPackageName: String?, + logger: MediaTttLogger, + ): Icon { + val iconInfo = getIconInfoFromPackageName(context, appPackageName, logger) + return Icon.Loaded( + iconInfo.drawable, + ContentDescription.Loaded(iconInfo.contentDescription) + ) + } + + /** * Returns the information needed to display the icon. * * The information will either contain app name and icon of the app playing media, or a diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index 089625ca8d9c..dc794e66b918 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -25,7 +25,6 @@ import android.graphics.drawable.Icon import android.media.MediaRoute2Info import android.os.Handler import android.os.PowerManager -import android.util.Log import android.view.Gravity import android.view.View import android.view.ViewGroup @@ -116,7 +115,7 @@ class MediaTttChipControllerReceiver @Inject constructor( logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName) if (chipState == null) { - Log.e(RECEIVER_TAG, "Unhandled MediaTransferReceiverState $displayState") + logger.logStateChangeError(displayState) return } uiEventLogger.logReceiverStateChange(chipState) @@ -236,5 +235,3 @@ data class ChipReceiverInfo( ) : TemporaryViewInfo { override fun getTimeoutMs() = DEFAULT_TIMEOUT_MILLIS } - -private const val RECEIVER_TAG = "MediaTapToTransferRcvr" diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt index c24b0307fcd1..6e596ee1f473 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt @@ -18,17 +18,12 @@ package com.android.systemui.media.taptotransfer.sender import android.app.StatusBarManager import android.content.Context -import android.media.MediaRoute2Info import android.util.Log -import android.view.View import androidx.annotation.StringRes import com.android.internal.logging.UiEventLogger -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R -import com.android.systemui.plugins.FalsingManager +import com.android.systemui.common.shared.model.Text import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS -import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo -import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator /** * A class enumerating all the possible states of the media tap-to-transfer chip on the sender @@ -38,6 +33,7 @@ import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator * @property stringResId the res ID of the string that should be displayed in the chip. Null if the * state should not have the chip be displayed. * @property transferStatus the transfer status that the chip state represents. + * @property endItem the item that should be displayed in the end section of the chip. * @property timeout the amount of time this chip should display on the screen before it times out * and disappears. */ @@ -46,6 +42,7 @@ enum class ChipStateSender( val uiEvent: UiEventLogger.UiEventEnum, @StringRes val stringResId: Int?, val transferStatus: TransferStatus, + val endItem: SenderEndItem?, val timeout: Long = DEFAULT_TIMEOUT_MILLIS ) { /** @@ -58,6 +55,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST, R.string.media_move_closer_to_start_cast, transferStatus = TransferStatus.NOT_STARTED, + endItem = null, ), /** @@ -71,6 +69,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST, R.string.media_move_closer_to_end_cast, transferStatus = TransferStatus.NOT_STARTED, + endItem = null, ), /** @@ -82,6 +81,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED, R.string.media_transfer_playing_different_device, transferStatus = TransferStatus.IN_PROGRESS, + endItem = SenderEndItem.Loading, timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS ), @@ -94,6 +94,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED, R.string.media_transfer_playing_this_device, transferStatus = TransferStatus.IN_PROGRESS, + endItem = SenderEndItem.Loading, timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS ), @@ -105,36 +106,13 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED, R.string.media_transfer_playing_different_device, transferStatus = TransferStatus.SUCCEEDED, - ) { - override fun undoClickListener( - chipbarCoordinator: ChipbarCoordinator, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger, - falsingManager: FalsingManager, - ): View.OnClickListener? { - if (undoCallback == null) { - return null - } - return View.OnClickListener { - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener - - uiEventLogger.logUndoClicked( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED - ) - undoCallback.onUndoTriggered() - // The external service should eventually send us a TransferToThisDeviceTriggered - // state, but that may take too long to go through the binder and the user may be - // confused as to why the UI hasn't changed yet. So, we immediately change the UI - // here. - chipbarCoordinator.displayView( - ChipSenderInfo( - TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo, undoCallback - ) - ) - } - } - }, + endItem = SenderEndItem.UndoButton( + uiEventOnClick = + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED, + newState = + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED + ), + ), /** * A state representing that a transfer back to this device has been successfully completed. @@ -144,36 +122,13 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, R.string.media_transfer_playing_this_device, transferStatus = TransferStatus.SUCCEEDED, - ) { - override fun undoClickListener( - chipbarCoordinator: ChipbarCoordinator, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger, - falsingManager: FalsingManager, - ): View.OnClickListener? { - if (undoCallback == null) { - return null - } - return View.OnClickListener { - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener - - uiEventLogger.logUndoClicked( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED - ) - undoCallback.onUndoTriggered() - // The external service should eventually send us a TransferToReceiverTriggered - // state, but that may take too long to go through the binder and the user may be - // confused as to why the UI hasn't changed yet. So, we immediately change the UI - // here. - chipbarCoordinator.displayView( - ChipSenderInfo( - TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo, undoCallback - ) - ) - } - } - }, + endItem = SenderEndItem.UndoButton( + uiEventOnClick = + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED, + newState = + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED + ), + ), /** A state representing that a transfer to the receiver device has failed. */ TRANSFER_TO_RECEIVER_FAILED( @@ -181,6 +136,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED, R.string.media_transfer_failed, transferStatus = TransferStatus.FAILED, + endItem = SenderEndItem.Error, ), /** A state representing that a transfer back to this device has failed. */ @@ -189,6 +145,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED, R.string.media_transfer_failed, transferStatus = TransferStatus.FAILED, + endItem = SenderEndItem.Error, ), /** A state representing that this device is far away from any receiver device. */ @@ -197,37 +154,27 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER, stringResId = null, transferStatus = TransferStatus.TOO_FAR, - ); + // We shouldn't be displaying the chipbar anyway + endItem = null, + ) { + override fun getChipTextString(context: Context, otherDeviceName: String): Text { + // TODO(b/245610654): Better way to handle this. + throw IllegalArgumentException("FAR_FROM_RECEIVER should never be displayed, " + + "so its string should never be fetched") + } + }; /** * Returns a fully-formed string with the text that the chip should display. * + * Throws an NPE if [stringResId] is null. + * * @param otherDeviceName the name of the other device involved in the transfer. */ - fun getChipTextString(context: Context, otherDeviceName: String): String? { - if (stringResId == null) { - return null - } - return context.getString(stringResId, otherDeviceName) + open fun getChipTextString(context: Context, otherDeviceName: String): Text { + return Text.Loaded(context.getString(stringResId!!, otherDeviceName)) } - /** - * Returns a click listener for the undo button on the chip. Returns null if this chip state - * doesn't have an undo button. - * - * @param chipbarCoordinator passed as a parameter in case we want to display a new chipbar - * when undo is clicked. - * @param undoCallback if present, the callback that should be called when the user clicks the - * undo button. The undo button will only be shown if this is non-null. - */ - open fun undoClickListener( - chipbarCoordinator: ChipbarCoordinator, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger, - falsingManager: FalsingManager, - ): View.OnClickListener? = null - companion object { /** * Returns the sender state enum associated with the given [displayState] from @@ -253,6 +200,26 @@ enum class ChipStateSender( } } +/** Represents the item that should be displayed in the end section of the chip. */ +sealed class SenderEndItem { + /** A loading icon should be displayed. */ + object Loading : SenderEndItem() + + /** An error icon should be displayed. */ + object Error : SenderEndItem() + + /** + * An undo button should be displayed. + * + * @property uiEventOnClick the UI event to log when this button is clicked. + * @property newState the state that should immediately be transitioned to. + */ + data class UndoButton( + val uiEventOnClick: UiEventLogger.UiEventEnum, + @StatusBarManager.MediaTransferSenderState val newState: Int, + ) : SenderEndItem() +} + // Give the Transfer*Triggered states a longer timeout since those states represent an active // process and we should keep the user informed about it as long as possible (but don't allow it to // continue indefinitely). diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt index 5aaab14c1065..1fa8faeecd82 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -19,16 +19,20 @@ package com.android.systemui.media.taptotransfer.sender import android.app.StatusBarManager import android.content.Context import android.media.MediaRoute2Info -import android.util.Log +import android.view.View +import com.android.internal.logging.UiEventLogger import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.CoreStartable +import com.android.systemui.R +import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.media.taptotransfer.common.MediaTttLogger +import com.android.systemui.media.taptotransfer.common.MediaTttUtils import com.android.systemui.statusbar.CommandQueue -import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator -import com.android.systemui.temporarydisplay.chipbar.SENDER_TAG +import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem +import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo import javax.inject.Inject /** @@ -47,6 +51,8 @@ constructor( private val uiEventLogger: MediaTttSenderUiEventLogger, ) : CoreStartable { + private var displayedState: ChipStateSender? = null + private val commandQueueCallbacks = object : CommandQueue.Callbacks { override fun updateMediaTapToTransferSenderDisplay( @@ -78,15 +84,117 @@ constructor( logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName) if (chipState == null) { - Log.e(SENDER_TAG, "Unhandled MediaTransferSenderState $displayState") + logger.logStateChangeError(displayState) return } uiEventLogger.logSenderStateChange(chipState) if (chipState == ChipStateSender.FAR_FROM_RECEIVER) { - chipbarCoordinator.removeView(removalReason = ChipStateSender.FAR_FROM_RECEIVER.name) + // Return early if we're not displaying a chip anyway + val currentDisplayedState = displayedState ?: return + + val removalReason = ChipStateSender.FAR_FROM_RECEIVER.name + if ( + currentDisplayedState.transferStatus == TransferStatus.IN_PROGRESS || + currentDisplayedState.transferStatus == TransferStatus.SUCCEEDED + ) { + // Don't remove the chip if we're in progress or succeeded, since the user should + // still be able to see the status of the transfer. + logger.logRemovalBypass( + removalReason, + bypassReason = "transferStatus=${currentDisplayedState.transferStatus.name}" + ) + return + } + + displayedState = null + chipbarCoordinator.removeView(removalReason) } else { - chipbarCoordinator.displayView(ChipSenderInfo(chipState, routeInfo, undoCallback)) + displayedState = chipState + chipbarCoordinator.displayView( + createChipbarInfo( + chipState, + routeInfo, + undoCallback, + context, + logger, + ) + ) } } + + /** + * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display. + */ + private fun createChipbarInfo( + chipStateSender: ChipStateSender, + routeInfo: MediaRoute2Info, + undoCallback: IUndoMediaTransferCallback?, + context: Context, + logger: MediaTttLogger, + ): ChipbarInfo { + val packageName = routeInfo.clientPackageName + val otherDeviceName = routeInfo.name.toString() + + return ChipbarInfo( + // Display the app's icon as the start icon + startIcon = MediaTttUtils.getIconFromPackageName(context, packageName, logger), + text = chipStateSender.getChipTextString(context, otherDeviceName), + endItem = + when (chipStateSender.endItem) { + null -> null + is SenderEndItem.Loading -> ChipbarEndItem.Loading + is SenderEndItem.Error -> ChipbarEndItem.Error + is SenderEndItem.UndoButton -> { + if (undoCallback != null) { + getUndoButton( + undoCallback, + chipStateSender.endItem.uiEventOnClick, + chipStateSender.endItem.newState, + routeInfo, + ) + } else { + null + } + } + }, + vibrationEffect = chipStateSender.transferStatus.vibrationEffect, + ) + } + + /** + * Returns an undo button for the chip. + * + * When the button is clicked: [undoCallback] will be triggered, [uiEvent] will be logged, and + * this coordinator will transition to [newState]. + */ + private fun getUndoButton( + undoCallback: IUndoMediaTransferCallback, + uiEvent: UiEventLogger.UiEventEnum, + @StatusBarManager.MediaTransferSenderState newState: Int, + routeInfo: MediaRoute2Info, + ): ChipbarEndItem.Button { + val onClickListener = + View.OnClickListener { + uiEventLogger.logUndoClicked(uiEvent) + undoCallback.onUndoTriggered() + + // The external service should eventually send us a new TransferTriggered state, but + // but that may take too long to go through the binder and the user may be confused + // as to why the UI hasn't changed yet. So, we immediately change the UI here. + updateMediaTapToTransferSenderDisplay( + newState, + routeInfo, + // Since we're force-updating the UI, we don't have any [undoCallback] from the + // external service (and TransferTriggered states don't have undo callbacks + // anyway). + undoCallback = null, + ) + } + + return ChipbarEndItem.Button( + Text.Resource(R.string.media_transfer_undo), + onClickListener, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt index f15720df5245..b96380976dec 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt @@ -16,16 +16,36 @@ package com.android.systemui.media.taptotransfer.sender -/** Represents the different possible transfer states that we could be in. */ -enum class TransferStatus { +import android.os.VibrationEffect + +/** + * Represents the different possible transfer states that we could be in and the vibration effects + * that come with updating transfer states. + * + * @property vibrationEffect an optional vibration effect when the transfer status is changed. + */ +enum class TransferStatus( + val vibrationEffect: VibrationEffect? = null, +) { /** The transfer hasn't started yet. */ - NOT_STARTED, + NOT_STARTED( + vibrationEffect = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1.0f, 0) + .compose() + ), /** The transfer is currently ongoing but hasn't completed yet. */ - IN_PROGRESS, + IN_PROGRESS( + vibrationEffect = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 0) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.7f, 70) + .compose(), + ), /** The transfer has completed successfully. */ SUCCEEDED, /** The transfer has completed with a failure. */ - FAILED, + FAILED(vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)), /** The device is too far away to do a transfer. */ TOO_FAR, } diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt index 1ea93474f954..03503fd1ff61 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt @@ -17,10 +17,10 @@ package com.android.systemui.privacy.logging import android.permission.PermissionGroupUsage -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.PrivacyLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogMessage import com.android.systemui.privacy.PrivacyDialog import com.android.systemui.privacy.PrivacyItem import java.text.SimpleDateFormat diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 482a1397642b..bb2b4419a80a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -52,6 +52,7 @@ import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator +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 @@ -98,10 +99,10 @@ interface FgsManagerController { fun init() /** - * Show the foreground services dialog. The dialog will be expanded from [viewLaunchedFrom] if + * Show the foreground services dialog. The dialog will be expanded from [expandable] if * it's not `null`. */ - fun showDialog(viewLaunchedFrom: View?) + fun showDialog(expandable: Expandable?) /** Add a [OnNumberOfPackagesChangedListener]. */ fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) @@ -367,7 +368,7 @@ class FgsManagerControllerImpl @Inject constructor( override fun shouldUpdateFooterVisibility() = dialog == null - override fun showDialog(viewLaunchedFrom: View?) { + override fun showDialog(expandable: Expandable?) { synchronized(lock) { if (dialog == null) { @@ -403,16 +404,18 @@ class FgsManagerControllerImpl @Inject constructor( } mainExecutor.execute { - viewLaunchedFrom - ?.let { - dialogLaunchAnimator.showFromView( - dialog, it, - cuj = DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG - ) + val controller = + expandable?.dialogLaunchController( + DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG, ) - } ?: dialog.show() + ) + if (controller != null) { + dialogLaunchAnimator.show(dialog, controller) + } else { + dialog.show() + } } backgroundExecutor.execute { diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt index 9d64781ef2e9..a9943e886339 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt @@ -32,6 +32,7 @@ import com.android.internal.logging.nano.MetricsProto import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.globalactions.GlobalActionsDialogLite import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager @@ -156,7 +157,7 @@ internal class FooterActionsController @Inject constructor( startSettingsActivity() } else if (v === powerMenuLite) { uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS) - globalActionsDialog?.showOrHideDialog(false, true, v) + globalActionsDialog?.showOrHideDialog(false, true, Expandable.fromView(powerMenuLite)) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java index 7511278e0919..b1b9dd721eaf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java @@ -29,6 +29,7 @@ import android.widget.TextView; import androidx.annotation.Nullable; import com.android.systemui.R; +import com.android.systemui.animation.Expandable; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.qs.dagger.QSScope; @@ -130,7 +131,7 @@ public class QSFgsManagerFooter implements View.OnClickListener, @Override public void onClick(View view) { - mFgsManagerController.showDialog(mRootView); + mFgsManagerController.showDialog(Expandable.fromView(view)); } public void refreshState() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 0fe3d1699de0..20f1a8ed7689 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -49,7 +49,7 @@ 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.media.controls.ui.MediaHost; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.qs.QSContainerController; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt index 8544f61d7031..c663aa605ca2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.qs -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.QSFragmentDisableLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.DisableFlagsLogger import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index f6db775a7749..abc0adecbfeb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -29,9 +29,9 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.media.MediaHierarchyManager; -import com.android.systemui.media.MediaHost; -import com.android.systemui.media.MediaHostState; +import com.android.systemui.media.controls.ui.MediaHierarchyManager; +import com.android.systemui.media.controls.ui.MediaHost; +import com.android.systemui.media.controls.ui.MediaHostState; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSScope; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index 2727c83ad877..2a80de0e24de 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -32,7 +32,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.Dumpable; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaHost; +import com.android.systemui.media.controls.ui.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTileView; import com.android.systemui.qs.customize.QSCustomizerController; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java index 67bf3003deff..6c1e95645550 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java @@ -39,6 +39,7 @@ import androidx.annotation.Nullable; import com.android.internal.util.FrameworkStatsLog; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.animation.Expandable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.common.shared.model.Icon; import com.android.systemui.dagger.qualifiers.Background; @@ -169,7 +170,7 @@ public class QSSecurityFooter extends ViewController<View> // TODO(b/242040009): Remove this. public void showDeviceMonitoringDialog() { - mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, mView); + mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, Expandable.fromView(mView)); } public void refreshState() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java index ae6ed2008a77..67bc76998597 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java @@ -75,6 +75,7 @@ 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.animation.Expandable; import com.android.systemui.common.shared.model.ContentDescription; import com.android.systemui.common.shared.model.Icon; import com.android.systemui.dagger.SysUISingleton; @@ -190,8 +191,9 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { } /** Show the device monitoring dialog. */ - public void showDeviceMonitoringDialog(Context quickSettingsContext, @Nullable View view) { - createDialog(quickSettingsContext, view); + public void showDeviceMonitoringDialog(Context quickSettingsContext, + @Nullable Expandable expandable) { + createDialog(quickSettingsContext, expandable); } /** @@ -440,7 +442,7 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { } } - private void createDialog(Context quickSettingsContext, @Nullable View view) { + private void createDialog(Context quickSettingsContext, @Nullable Expandable expandable) { mShouldUseSettingsButton.set(false); mBgHandler.post(() -> { String settingsButtonText = getSettingsButton(); @@ -453,9 +455,12 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { ? 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)); + DialogLaunchAnimator.Controller controller = + expandable != null ? expandable.dialogLaunchController(new DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)) + : null; + if (controller != null) { + mDialogLaunchAnimator.show(mDialog, controller); } else { mDialog.show(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index ac46c85c10a4..f37d66877069 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -34,10 +34,12 @@ import com.android.internal.logging.InstanceId; import com.android.internal.logging.InstanceIdSequence; import com.android.internal.logging.UiEventLogger; import com.android.systemui.Dumpable; +import com.android.systemui.ProtoDumpable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.dump.nano.SystemUIProtoDump; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.qs.QSTile; @@ -48,6 +50,7 @@ import com.android.systemui.qs.external.TileLifecycleManager; import com.android.systemui.qs.external.TileServiceKey; import com.android.systemui.qs.external.TileServiceRequestController; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.nano.QsTileState; import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.plugins.PluginManager; @@ -59,16 +62,20 @@ import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.leak.GarbageMonitor; import com.android.systemui.util.settings.SecureSettings; +import org.jetbrains.annotations.NotNull; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Provider; @@ -82,7 +89,7 @@ import javax.inject.Provider; * This class also provides the interface for adding/removing/changing tiles. */ @SysUISingleton -public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, Dumpable { +public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable { private static final String TAG = "QSTileHost"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int MAX_QS_INSTANCE_ID = 1 << 20; @@ -671,4 +678,15 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D mTiles.values().stream().filter(obj -> obj instanceof Dumpable) .forEach(o -> ((Dumpable) o).dump(pw, args)); } + + @Override + public void dumpProto(@NotNull SystemUIProtoDump systemUIProtoDump, @NotNull String[] args) { + List<QsTileState> data = mTiles.values().stream() + .map(QSTile::getState) + .map(TileStateToProtoKt::toProto) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + systemUIProtoDump.tiles = data.toArray(new QsTileState[0]); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java index 9739974256f6..6aabe3b1ced1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java @@ -26,8 +26,8 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaHierarchyManager; -import com.android.systemui.media.MediaHost; +import com.android.systemui.media.controls.ui.MediaHierarchyManager; +import com.android.systemui.media.controls.ui.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSScope; diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt b/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt new file mode 100644 index 000000000000..2c8a5a4981d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt @@ -0,0 +1,51 @@ +/* + * 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.service.quicksettings.Tile +import android.text.TextUtils +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.external.CustomTile +import com.android.systemui.qs.nano.QsTileState +import com.android.systemui.util.nano.ComponentNameProto + +fun QSTile.State.toProto(): QsTileState? { + if (TextUtils.isEmpty(spec)) return null + val state = QsTileState() + if (spec.startsWith(CustomTile.PREFIX)) { + val protoComponentName = ComponentNameProto() + val tileComponentName = CustomTile.getComponentFromSpec(spec) + protoComponentName.packageName = tileComponentName.packageName + protoComponentName.className = tileComponentName.className + state.componentName = protoComponentName + } else { + state.spec = spec + } + state.state = + when (this.state) { + Tile.STATE_UNAVAILABLE -> QsTileState.UNAVAILABLE + Tile.STATE_INACTIVE -> QsTileState.INACTIVE + Tile.STATE_ACTIVE -> QsTileState.ACTIVE + else -> QsTileState.UNAVAILABLE + } + label?.let { state.label = it.toString() } + secondaryLabel?.let { state.secondaryLabel = it.toString() } + if (this is QSTile.BooleanState) { + state.booleanState = value + } + return state +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java index 4cacbbacec2f..5d03da3cc113 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java @@ -35,6 +35,7 @@ import androidx.annotation.Nullable; import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.qs.QSTileHost; import com.android.systemui.settings.UserTracker; @@ -53,6 +54,7 @@ import javax.inject.Provider; /** * Runs the day-to-day operations of which tiles should be bound and when. */ +@SysUISingleton public class TileServices extends IQSService.Stub { static final int DEFAULT_MAX_BOUND = 3; static final int REDUCED_MAX_BOUND = 1; 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 index cf9b41c25388..9ba3501c3434 100644 --- 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 @@ -23,13 +23,11 @@ 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 @@ -74,37 +72,27 @@ interface FooterActionsInteractor { 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. + * Show the device monitoring dialog, expanded from [expandable] if it's not null. * * 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) + fun showDeviceMonitoringDialog(quickSettingsContext: Context, expandable: Expandable?) /** Show the foreground services dialog. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showForegroundServicesDialog(view: View) + fun showForegroundServicesDialog(expandable: Expandable) /** Show the power menu dialog. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) + fun showPowerMenuDialog( + globalActionsDialogLite: GlobalActionsDialogLite, + expandable: Expandable, + ) /** Show the settings. */ fun showSettings(expandable: Expandable) /** Show the user switcher. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showUserSwitcher(view: View) + fun showUserSwitcher(context: Context, expandable: Expandable) } @SysUISingleton @@ -147,28 +135,32 @@ constructor( 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 showDeviceMonitoringDialog( + quickSettingsContext: Context, + expandable: Expandable?, + ) { + qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, expandable) + if (expandable != null) { + DevicePolicyEventLogger.createEvent( + FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED + ) + .write() + } } - override fun showForegroundServicesDialog(view: View) { - fgsManagerController.showDialog(view) + override fun showForegroundServicesDialog(expandable: Expandable) { + fgsManagerController.showDialog(expandable) } - override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) { + override fun showPowerMenuDialog( + globalActionsDialogLite: GlobalActionsDialogLite, + expandable: Expandable, + ) { uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS) globalActionsDialogLite.showOrHideDialog( /* keyguardShowing= */ false, /* isDeviceProvisioned= */ true, - view, + expandable, ) } @@ -189,21 +181,21 @@ constructor( ) } - override fun showUserSwitcher(view: View) { + override fun showUserSwitcher(context: Context, expandable: Expandable) { if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { - userSwitchDialogController.showDialog(view) + userSwitchDialogController.showDialog(context, expandable) return } val intent = - Intent(view.context, UserSwitcherActivity::class.java).apply { + Intent(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), + expandable.activityLaunchController(), true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM, ) 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 index dd1ffcc9fa12..3e39c8ee62f1 100644 --- 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 @@ -31,6 +31,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.R +import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.people.ui.view.PeopleViewBinder.bind @@ -125,7 +126,7 @@ object FooterActionsViewBinder { launch { viewModel.security.collect { security -> if (previousSecurity != security) { - bindSecurity(securityHolder, security) + bindSecurity(view.context, securityHolder, security) previousSecurity = security } } @@ -159,6 +160,7 @@ object FooterActionsViewBinder { } private fun bindSecurity( + quickSettingsContext: Context, securityHolder: TextButtonViewHolder, security: FooterActionsSecurityButtonViewModel?, ) { @@ -171,9 +173,12 @@ object FooterActionsViewBinder { // 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) { + val onClick = security.onClick + if (onClick != null) { securityView.isClickable = true - securityView.setOnClickListener(security.onClick) + securityView.setOnClickListener { + onClick(quickSettingsContext, Expandable.fromView(securityView)) + } chevron.isVisible = true } else { securityView.isClickable = false @@ -205,7 +210,9 @@ object FooterActionsViewBinder { foregroundServicesWithNumberView.isVisible = false foregroundServicesWithTextView.isVisible = true - foregroundServicesWithTextView.setOnClickListener(foregroundServices.onClick) + foregroundServicesWithTextView.setOnClickListener { + foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView)) + } foregroundServicesWithTextHolder.text.text = foregroundServices.text foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges } else { @@ -213,7 +220,9 @@ object FooterActionsViewBinder { foregroundServicesWithTextView.isVisible = false foregroundServicesWithNumberView.visibility = View.VISIBLE - foregroundServicesWithNumberView.setOnClickListener(foregroundServices.onClick) + foregroundServicesWithNumberView.setOnClickListener { + foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView)) + } foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString() foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges @@ -229,7 +238,7 @@ object FooterActionsViewBinder { } buttonView.setBackgroundResource(model.background) - buttonView.setOnClickListener(model.onClick) + buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) } val icon = model.icon val iconView = button.icon 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 index 9b5f683d8dab..8d819dacba67 100644 --- 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 @@ -17,7 +17,7 @@ package com.android.systemui.qs.footer.ui.viewmodel import android.annotation.DrawableRes -import android.view.View +import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon /** @@ -29,7 +29,5 @@ data class FooterActionsButtonViewModel( val icon: Icon, val iconTint: Int?, @DrawableRes val background: Int, - // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog - // or activity. - val onClick: (View) -> Unit, + val onClick: (Expandable) -> 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 index 98b53cb0ed5a..ff8130d3e6ec 100644 --- 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 @@ -16,7 +16,7 @@ package com.android.systemui.qs.footer.ui.viewmodel -import android.view.View +import com.android.systemui.animation.Expandable /** A ViewModel for the foreground services button. */ data class FooterActionsForegroundServicesButtonViewModel( @@ -24,5 +24,5 @@ data class FooterActionsForegroundServicesButtonViewModel( val text: String, val displayText: Boolean, val hasNewChanges: Boolean, - val onClick: (View) -> Unit, + val onClick: (Expandable) -> 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 index 98ab129fc9de..3450505f9f86 100644 --- 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 @@ -16,12 +16,13 @@ package com.android.systemui.qs.footer.ui.viewmodel -import android.view.View +import android.content.Context +import com.android.systemui.animation.Expandable 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)?, + val onClick: ((quickSettingsContext: Context, Expandable) -> 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 index d3c06f60bc90..dee6fadbc9cb 100644 --- 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 @@ -18,7 +18,6 @@ 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 @@ -199,50 +198,51 @@ class FooterActionsViewModel( */ suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) { footerActionsInteractor.deviceMonitoringDialogRequests.collect { - footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext) + footerActionsInteractor.showDeviceMonitoringDialog( + quickSettingsContext, + expandable = null, + ) } } - private fun onSecurityButtonClicked(view: View) { + private fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showDeviceMonitoringDialog(view) + footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable) } - private fun onForegroundServiceButtonClicked(view: View) { + private fun onForegroundServiceButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showForegroundServicesDialog(view) + footerActionsInteractor.showForegroundServicesDialog(expandable) } - private fun onUserSwitcherClicked(view: View) { + private fun onUserSwitcherClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showUserSwitcher(view) + footerActionsInteractor.showUserSwitcher(context, expandable) } - // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog - // or activity. - private fun onSettingsButtonClicked(view: View) { + private fun onSettingsButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showSettings(Expandable.fromView(view)) + footerActionsInteractor.showSettings(expandable) } - private fun onPowerButtonClicked(view: View) { + private fun onPowerButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, view) + footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, expandable) } private fun userSwitcherButton( diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt index 60380064e098..931dc8df151a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt @@ -17,12 +17,12 @@ package com.android.systemui.qs.logging import android.service.quicksettings.Tile -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.VERBOSE -import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.QSLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogMessage import com.android.systemui.plugins.qs.QSTile import com.android.systemui.statusbar.StatusBarState import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto new file mode 100644 index 000000000000..2a61033cb302 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto @@ -0,0 +1,47 @@ +/* + * 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. + */ + +syntax = "proto3"; + +package com.android.systemui.qs; + +import "frameworks/base/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto"; + +option java_multiple_files = true; + +message QsTileState { + oneof identifier { + string spec = 1; + com.android.systemui.util.ComponentNameProto component_name = 2; + } + + enum State { + UNAVAILABLE = 0; + INACTIVE = 1; + ACTIVE = 2; + } + + State state = 3; + oneof optional_boolean_state { + bool boolean_state = 4; + } + oneof optional_label { + string label = 5; + } + oneof optional_secondary_label { + string secondary_label = 6; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index d2d5063c7ae0..57a00c9a1620 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -26,6 +26,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.logging.MetricsLogger; @@ -43,6 +44,9 @@ import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.user.data.source.UserRecord; +import java.util.List; +import java.util.stream.Collectors; + import javax.inject.Inject; /** @@ -83,6 +87,13 @@ public class UserDetailView extends PseudoGridView { private final FalsingManager mFalsingManager; private @Nullable UserSwitchDialogController.DialogShower mDialogShower; + @NonNull + @Override + protected List<UserRecord> getUsers() { + return super.getUsers().stream().filter( + userRecord -> !userRecord.isManageUsers).collect(Collectors.toList()); + } + @Inject public Adapter(Context context, UserSwitcherController controller, UiEventLogger uiEventLogger, FalsingManager falsingManager) { @@ -193,6 +204,15 @@ public class UserDetailView extends PseudoGridView { Trace.endSection(); } + @Override + public void onUserListItemClicked(@NonNull UserRecord record, + @Nullable UserSwitchDialogController.DialogShower dialogShower) { + if (dialogShower != null) { + mDialogShower.dismiss(); + } + super.onUserListItemClicked(record, dialogShower); + } + public void linkToViewGroup(ViewGroup viewGroup) { PseudoGridView.ViewGroupAdapterBridge.link(viewGroup, this); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt index bdcc6b0b2a57..314252bf310b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt @@ -23,13 +23,13 @@ import android.content.DialogInterface.BUTTON_NEUTRAL import android.content.Intent import android.provider.Settings import android.view.LayoutInflater -import android.view.View import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger import com.android.systemui.R import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager @@ -77,10 +77,10 @@ class UserSwitchDialogController @VisibleForTesting constructor( * Show a [UserDialog]. * * Populate the dialog with information from and adapter obtained from - * [userDetailViewAdapterProvider] and show it as launched from [view]. + * [userDetailViewAdapterProvider] and show it as launched from [expandable]. */ - fun showDialog(view: View) { - with(dialogFactory(view.context)) { + fun showDialog(context: Context, expandable: Expandable) { + with(dialogFactory(context)) { setShowForAllUsers(true) setCanceledOnTouchOutside(true) @@ -112,13 +112,19 @@ class UserSwitchDialogController @VisibleForTesting constructor( adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid)) - dialogLaunchAnimator.showFromView( - this, view, - cuj = DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG + val controller = + expandable.dialogLaunchController( + DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) ) - ) + if (controller != null) { + dialogLaunchAnimator.show( + this, + controller, + ) + } else { + show() + } + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN) adapter.injectDialogShower(DialogShowerImpl(this, dialogLaunchAnimator)) } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 231e415f17c6..d524a356a323 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -20,6 +20,7 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; +import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS; @@ -634,6 +635,11 @@ public class ScreenshotController { return true; } }); + + if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { + mScreenshotView.badgeScreenshot( + mContext.getPackageManager().getUserBadgeForDensity(owner, 0)); + } mScreenshotView.setScreenshot(mScreenBitmap, screenInsets); if (DEBUG_WINDOW) { Log.d(TAG, "setContentView: " + mScreenshotView); @@ -1038,7 +1044,7 @@ public class ScreenshotController { private boolean isUserSetupComplete(UserHandle owner) { return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0) - .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; + .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 26cbcbf5214f..27331ae7a389 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -74,7 +74,6 @@ import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; import android.view.accessibility.AccessibilityManager; -import android.view.animation.AccelerateInterpolator; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.FrameLayout; @@ -122,15 +121,9 @@ public class ScreenshotView extends FrameLayout implements private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234; private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100; - private static final long SCREENSHOT_DISMISS_X_DURATION_MS = 350; - private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 350; - private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f; - private static final float ROUNDED_CORNER_RADIUS = .25f; private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe - private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator(); - private final Resources mResources; private final Interpolator mFastOutSlowIn; private final DisplayMetrics mDisplayMetrics; @@ -145,6 +138,7 @@ public class ScreenshotView extends FrameLayout implements private ImageView mScrollingScrim; private DraggableConstraintLayout mScreenshotStatic; private ImageView mScreenshotPreview; + private ImageView mScreenshotBadge; private View mScreenshotPreviewBorder; private ImageView mScrollablePreview; private ImageView mScreenshotFlash; @@ -355,6 +349,7 @@ public class ScreenshotView extends FrameLayout implements mScreenshotPreviewBorder = requireNonNull( findViewById(R.id.screenshot_preview_border)); mScreenshotPreview.setClipToOutline(true); + mScreenshotBadge = requireNonNull(findViewById(R.id.screenshot_badge)); mActionsContainerBackground = requireNonNull(findViewById( R.id.actions_container_background)); @@ -595,8 +590,11 @@ public class ScreenshotView extends FrameLayout implements ValueAnimator borderFadeIn = ValueAnimator.ofFloat(0, 1); borderFadeIn.setDuration(100); - borderFadeIn.addUpdateListener((animation) -> - mScreenshotPreviewBorder.setAlpha(animation.getAnimatedFraction())); + borderFadeIn.addUpdateListener((animation) -> { + float borderAlpha = animation.getAnimatedFraction(); + mScreenshotPreviewBorder.setAlpha(borderAlpha); + mScreenshotBadge.setAlpha(borderAlpha); + }); if (showFlash) { dropInAnimation.play(flashOutAnimator).after(flashInAnimator); @@ -763,11 +761,18 @@ public class ScreenshotView extends FrameLayout implements return animator; } + void badgeScreenshot(Drawable badge) { + mScreenshotBadge.setImageDrawable(badge); + mScreenshotBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE); + } + void setChipIntents(ScreenshotController.SavedImageData imageData) { mShareChip.setOnClickListener(v -> { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName); if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) { - mActionExecutor.launchIntentAsync(ActionIntentCreator.INSTANCE.createShareIntent( + prepareSharedTransition(); + mActionExecutor.launchIntentAsync( + ActionIntentCreator.INSTANCE.createShareIntent( imageData.uri, imageData.subject), imageData.shareTransition.get().bundle, imageData.owner.getIdentifier(), false); @@ -778,6 +783,7 @@ public class ScreenshotView extends FrameLayout implements mEditChip.setOnClickListener(v -> { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName); if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) { + prepareSharedTransition(); mActionExecutor.launchIntentAsync( ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext), imageData.editTransition.get().bundle, @@ -789,6 +795,7 @@ public class ScreenshotView extends FrameLayout implements mScreenshotPreview.setOnClickListener(v -> { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName); if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) { + prepareSharedTransition(); mActionExecutor.launchIntentAsync( ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext), imageData.editTransition.get().bundle, @@ -1023,6 +1030,9 @@ public class ScreenshotView extends FrameLayout implements mScreenshotPreview.setVisibility(View.INVISIBLE); mScreenshotPreview.setAlpha(1f); mScreenshotPreviewBorder.setAlpha(0); + mScreenshotBadge.setAlpha(0f); + mScreenshotBadge.setVisibility(View.GONE); + mScreenshotBadge.setImageDrawable(null); mPendingSharedTransition = false; mActionsContainerBackground.setVisibility(View.GONE); mActionsContainer.setVisibility(View.GONE); @@ -1064,6 +1074,12 @@ public class ScreenshotView extends FrameLayout implements } } + private void prepareSharedTransition() { + mPendingSharedTransition = true; + // fade out non-preview UI + createScreenshotFadeDismissAnimation().start(); + } + ValueAnimator createScreenshotFadeDismissAnimation() { ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); alphaAnim.addUpdateListener(animation -> { @@ -1072,6 +1088,7 @@ public class ScreenshotView extends FrameLayout implements mActionsContainerBackground.setAlpha(alpha); mActionsContainer.setAlpha(alpha); mScreenshotPreviewBorder.setAlpha(alpha); + mScreenshotBadge.setAlpha(alpha); }); alphaAnim.setDuration(600); return alphaAnim; diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java index bbba0071094b..b36f0d7bacfc 100644 --- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java @@ -33,13 +33,13 @@ import android.view.animation.DecelerateInterpolator; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; +import com.android.systemui.statusbar.notification.stack.StackStateAnimator; /** * Drawable used on SysUI scrims. */ public class ScrimDrawable extends Drawable { private static final String TAG = "ScrimDrawable"; - private static final long COLOR_ANIMATION_DURATION = 2000; private final Paint mPaint; private int mAlpha = 255; @@ -76,7 +76,7 @@ public class ScrimDrawable extends Drawable { final int mainFrom = mMainColor; ValueAnimator anim = ValueAnimator.ofFloat(0, 1); - anim.setDuration(COLOR_ANIMATION_DURATION); + anim.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); anim.addUpdateListener(animation -> { float ratio = (float) animation.getAnimatedValue(); mMainColor = ColorUtils.blendARGB(mainFrom, mainColor, ratio); diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java index 6e9f859c202b..d5a395436271 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java @@ -20,6 +20,7 @@ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.app.Activity; +import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.view.Gravity; @@ -36,6 +37,8 @@ import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; +import java.util.List; + import javax.inject.Inject; /** A dialog that provides controls for adjusting the screen brightness. */ @@ -83,6 +86,15 @@ public class BrightnessDialog extends Activity { lp.leftMargin = horizontalMargin; lp.rightMargin = horizontalMargin; frame.setLayoutParams(lp); + Rect bounds = new Rect(); + frame.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + // Exclude this view (and its horizontal margins) from triggering gestures. + // This prevents back gesture from being triggered by dragging close to the + // edge of the slider (0% or 100%). + bounds.set(-horizontalMargin, 0, right - left + horizontalMargin, bottom - top); + v.setSystemGestureExclusionRects(List.of(bounds)); + }); BrightnessSliderController controller = mToggleSliderFactory.create(this, frame); controller.init(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt index a494f42985ac..6b540aa9f392 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt @@ -292,6 +292,7 @@ class LargeScreenShadeHeaderController @Inject constructor( clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ -> val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f v.pivotX = newPivot + v.pivotY = v.height.toFloat() / 2 } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt index 07e8b9fe3123..754036d3baa9 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt @@ -16,7 +16,7 @@ package com.android.systemui.shade import android.view.MotionEvent import com.android.systemui.dump.DumpsysTableLogger import com.android.systemui.dump.Row -import com.android.systemui.util.collection.RingBuffer +import com.android.systemui.plugins.util.RingBuffer import java.text.SimpleDateFormat import java.util.Locale diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index a49b7f03acc6..ddb57f74cacf 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -79,7 +79,10 @@ import android.os.UserManager; import android.os.VibrationEffect; import android.provider.Settings; import android.transition.ChangeBounds; +import android.transition.Transition; import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.transition.TransitionValues; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; @@ -144,10 +147,12 @@ import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; -import com.android.systemui.media.KeyguardMediaController; -import com.android.systemui.media.MediaDataManager; -import com.android.systemui.media.MediaHierarchyManager; +import com.android.systemui.media.controls.pipeline.MediaDataManager; +import com.android.systemui.media.controls.ui.KeyguardMediaController; +import com.android.systemui.media.controls.ui.MediaHierarchyManager; import com.android.systemui.model.SysUiState; +import com.android.systemui.navigationbar.NavigationBarController; +import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.FalsingManager.FalsingTapListener; @@ -580,6 +585,7 @@ public final class NotificationPanelViewController { private final SysUiState mSysUiState; private final NotificationShadeDepthController mDepthController; + private final NavigationBarController mNavigationBarController; private final int mDisplayId; private KeyguardIndicationController mKeyguardIndicationController; @@ -689,6 +695,7 @@ public final class NotificationPanelViewController { private int mScreenCornerRadius; private boolean mQSAnimatingHiddenFromCollapsed; private boolean mUseLargeScreenShadeHeader; + private boolean mEnableQsClipping; private int mQsClipTop; private int mQsClipBottom; @@ -857,6 +864,7 @@ public final class NotificationPanelViewController { PrivacyDotViewController privacyDotViewController, TapAgainViewController tapAgainViewController, NavigationModeController navigationModeController, + NavigationBarController navigationBarController, FragmentService fragmentService, ContentResolver contentResolver, RecordingController recordingController, @@ -950,6 +958,7 @@ public final class NotificationPanelViewController { mNotificationsQSContainerController = notificationsQSContainerController; mNotificationListContainer = notificationListContainer; mNotificationStackSizeCalculator = notificationStackSizeCalculator; + mNavigationBarController = navigationBarController; mKeyguardBottomAreaViewControllerProvider = keyguardBottomAreaViewControllerProvider; mNotificationsQSContainerController.init(); mNotificationStackScrollLayoutController = notificationStackScrollLayoutController; @@ -1298,6 +1307,8 @@ public final class NotificationPanelViewController { mSplitShadeFullTransitionDistance = mResources.getDimensionPixelSize(R.dimen.split_shade_full_transition_distance); + + mEnableQsClipping = mResources.getBoolean(R.bool.qs_enable_clipping); } private void onSplitShadeEnabledChanged() { @@ -1437,6 +1448,16 @@ public final class NotificationPanelViewController { mMaxAllowedKeyguardNotifications = maxAllowed; } + @VisibleForTesting + boolean getClosing() { + return mClosing; + } + + @VisibleForTesting + boolean getIsFlinging() { + return mIsFlinging; + } + private void updateMaxDisplayedNotifications(boolean recompute) { if (recompute) { setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1)); @@ -1664,9 +1685,40 @@ public final class NotificationPanelViewController { // horizontally properly. transition.excludeTarget(R.id.status_view_media_container, true); } + transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); - TransitionManager.beginDelayedTransition(mNotificationContainerParent, transition); + + boolean customClockAnimation = + mKeyguardStatusViewController.getClockAnimations() != null + && mKeyguardStatusViewController.getClockAnimations() + .getHasCustomPositionUpdatedAnimation(); + + if (mFeatureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION) && customClockAnimation) { + // Find the clock, so we can exclude it from this transition. + FrameLayout clockContainerView = + mView.findViewById(R.id.lockscreen_clock_view_large); + View clockView = clockContainerView.getChildAt(0); + + transition.excludeTarget(clockView, /* exclude= */ true); + + TransitionSet set = new TransitionSet(); + set.addTransition(transition); + + SplitShadeTransitionAdapter adapter = + new SplitShadeTransitionAdapter(mKeyguardStatusViewController); + + // Use linear here, so the actual clock can pick its own interpolator. + adapter.setInterpolator(Interpolators.LINEAR); + adapter.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); + adapter.addTarget(clockView); + set.addTransition(adapter); + + TransitionManager.beginDelayedTransition(mNotificationContainerParent, set); + } else { + TransitionManager.beginDelayedTransition( + mNotificationContainerParent, transition); + } } constraintSet.applyTo(mNotificationContainerParent); @@ -2090,7 +2142,8 @@ public final class NotificationPanelViewController { animator.start(); } - private void onFlingEnd(boolean cancelled) { + @VisibleForTesting + void onFlingEnd(boolean cancelled) { mIsFlinging = false; // No overshoot when the animation ends setOverExpansionInternal(0, false /* isFromGesture */); @@ -2633,12 +2686,16 @@ public final class NotificationPanelViewController { mQsExpanded = expanded; updateQsState(); updateExpandedHeightToMaxHeight(); - mFalsingCollector.setQsExpanded(expanded); - mCentralSurfaces.setQsExpanded(expanded); - mNotificationsQSContainerController.setQsExpanded(expanded); - mPulseExpansionHandler.setQsExpanded(expanded); - mKeyguardBypassController.setQSExpanded(expanded); - mPrivacyDotViewController.setQsExpanded(expanded); + setStatusAccessibilityImportance(expanded + ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); + updateSystemUiStateFlags(); + NavigationBarView navigationBarView = + mNavigationBarController.getNavigationBarView(mDisplayId); + if (navigationBarView != null) { + navigationBarView.onStatusBarPanelStateChanged(); + } + mShadeExpansionStateManager.onQsExpansionChanged(expanded); } } @@ -2952,8 +3009,10 @@ public final class NotificationPanelViewController { mQsTranslationForFullShadeTransition = qsTranslation; updateQsFrameTranslation(); float currentTranslation = mQsFrame.getTranslationY(); - mQsClipTop = (int) (top - currentTranslation - mQsFrame.getTop()); - mQsClipBottom = (int) (bottom - currentTranslation - mQsFrame.getTop()); + mQsClipTop = mEnableQsClipping + ? (int) (top - currentTranslation - mQsFrame.getTop()) : 0; + mQsClipBottom = mEnableQsClipping + ? (int) (bottom - currentTranslation - mQsFrame.getTop()) : 0; mQsVisible = qsVisible; mQs.setQsVisible(mQsVisible); mQs.setFancyClipping( @@ -3678,6 +3737,11 @@ public final class NotificationPanelViewController { setListening(true); } + @VisibleForTesting + void setTouchSlopExceeded(boolean isTouchSlopExceeded) { + mTouchSlopExceeded = isTouchSlopExceeded; + } + public void setOverExpansion(float overExpansion) { if (overExpansion == mOverExpansion) { return; @@ -3829,12 +3893,14 @@ public final class NotificationPanelViewController { } } - private void setIsClosing(boolean isClosing) { + @VisibleForTesting + void setIsClosing(boolean isClosing) { boolean wasClosing = isClosing(); mClosing = isClosing; if (wasClosing != isClosing) { mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing); } + mAmbientState.setIsClosing(isClosing); } private void updateDozingVisibilities(boolean animate) { @@ -3864,12 +3930,16 @@ public final class NotificationPanelViewController { switch (mBarState) { case KEYGUARD: if (!mDozingOnDown) { - if (mUpdateMonitor.isFaceEnrolled() - && !mUpdateMonitor.isFaceDetectionRunning() - && !mUpdateMonitor.getUserCanSkipBouncer( - KeyguardUpdateMonitor.getCurrentUser())) { - mUpdateMonitor.requestFaceAuth(true, - FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED); + mShadeLog.v("onMiddleClicked on Keyguard, mDozingOnDown: false"); + // Try triggering face auth, this "might" run. Check + // KeyguardUpdateMonitor#shouldListenForFace to see when face auth won't run. + boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true, + FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED); + + if (didFaceAuthRun) { + mUpdateMonitor.requestActiveUnlock( + ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT, + "lockScreenEmptySpaceTap"); } else { mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT, 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); @@ -3877,11 +3947,6 @@ public final class NotificationPanelViewController { .log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT); startUnlockHintAnimation(); } - if (mUpdateMonitor.isFaceEnrolled()) { - mUpdateMonitor.requestActiveUnlock( - ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT, - "lockScreenEmptySpaceTap"); - } } return true; case StatusBarState.SHADE_LOCKED: @@ -4158,8 +4223,8 @@ public final class NotificationPanelViewController { /** * Sets the dozing state. * - * @param dozing {@code true} when dozing. - * @param animate if transition should be animated. + * @param dozing {@code true} when dozing. + * @param animate if transition should be animated. */ public void setDozing(boolean dozing, boolean animate) { if (dozing == mDozing) return; @@ -4299,35 +4364,35 @@ public final class NotificationPanelViewController { /** * Starts fold to AOD animation. * - * @param startAction invoked when the animation starts. - * @param endAction invoked when the animation finishes, also if it was cancelled. + * @param startAction invoked when the animation starts. + * @param endAction invoked when the animation finishes, also if it was cancelled. * @param cancelAction invoked when the animation is cancelled, before endAction. */ public void startFoldToAodAnimation(Runnable startAction, Runnable endAction, Runnable cancelAction) { mView.animate() - .translationX(0) - .alpha(1f) - .setDuration(ANIMATION_DURATION_FOLD_TO_AOD) - .setInterpolator(EMPHASIZED_DECELERATE) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - startAction.run(); - } + .translationX(0) + .alpha(1f) + .setDuration(ANIMATION_DURATION_FOLD_TO_AOD) + .setInterpolator(EMPHASIZED_DECELERATE) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + startAction.run(); + } - @Override - public void onAnimationCancel(Animator animation) { - cancelAction.run(); - } + @Override + public void onAnimationCancel(Animator animation) { + cancelAction.run(); + } - @Override - public void onAnimationEnd(Animator animation) { - endAction.run(); - } - }).setUpdateListener(anim -> { - mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction()); - }).start(); + @Override + public void onAnimationEnd(Animator animation) { + endAction.run(); + } + }).setUpdateListener(anim -> { + mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction()); + }).start(); } /** @@ -4625,14 +4690,16 @@ public final class NotificationPanelViewController { Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args)); } - private void notifyExpandingStarted() { + @VisibleForTesting + void notifyExpandingStarted() { if (!mExpanding) { mExpanding = true; onExpandingStarted(); } } - private void notifyExpandingFinished() { + @VisibleForTesting + void notifyExpandingFinished() { endClosing(); if (mExpanding) { mExpanding = false; @@ -4685,8 +4752,10 @@ public final class NotificationPanelViewController { /** * Maybe vibrate as panel is opened. * - * @param openingWithTouch Whether the panel is being opened with touch. If the panel is instead - * being opened programmatically (such as by the open panel gesture), we always play haptic. + * @param openingWithTouch Whether the panel is being opened with touch. If the panel is + * instead + * being opened programmatically (such as by the open panel gesture), we + * always play haptic. */ private void maybeVibrateOnOpening(boolean openingWithTouch) { if (mVibrateOnOpening) { @@ -4732,6 +4801,7 @@ public final class NotificationPanelViewController { mAmbientState.setSwipingUp(false); if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop || Math.abs(y - mInitialExpandY) > mTouchSlop + || (!isFullyExpanded() && !isFullyCollapsed()) || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) { mVelocityTracker.computeCurrentVelocity(1000); float vel = mVelocityTracker.getYVelocity(); @@ -4851,10 +4921,12 @@ public final class NotificationPanelViewController { 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; @@ -4902,7 +4974,7 @@ public final class NotificationPanelViewController { if (isNaN(h)) { Log.wtf(TAG, "ExpandedHeight set to NaN"); } - mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> { + mNotificationShadeWindowController.batchApplyWindowLayoutParams(() -> { if (mExpandLatencyTracking && h != 0f) { DejankUtils.postAfterTraversal( () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL)); @@ -5093,7 +5165,7 @@ public final class NotificationPanelViewController { /** * Create an animator that can also overshoot * - * @param targetHeight the target height + * @param targetHeight the target height * @param overshootAmount the amount of overshoot desired */ private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) { @@ -5129,7 +5201,8 @@ public final class NotificationPanelViewController { */ public void updatePanelExpansionAndVisibility() { mShadeExpansionStateManager.onPanelExpansionChanged( - mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx); + mExpandedFraction, isExpanded(), + mTracking, mExpansionDragDownAmountPx); updateVisibility(); } @@ -5890,7 +5963,7 @@ public final class NotificationPanelViewController { public final class TouchHandler implements View.OnTouchListener { private long mLastTouchDownTime = -1L; - /** @see ViewGroup#onInterceptTouchEvent(MotionEvent) */ + /** @see ViewGroup#onInterceptTouchEvent(MotionEvent) */ public boolean onInterceptTouchEvent(MotionEvent event) { if (SPEW_LOGCAT) { Log.v(TAG, @@ -6089,7 +6162,7 @@ public final class NotificationPanelViewController { mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding"); return false; } - if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) { + if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) { mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled"); return false; } @@ -6246,4 +6319,54 @@ public final class NotificationPanelViewController { loadDimens(); } } + + static class SplitShadeTransitionAdapter extends Transition { + private static final String PROP_BOUNDS = "splitShadeTransitionAdapter:bounds"; + private static final String[] TRANSITION_PROPERTIES = { PROP_BOUNDS }; + + private final KeyguardStatusViewController mController; + + SplitShadeTransitionAdapter(KeyguardStatusViewController controller) { + mController = controller; + } + + private void captureValues(TransitionValues transitionValues) { + Rect boundsRect = new Rect(); + boundsRect.left = transitionValues.view.getLeft(); + boundsRect.top = transitionValues.view.getTop(); + boundsRect.right = transitionValues.view.getRight(); + boundsRect.bottom = transitionValues.view.getBottom(); + transitionValues.values.put(PROP_BOUNDS, boundsRect); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + ValueAnimator anim = ValueAnimator.ofFloat(0, 1); + + Rect from = (Rect) startValues.values.get(PROP_BOUNDS); + Rect to = (Rect) endValues.values.get(PROP_BOUNDS); + + anim.addUpdateListener( + animation -> mController.getClockAnimations().onPositionUpdated( + from, to, animation.getAnimatedFraction())); + + return anim; + } + + @Override + public String[] getTransitionProperties() { + return TRANSITION_PROPERTIES; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 1d9210592b78..66a22f4ddc0d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -135,7 +135,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW DumpManager dumpManager, KeyguardStateController keyguardStateController, ScreenOffAnimationController screenOffAnimationController, - AuthController authController) { + AuthController authController, + ShadeExpansionStateManager shadeExpansionStateManager) { mContext = context; mWindowManager = windowManager; mActivityManager = activityManager; @@ -156,6 +157,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW .addCallback(mStateListener, SysuiStatusBarStateController.RANK_STATUS_BAR_WINDOW_CONTROLLER); configurationController.addCallback(this); + shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged); float desiredPreferredRefreshRate = context.getResources() .getInteger(R.integer.config_keyguardRefreshRate); @@ -607,8 +609,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW apply(mCurrentState); } - @Override - public void setQsExpanded(boolean expanded) { + private void onQsExpansionChanged(Boolean expanded) { mCurrentState.mQsExpanded = expanded; apply(mCurrentState); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index d6f0de83ecc1..73c6d507f035 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -36,17 +36,12 @@ class NotificationsQSContainerController @Inject constructor( private val navigationModeController: NavigationModeController, private val overviewProxyService: OverviewProxyService, private val largeScreenShadeHeaderController: LargeScreenShadeHeaderController, + private val shadeExpansionStateManager: ShadeExpansionStateManager, private val featureFlags: FeatureFlags, @Main private val delayableExecutor: DelayableExecutor ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController { - var qsExpanded = false - set(value) { - if (field != value) { - field = value - mView.invalidate() - } - } + private var qsExpanded = false private var splitShadeEnabled = false private var isQSDetailShowing = false private var isQSCustomizing = false @@ -71,6 +66,13 @@ class NotificationsQSContainerController @Inject constructor( taskbarVisible = visible } } + private val shadeQsExpansionListener: ShadeQsExpansionListener = + ShadeQsExpansionListener { isQsExpanded -> + if (qsExpanded != isQsExpanded) { + qsExpanded = isQsExpanded + mView.invalidate() + } + } // With certain configuration changes (like light/dark changes), the nav bar will disappear // for a bit, causing `bottomStableInsets` to be unstable for some time. Debounce the value @@ -106,6 +108,7 @@ class NotificationsQSContainerController @Inject constructor( public override fun onViewAttached() { updateResources() overviewProxyService.addCallback(taskbarVisibilityListener) + shadeExpansionStateManager.addQsExpansionListener(shadeQsExpansionListener) mView.setInsetsChangedListener(delayedInsetSetter) mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) } mView.setConfigurationChangedListener { updateResources() } @@ -113,6 +116,7 @@ class NotificationsQSContainerController @Inject constructor( override fun onViewDetached() { overviewProxyService.removeCallback(taskbarVisibilityListener) + shadeExpansionStateManager.removeQsExpansionListener(shadeQsExpansionListener) mView.removeOnInsetsChangedListener() mView.removeQSFragmentAttachedListener() mView.setConfigurationChangedListener(null) diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt index f617d471351e..7bba74a8b125 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt @@ -21,6 +21,7 @@ import android.util.Log import androidx.annotation.FloatRange import com.android.systemui.dagger.SysUISingleton import com.android.systemui.util.Compile +import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject /** @@ -31,12 +32,14 @@ import javax.inject.Inject @SysUISingleton class ShadeExpansionStateManager @Inject constructor() { - private val expansionListeners = mutableListOf<ShadeExpansionListener>() - private val stateListeners = mutableListOf<ShadeStateListener>() + private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>() + private val qsExpansionListeners = CopyOnWriteArrayList<ShadeQsExpansionListener>() + private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>() @PanelState private var state: Int = STATE_CLOSED @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f private var expanded: Boolean = false + private var qsExpanded: Boolean = false private var tracking: Boolean = false private var dragDownPxAmount: Float = 0f @@ -57,6 +60,15 @@ class ShadeExpansionStateManager @Inject constructor() { expansionListeners.remove(listener) } + fun addQsExpansionListener(listener: ShadeQsExpansionListener) { + qsExpansionListeners.add(listener) + listener.onQsExpansionChanged(qsExpanded) + } + + fun removeQsExpansionListener(listener: ShadeQsExpansionListener) { + qsExpansionListeners.remove(listener) + } + /** Adds a listener that will be notified when the panel state has changed. */ fun addStateListener(listener: ShadeStateListener) { stateListeners.add(listener) @@ -126,6 +138,14 @@ class ShadeExpansionStateManager @Inject constructor() { expansionListeners.forEach { it.onPanelExpansionChanged(expansionChangeEvent) } } + /** Called when the quick settings expansion changes to fully expanded or collapsed. */ + fun onQsExpansionChanged(qsExpanded: Boolean) { + this.qsExpanded = qsExpanded + + debugLog("qsExpanded=$qsExpanded") + qsExpansionListeners.forEach { it.onQsExpansionChanged(qsExpanded) } + } + /** Updates the panel state if necessary. */ fun updateState(@PanelState state: Int) { debugLog( diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt index 7bee0ba17afc..2b788d85a14c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt @@ -1,10 +1,10 @@ package com.android.systemui.shade import android.view.MotionEvent -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.ShadeLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogMessage import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject @@ -12,64 +12,69 @@ private const val TAG = "systemui.shade" /** Lightweight logging utility for the Shade. */ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { - fun v(@CompileTimeConstant msg: String) { - buffer.log(TAG, LogLevel.VERBOSE, msg) - } + fun v(@CompileTimeConstant msg: String) { + buffer.log(TAG, LogLevel.VERBOSE, msg) + } - private inline fun log( - logLevel: LogLevel, - initializer: LogMessage.() -> Unit, - noinline printer: LogMessage.() -> String - ) { - buffer.log(TAG, logLevel, initializer, printer) - } + private inline fun log( + logLevel: LogLevel, + initializer: LogMessage.() -> Unit, + noinline printer: LogMessage.() -> String + ) { + buffer.log(TAG, logLevel, initializer, printer) + } - fun onQsInterceptMoveQsTrackingEnabled(h: Float) { - log( - LogLevel.VERBOSE, - { double1 = h.toDouble() }, - { "onQsIntercept: move action, QS tracking enabled. h = $double1" }) - } + fun onQsInterceptMoveQsTrackingEnabled(h: Float) { + log( + LogLevel.VERBOSE, + { double1 = h.toDouble() }, + { "onQsIntercept: move action, QS tracking enabled. h = $double1" } + ) + } - fun logQsTrackingNotStarted( - initialTouchY: Float, - y: Float, - h: Float, - touchSlop: Float, - qsExpanded: Boolean, - collapsedOnDown: Boolean, - keyguardShowing: Boolean, - qsExpansionEnabled: Boolean - ) { - log( - LogLevel.VERBOSE, - { - int1 = initialTouchY.toInt() - int2 = y.toInt() - long1 = h.toLong() - double1 = touchSlop.toDouble() - bool1 = qsExpanded - bool2 = collapsedOnDown - bool3 = keyguardShowing - bool4 = qsExpansionEnabled - }, - { - "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded=" + - "$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4" - }) - } + fun logQsTrackingNotStarted( + initialTouchY: Float, + y: Float, + h: Float, + touchSlop: Float, + qsExpanded: Boolean, + collapsedOnDown: Boolean, + keyguardShowing: Boolean, + qsExpansionEnabled: Boolean + ) { + log( + LogLevel.VERBOSE, + { + int1 = initialTouchY.toInt() + int2 = y.toInt() + long1 = h.toLong() + double1 = touchSlop.toDouble() + bool1 = qsExpanded + bool2 = collapsedOnDown + bool3 = keyguardShowing + bool4 = qsExpansionEnabled + }, + { + "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded" + + "=$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4" + } + ) + } - fun logMotionEvent(event: MotionEvent, message: String) { - log( - LogLevel.VERBOSE, - { - str1 = message - long1 = event.eventTime - long2 = event.downTime - int1 = event.action - int2 = event.classification - double1 = event.y.toDouble() - }, - { "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,classification=$int2" }) - } + fun logMotionEvent(event: MotionEvent, message: String) { + log( + LogLevel.VERBOSE, + { + str1 = message + long1 = event.eventTime + long2 = event.downTime + int1 = event.action + int2 = event.classification + double1 = event.y.toDouble() + }, + { + "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,class=$int2" + } + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt new file mode 100644 index 000000000000..14882b9afd2f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt @@ -0,0 +1,25 @@ +/* + * 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.shade + +/** A listener interface to be notified of expansion events for the quick settings panel. */ +fun interface ShadeQsExpansionListener { + /** + * Invoked whenever the quick settings expansion changes, when it is fully collapsed or expanded + */ + fun onQsExpansionChanged(isQsExpanded: Boolean) +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt new file mode 100644 index 000000000000..09019a69df47 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt @@ -0,0 +1,62 @@ +/* + * 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.shade.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.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.ShadeExpansionListener +import com.android.systemui.shade.ShadeExpansionStateManager +import com.android.systemui.shade.domain.model.ShadeModel +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +/** Business logic for shade interactions */ +@SysUISingleton +class ShadeRepository @Inject constructor(shadeExpansionStateManager: ShadeExpansionStateManager) { + + val shadeModel: Flow<ShadeModel> = + conflatedCallbackFlow { + val callback = + object : ShadeExpansionListener { + override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) { + // Don't propagate ShadeExpansionChangeEvent.dragDownPxAmount field. + // It is too noisy and produces extra events that consumers won't care + // about + val info = + ShadeModel( + expansionAmount = event.fraction, + isExpanded = event.expanded, + isUserDragging = event.tracking + ) + trySendWithFailureLogging(info, TAG, "updated shade expansion info") + } + } + + shadeExpansionStateManager.addExpansionListener(callback) + trySendWithFailureLogging(ShadeModel(), TAG, "initial shade expansion info") + + awaitClose { shadeExpansionStateManager.removeExpansionListener(callback) } + } + .distinctUntilChanged() + + companion object { + private const val TAG = "ShadeRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt new file mode 100644 index 000000000000..ce0f4283ff83 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.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.shade.domain.model + +import android.annotation.FloatRange + +/** Information about shade (NotificationPanel) expansion */ +data class ShadeModel( + /** 0 when collapsed, 1 when fully expanded. */ + @FloatRange(from = 0.0, to = 1.0) val expansionAmount: Float = 0f, + /** Whether the panel should be considered expanded */ + val isExpanded: Boolean = false, + /** Whether the user is actively dragging the panel. */ + val isUserDragging: Boolean = false, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt index 7f7ff9cf4881..90c52bd8c9f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt @@ -17,9 +17,9 @@ package com.android.systemui.statusbar import android.app.PendingIntent -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotifInteractionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.collection.NotificationEntry import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 04621168493b..e6d7e4124d01 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -69,12 +69,15 @@ import com.android.internal.statusbar.LetterboxDetails; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.util.GcUtils; import com.android.internal.view.AppearanceRegion; +import com.android.systemui.dump.DumpHandler; import com.android.systemui.statusbar.CommandQueue.Callbacks; import com.android.systemui.statusbar.commandline.CommandRegistry; import com.android.systemui.statusbar.policy.CallbackController; import com.android.systemui.tracing.ProtoTracer; +import java.io.FileDescriptor; import java.io.FileOutputStream; +import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; @@ -184,6 +187,7 @@ public class CommandQueue extends IStatusBar.Stub implements private int mLastUpdatedImeDisplayId = INVALID_DISPLAY; private ProtoTracer mProtoTracer; private final @Nullable CommandRegistry mRegistry; + private final @Nullable DumpHandler mDumpHandler; /** * These methods are called back on the main thread. @@ -473,12 +477,18 @@ public class CommandQueue extends IStatusBar.Stub implements } public CommandQueue(Context context) { - this(context, null, null); + this(context, null, null, null); } - public CommandQueue(Context context, ProtoTracer protoTracer, CommandRegistry registry) { + public CommandQueue( + Context context, + ProtoTracer protoTracer, + CommandRegistry registry, + DumpHandler dumpHandler + ) { mProtoTracer = protoTracer; mRegistry = registry; + mDumpHandler = dumpHandler; context.getSystemService(DisplayManager.class).registerDisplayListener(this, mHandler); // We always have default display. setDisabled(DEFAULT_DISPLAY, DISABLE_NONE, DISABLE2_NONE); @@ -1178,6 +1188,35 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override + public void dumpProto(String[] args, ParcelFileDescriptor pfd) { + final FileDescriptor fd = pfd.getFileDescriptor(); + // This is mimicking Binder#dumpAsync, but on this side of the binder. Might be possible + // to just throw this work onto the handler just like the other messages + Thread thr = new Thread("Sysui.dumpProto") { + public void run() { + try { + if (mDumpHandler == null) { + return; + } + // We won't be using the PrintWriter. + OutputStream o = new OutputStream() { + @Override + public void write(int b) {} + }; + mDumpHandler.dump(fd, new PrintWriter(o), args); + } finally { + try { + // Close the file descriptor so the TransferPipe finishes its thread + pfd.close(); + } catch (Exception e) { + } + } + } + }; + thr.start(); + } + + @Override public void runGcForTest() { // Gc sysui GcUtils.runGcAndFinalizersSync(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt index 886ad684649f..5fb500247697 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt @@ -5,7 +5,7 @@ import android.util.IndentingPrintWriter import android.util.MathUtils import com.android.systemui.R import com.android.systemui.dump.DumpManager -import com.android.systemui.media.MediaHierarchyManager +import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.shade.NotificationPanelViewController import com.android.systemui.statusbar.policy.ConfigurationController import dagger.assisted.Assisted diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index 80069319601f..a2e4536ce45f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -24,7 +24,7 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.WakefulnessLifecycle -import com.android.systemui.media.MediaHierarchyManager +import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QS diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index 4be5a1aa0215..ced725e0b1d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -48,9 +48,9 @@ import com.android.systemui.animation.Interpolators; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaData; -import com.android.systemui.media.MediaDataManager; -import com.android.systemui.media.SmartspaceMediaData; +import com.android.systemui.media.controls.models.player.MediaData; +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData; +import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.dagger.CentralSurfacesModule; import com.android.systemui.statusbar.notification.collection.NotifCollection; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java index 0c9e1ec1ff77..e21acb7e0f68 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java @@ -92,9 +92,6 @@ public interface NotificationShadeWindowController extends RemoteInputController /** Sets the state of whether the keyguard is fading away or not. */ default void setKeyguardFadingAway(boolean keyguardFadingAway) {} - /** Sets the state of whether the quick settings is expanded or not. */ - default void setQsExpanded(boolean expanded) {} - /** Sets the state of whether the user activities are forced or not. */ default void setForceUserActivity(boolean forceUserActivity) {} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index f96198450ed6..87ef92a28d5d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -40,6 +40,7 @@ import com.android.systemui.animation.Interpolators; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.statusbar.notification.NotificationUtils; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; @@ -110,8 +111,8 @@ public class NotificationShelf extends ActivatableNotificationView implements setClipChildren(false); setClipToPadding(false); mShelfIcons.setIsStaticLayout(false); - setBottomRoundness(1.0f, false /* animate */); - setTopRoundness(1f, false /* animate */); + requestBottomRoundness(1.0f, /* animate = */ false, SourceType.DefaultValue); + requestTopRoundness(1f, false, SourceType.DefaultValue); // Setting this to first in section to get the clipping to the top roundness correct. This // value determines the way we are clipping to the top roundness of the overall shade @@ -413,7 +414,7 @@ public class NotificationShelf extends ActivatableNotificationView implements if (iconState != null && iconState.clampedAppearAmount == 1.0f) { // only if the first icon is fully in the shelf we want to clip to it! backgroundTop = (int) (child.getTranslationY() - getTranslationY()); - firstElementRoundness = expandableRow.getCurrentTopRoundness(); + firstElementRoundness = expandableRow.getTopRoundness(); } } @@ -507,28 +508,36 @@ public class NotificationShelf extends ActivatableNotificationView implements // Round bottom corners within animation bounds final float changeFraction = MathUtils.saturate( (viewEnd - cornerAnimationTop) / cornerAnimationDistance); - anv.setBottomRoundness(anv.isLastInSection() ? 1f : changeFraction, - false /* animate */); + anv.requestBottomRoundness( + anv.isLastInSection() ? 1f : changeFraction, + /* animate = */ false, + SourceType.OnScroll); } else if (viewEnd < cornerAnimationTop) { // Fast scroll skips frames and leaves corners with unfinished rounding. // Reset top and bottom corners outside of animation bounds. - anv.setBottomRoundness(anv.isLastInSection() ? 1f : smallCornerRadius, - false /* animate */); + anv.requestBottomRoundness( + anv.isLastInSection() ? 1f : smallCornerRadius, + /* animate = */ false, + SourceType.OnScroll); } if (viewStart >= cornerAnimationTop) { // Round top corners within animation bounds final float changeFraction = MathUtils.saturate( (viewStart - cornerAnimationTop) / cornerAnimationDistance); - anv.setTopRoundness(anv.isFirstInSection() ? 1f : changeFraction, - false /* animate */); + anv.requestTopRoundness( + anv.isFirstInSection() ? 1f : changeFraction, + false, + SourceType.OnScroll); } else if (viewStart < cornerAnimationTop) { // Fast scroll skips frames and leaves corners with unfinished rounding. // Reset top and bottom corners outside of animation bounds. - anv.setTopRoundness(anv.isFirstInSection() ? 1f : smallCornerRadius, - false /* animate */); + anv.requestTopRoundness( + anv.isFirstInSection() ? 1f : smallCornerRadius, + false, + SourceType.OnScroll); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt index 8222c9d9ba59..c630feba1dcb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt @@ -39,6 +39,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView @@ -68,6 +69,7 @@ constructor( configurationController: ConfigurationController, private val statusBarStateController: StatusBarStateController, private val falsingManager: FalsingManager, + shadeExpansionStateManager: ShadeExpansionStateManager, private val lockscreenShadeTransitionController: LockscreenShadeTransitionController, private val falsingCollector: FalsingCollector, dumpManager: DumpManager @@ -126,6 +128,13 @@ constructor( initResources(context) } }) + + shadeExpansionStateManager.addQsExpansionListener { isQsExpanded -> + if (qsExpanded != isQsExpanded) { + qsExpanded = isQsExpanded + } + } + mPowerManager = context.getSystemService(PowerManager::class.java) dumpManager.registerDumpable(this) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java index b3dd853cd2e1..402217dac185 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java @@ -71,9 +71,9 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.log.LogBuffer; -import com.android.systemui.log.LogLevel; import com.android.systemui.log.dagger.StatusBarNetworkControllerLog; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.qs.tiles.dialog.InternetDialogFactory; import com.android.systemui.settings.CurrentUserTracker; import com.android.systemui.statusbar.policy.ConfigurationController; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index 11e3d1773c4c..eacb18e3c50c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -29,8 +29,9 @@ import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dump.DumpHandler; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaDataManager; +import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.carrier.QSCarrierGroupController; @@ -181,8 +182,10 @@ public interface CentralSurfacesDependenciesModule { static CommandQueue provideCommandQueue( Context context, ProtoTracer protoTracer, - CommandRegistry registry) { - return new CommandQueue(context, protoTracer, registry); + CommandRegistry registry, + DumpHandler dumpHandler + ) { + return new CommandQueue(context, protoTracer, registry, dumpHandler); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt index d88f07ca304c..737b4812d4fb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt @@ -25,11 +25,12 @@ import android.view.Gravity import android.view.View import android.widget.FrameLayout import com.android.internal.annotations.GuardedBy -import com.android.systemui.animation.Interpolators import com.android.systemui.R +import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.StatusBarState.SHADE import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener @@ -42,7 +43,6 @@ import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN import com.android.systemui.util.leak.RotationUtils.Rotation - import java.util.concurrent.Executor import javax.inject.Inject @@ -67,7 +67,8 @@ class PrivacyDotViewController @Inject constructor( private val stateController: StatusBarStateController, private val configurationController: ConfigurationController, private val contentInsetsProvider: StatusBarContentInsetsProvider, - private val animationScheduler: SystemStatusAnimationScheduler + private val animationScheduler: SystemStatusAnimationScheduler, + shadeExpansionStateManager: ShadeExpansionStateManager ) { private lateinit var tl: View private lateinit var tr: View @@ -128,6 +129,13 @@ class PrivacyDotViewController @Inject constructor( updateStatusBarState() } }) + + shadeExpansionStateManager.addQsExpansionListener { isQsExpanded -> + dlog("setQsExpanded $isQsExpanded") + synchronized(lock) { + nextViewState = nextViewState.copy(qsExpanded = isQsExpanded) + } + } } fun setUiExecutor(e: DelayableExecutor) { @@ -138,13 +146,6 @@ class PrivacyDotViewController @Inject constructor( showingListener = l } - fun setQsExpanded(expanded: Boolean) { - dlog("setQsExpanded $expanded") - synchronized(lock) { - nextViewState = nextViewState.copy(qsExpanded = expanded) - } - } - @UiThread fun setNewRotation(rot: Int) { dlog("updateRotation: $rot") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt index 17feaa842165..9bdff928c44b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.gesture -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.SwipeStatusBarAwayLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** Log messages for [SwipeStatusBarAwayGestureHandler]. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index 8f8813b80b5f..842204bbf621 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -118,6 +118,7 @@ class LockscreenSmartspaceController @Inject constructor( regionSamplingEnabled, updateFun ) + initializeTextColors(regionSamplingInstance) regionSamplingInstance.startRegionSampler() regionSamplingInstances.put(v, regionSamplingInstance) connectSession() @@ -361,18 +362,20 @@ class LockscreenSmartspaceController @Inject constructor( } } + private fun initializeTextColors(regionSamplingInstance: RegionSamplingInstance) { + val lightThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_LightWallpaper) + val darkColor = Utils.getColorAttrDefaultColor(lightThemeContext, R.attr.wallpaperTextColor) + + val darkThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI) + val lightColor = Utils.getColorAttrDefaultColor(darkThemeContext, R.attr.wallpaperTextColor) + + regionSamplingInstance.setForegroundColors(lightColor, darkColor) + } + private fun updateTextColorFromRegionSampler() { smartspaceViews.forEach { - val isRegionDark = regionSamplingInstances.getValue(it).currentRegionDarkness() - val themeID = if (isRegionDark.isDark) { - R.style.Theme_SystemUI - } else { - R.style.Theme_SystemUI_LightWallpaper - } - val themedContext = ContextThemeWrapper(context, themeID) - val wallpaperTextColor = - Utils.getColorAttrDefaultColor(themedContext, R.attr.wallpaperTextColor) - it.setPrimaryTextColor(wallpaperTextColor) + val textColor = regionSamplingInstances.getValue(it).currentForegroundColor() + it.setPrimaryTextColor(textColor) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java index 822840dfd86b..0a5e9867a17f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java @@ -290,7 +290,7 @@ public class InstantAppNotifier .setComponent(aiaComponent) .setAction(Intent.ACTION_VIEW) .addCategory(Intent.CATEGORY_BROWSABLE) - .addCategory("unique:" + System.currentTimeMillis()) + .setIdentifier("unique:" + System.currentTimeMillis()) .putExtra(Intent.EXTRA_PACKAGE_NAME, appInfo.packageName) .putExtra( Intent.EXTRA_VERSION_CODE, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt index 7fbdd35796c1..2734511de78c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt @@ -17,40 +17,27 @@ package com.android.systemui.statusbar.notification import android.content.Context -import android.util.Log -import android.widget.Toast import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.util.Compile import javax.inject.Inject class NotifPipelineFlags @Inject constructor( val context: Context, val featureFlags: FeatureFlags ) { - fun checkLegacyPipelineEnabled(): Boolean { - if (Compile.IS_DEBUG) { - Toast.makeText(context, "Old pipeline code running!", Toast.LENGTH_SHORT).show() - } - if (featureFlags.isEnabled(Flags.NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE)) { - throw RuntimeException("Old pipeline code running with new pipeline enabled") - } else { - Log.d("NotifPipeline", "Old pipeline code running with new pipeline enabled", - Exception()) - } - return false - } - fun isDevLoggingEnabled(): Boolean = featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING) - fun isSmartspaceDedupingEnabled(): Boolean = - featureFlags.isEnabled(Flags.SMARTSPACE) && - featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING) - - fun removeUnrankedNotifs(): Boolean = - featureFlags.isEnabled(Flags.REMOVE_UNRANKED_NOTIFICATIONS) + fun isSmartspaceDedupingEnabled(): Boolean = featureFlags.isEnabled(Flags.SMARTSPACE) fun fullScreenIntentRequiresKeyguard(): Boolean = featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD) + + val isStabilityIndexFixEnabled: Boolean by lazy { + featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX) + } + + val isSemiStableSortEnabled: Boolean by lazy { + featureFlags.isEnabled(Flags.SEMI_STABLE_SORT) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt index ad3dfedcdb96..3058fbbc1031 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotifInteractionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.collection.NotificationEntry import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt index 553826dda919..0d35fdce953e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt @@ -70,8 +70,8 @@ class NotificationLaunchAnimatorController( val height = max(0, notification.actualHeight - notification.clipBottomAmount) val location = notification.locationOnScreen - val clipStartLocation = notificationListContainer.getTopClippingStartLocation() - val roundedTopClipping = Math.max(clipStartLocation - location[1], 0) + val clipStartLocation = notificationListContainer.topClippingStartLocation + val roundedTopClipping = (clipStartLocation - location[1]).coerceAtLeast(0) val windowTop = location[1] + roundedTopClipping val topCornerRadius = if (roundedTopClipping > 0) { // Because the rounded Rect clipping is complex, we start the top rounding at @@ -80,7 +80,7 @@ class NotificationLaunchAnimatorController( // if we'd like to have this perfect, but this is close enough. 0f } else { - notification.currentBackgroundRadiusTop + notification.topCornerRadius } val params = LaunchAnimationParameters( top = windowTop, @@ -88,7 +88,7 @@ class NotificationLaunchAnimatorController( left = location[0], right = location[0] + notification.width, topCornerRadius = topCornerRadius, - bottomCornerRadius = notification.currentBackgroundRadiusBottom + bottomCornerRadius = notification.bottomCornerRadius ) params.startTranslationZ = notification.translationZ @@ -97,8 +97,8 @@ class NotificationLaunchAnimatorController( params.startClipTopAmount = notification.clipTopAmount if (notification.isChildInGroup) { params.startNotificationTop += notification.notificationParent.translationY - val parentRoundedClip = Math.max( - clipStartLocation - notification.notificationParent.locationOnScreen[1], 0) + val locationOnScreen = notification.notificationParent.locationOnScreen[1] + val parentRoundedClip = (clipStartLocation - locationOnScreen).coerceAtLeast(0) params.parentStartRoundedTopClipping = parentRoundedClip val parentClip = notification.notificationParent.clipTopAmount diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt index 7242506f1015..d97b712df030 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt @@ -18,8 +18,10 @@ package com.android.systemui.statusbar.notification import android.animation.ObjectAnimator import android.util.FloatProperty +import com.android.systemui.Dumpable import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionListener @@ -32,17 +34,20 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener +import java.io.PrintWriter import javax.inject.Inject import kotlin.math.min @SysUISingleton class NotificationWakeUpCoordinator @Inject constructor( + dumpManager: DumpManager, private val mHeadsUpManager: HeadsUpManager, private val statusBarStateController: StatusBarStateController, private val bypassController: KeyguardBypassController, private val dozeParameters: DozeParameters, private val screenOffAnimationController: ScreenOffAnimationController -) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener { +) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener, + Dumpable { private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>( "notificationVisibility") { @@ -60,6 +65,7 @@ class NotificationWakeUpCoordinator @Inject constructor( private var mLinearDozeAmount: Float = 0.0f private var mDozeAmount: Float = 0.0f + private var mDozeAmountSource: String = "init" private var mNotificationVisibleAmount = 0.0f private var mNotificationsVisible = false private var mNotificationsVisibleForExpansion = false @@ -142,6 +148,7 @@ class NotificationWakeUpCoordinator @Inject constructor( } init { + dumpManager.registerDumpable(this) mHeadsUpManager.addListener(this) statusBarStateController.addCallback(this) addListener(object : WakeUpListener { @@ -248,13 +255,14 @@ class NotificationWakeUpCoordinator @Inject constructor( // Let's notify the scroller that an animation started notifyAnimationStart(mLinearDozeAmount == 1.0f) } - setDozeAmount(linear, eased) + setDozeAmount(linear, eased, source = "StatusBar") } - fun setDozeAmount(linear: Float, eased: Float) { + fun setDozeAmount(linear: Float, eased: Float, source: String) { val changed = linear != mLinearDozeAmount mLinearDozeAmount = linear mDozeAmount = eased + mDozeAmountSource = source mStackScrollerController.setDozeAmount(mDozeAmount) updateHideAmount() if (changed && linear == 0.0f) { @@ -271,7 +279,7 @@ class NotificationWakeUpCoordinator @Inject constructor( // undefined state, so it's an indication that we should do state cleanup. We override // the doze amount to 0f (not dozing) so that the notifications are no longer hidden. // See: UnlockedScreenOffAnimationController.onFinishedWakingUp() - setDozeAmount(0f, 0f) + setDozeAmount(0f, 0f, source = "Override: Shade->Shade (lock cancelled by unlock)") } if (overrideDozeAmountIfAnimatingScreenOff(mLinearDozeAmount)) { @@ -311,12 +319,11 @@ class NotificationWakeUpCoordinator @Inject constructor( */ private fun overrideDozeAmountIfBypass(): Boolean { if (bypassController.bypassEnabled) { - var amount = 1.0f - if (statusBarStateController.state == StatusBarState.SHADE || - statusBarStateController.state == StatusBarState.SHADE_LOCKED) { - amount = 0.0f + if (statusBarStateController.state == StatusBarState.KEYGUARD) { + setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)") + } else { + setDozeAmount(0f, 0f, source = "Override: bypass (shade)") } - setDozeAmount(amount, amount) return true } return false @@ -332,7 +339,7 @@ class NotificationWakeUpCoordinator @Inject constructor( */ private fun overrideDozeAmountIfAnimatingScreenOff(linearDozeAmount: Float): Boolean { if (screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()) { - setDozeAmount(1f, 1f) + setDozeAmount(1f, 1f, source = "Override: animating screen off") return true } @@ -414,6 +421,26 @@ class NotificationWakeUpCoordinator @Inject constructor( private fun shouldAnimateVisibility() = dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("mLinearDozeAmount: $mLinearDozeAmount") + pw.println("mDozeAmount: $mDozeAmount") + pw.println("mDozeAmountSource: $mDozeAmountSource") + pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount") + pw.println("mNotificationsVisible: $mNotificationsVisible") + pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion") + pw.println("mVisibilityAmount: $mVisibilityAmount") + pw.println("mLinearVisibilityAmount: $mLinearVisibilityAmount") + pw.println("pulseExpanding: $pulseExpanding") + pw.println("state: ${StatusBarState.toString(state)}") + pw.println("fullyAwake: $fullyAwake") + pw.println("wakingUp: $wakingUp") + pw.println("willWakeUp: $willWakeUp") + pw.println("collapsedEnoughToHide: $collapsedEnoughToHide") + pw.println("pulsing: $pulsing") + pw.println("notificationsFullyHidden: $notificationsFullyHidden") + pw.println("canShowPulsingHuns: $canShowPulsingHuns") + } + interface WakeUpListener { /** * Called whenever the notifications are fully hidden or shown diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt new file mode 100644 index 000000000000..ed7f648081c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt @@ -0,0 +1,284 @@ +package com.android.systemui.statusbar.notification + +import android.util.FloatProperty +import android.view.View +import androidx.annotation.FloatRange +import com.android.systemui.R +import com.android.systemui.statusbar.notification.stack.AnimationProperties +import com.android.systemui.statusbar.notification.stack.StackStateAnimator +import kotlin.math.abs + +/** + * Interface that allows to request/retrieve top and bottom roundness (a value between 0f and 1f). + * + * To request a roundness value, an [SourceType] must be specified. In case more origins require + * different roundness, for the same property, the maximum value will always be chosen. + * + * It also returns the current radius for all corners ([updatedRadii]). + */ +interface Roundable { + /** Properties required for a Roundable */ + val roundableState: RoundableState + + /** Current top roundness */ + @get:FloatRange(from = 0.0, to = 1.0) + @JvmDefault + val topRoundness: Float + get() = roundableState.topRoundness + + /** Current bottom roundness */ + @get:FloatRange(from = 0.0, to = 1.0) + @JvmDefault + val bottomRoundness: Float + get() = roundableState.bottomRoundness + + /** Max radius in pixel */ + @JvmDefault + val maxRadius: Float + get() = roundableState.maxRadius + + /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */ + @JvmDefault + val topCornerRadius: Float + get() = topRoundness * maxRadius + + /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */ + @JvmDefault + val bottomCornerRadius: Float + get() = bottomRoundness * maxRadius + + /** Get and update the current radii */ + @JvmDefault + val updatedRadii: FloatArray + get() = + roundableState.radiiBuffer.also { radii -> + updateRadii( + topCornerRadius = topCornerRadius, + bottomCornerRadius = bottomCornerRadius, + radii = radii, + ) + } + + /** + * Request the top roundness [value] for a specific [sourceType]. + * + * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more + * origins require different roundness, for the same property, the maximum value will always be + * chosen. + * + * @param value a value between 0f and 1f. + * @param animate true if it should animate to that value. + * @param sourceType the source from which the request for roundness comes. + * @return Whether the roundness was changed. + */ + @JvmDefault + fun requestTopRoundness( + @FloatRange(from = 0.0, to = 1.0) value: Float, + animate: Boolean, + sourceType: SourceType, + ): Boolean { + val roundnessMap = roundableState.topRoundnessMap + val lastValue = roundnessMap.values.maxOrNull() ?: 0f + if (value == 0f) { + // we should only take the largest value, and since the smallest value is 0f, we can + // remove this value from the list. In the worst case, the list is empty and the + // default value is 0f. + roundnessMap.remove(sourceType) + } else { + roundnessMap[sourceType] = value + } + val newValue = roundnessMap.values.maxOrNull() ?: 0f + + if (lastValue != newValue) { + val wasAnimating = roundableState.isTopAnimating() + + // Fail safe: + // when we've been animating previously and we're now getting an update in the + // other direction, make sure to animate it too, otherwise, the localized updating + // may make the start larger than 1.0. + val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f + + roundableState.setTopRoundness(value = newValue, animated = shouldAnimate || animate) + return true + } + return false + } + + /** + * Request the bottom roundness [value] for a specific [sourceType]. + * + * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more + * origins require different roundness, for the same property, the maximum value will always be + * chosen. + * + * @param value value between 0f and 1f. + * @param animate true if it should animate to that value. + * @param sourceType the source from which the request for roundness comes. + * @return Whether the roundness was changed. + */ + @JvmDefault + fun requestBottomRoundness( + @FloatRange(from = 0.0, to = 1.0) value: Float, + animate: Boolean, + sourceType: SourceType, + ): Boolean { + val roundnessMap = roundableState.bottomRoundnessMap + val lastValue = roundnessMap.values.maxOrNull() ?: 0f + if (value == 0f) { + // we should only take the largest value, and since the smallest value is 0f, we can + // remove this value from the list. In the worst case, the list is empty and the + // default value is 0f. + roundnessMap.remove(sourceType) + } else { + roundnessMap[sourceType] = value + } + val newValue = roundnessMap.values.maxOrNull() ?: 0f + + if (lastValue != newValue) { + val wasAnimating = roundableState.isBottomAnimating() + + // Fail safe: + // when we've been animating previously and we're now getting an update in the + // other direction, make sure to animate it too, otherwise, the localized updating + // may make the start larger than 1.0. + val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f + + roundableState.setBottomRoundness(value = newValue, animated = shouldAnimate || animate) + return true + } + return false + } + + /** Apply the roundness changes, usually means invalidate the [RoundableState.targetView]. */ + @JvmDefault + fun applyRoundness() { + roundableState.targetView.invalidate() + } + + /** @return true if top or bottom roundness is not zero. */ + @JvmDefault + fun hasRoundedCorner(): Boolean { + return topRoundness != 0f || bottomRoundness != 0f + } + + /** + * Update an Array of 8 values, 4 pairs of [X,Y] radii. As expected by param radii of + * [android.graphics.Path.addRoundRect]. + * + * This method reuses the previous [radii] for performance reasons. + */ + @JvmDefault + fun updateRadii( + topCornerRadius: Float, + bottomCornerRadius: Float, + radii: FloatArray, + ) { + if (radii.size != 8) error("Unexpected radiiBuffer size ${radii.size}") + + if (radii[0] != topCornerRadius || radii[4] != bottomCornerRadius) { + (0..3).forEach { radii[it] = topCornerRadius } + (4..7).forEach { radii[it] = bottomCornerRadius } + } + } +} + +/** + * State object for a `Roundable` class. + * @param targetView Will handle the [AnimatableProperty] + * @param roundable Target of the radius animation + * @param maxRadius Max corner radius in pixels + */ +class RoundableState( + internal val targetView: View, + roundable: Roundable, + internal val maxRadius: Float, +) { + /** Animatable for top roundness */ + private val topAnimatable = topAnimatable(roundable) + + /** Animatable for bottom roundness */ + private val bottomAnimatable = bottomAnimatable(roundable) + + /** Current top roundness. Use [setTopRoundness] to update this value */ + @set:FloatRange(from = 0.0, to = 1.0) + internal var topRoundness = 0f + private set + + /** Current bottom roundness. Use [setBottomRoundness] to update this value */ + @set:FloatRange(from = 0.0, to = 1.0) + internal var bottomRoundness = 0f + private set + + /** Last requested top roundness associated by [SourceType] */ + internal val topRoundnessMap = mutableMapOf<SourceType, Float>() + + /** Last requested bottom roundness associated by [SourceType] */ + internal val bottomRoundnessMap = mutableMapOf<SourceType, Float>() + + /** Last cached radii */ + internal val radiiBuffer = FloatArray(8) + + /** Is top roundness animation in progress? */ + internal fun isTopAnimating() = PropertyAnimator.isAnimating(targetView, topAnimatable) + + /** Is bottom roundness animation in progress? */ + internal fun isBottomAnimating() = PropertyAnimator.isAnimating(targetView, bottomAnimatable) + + /** Set the current top roundness */ + internal fun setTopRoundness( + value: Float, + animated: Boolean = targetView.isShown, + ) { + PropertyAnimator.setProperty(targetView, topAnimatable, value, DURATION, animated) + } + + /** Set the current bottom roundness */ + internal fun setBottomRoundness( + value: Float, + animated: Boolean = targetView.isShown, + ) { + PropertyAnimator.setProperty(targetView, bottomAnimatable, value, DURATION, animated) + } + + companion object { + private val DURATION: AnimationProperties = + AnimationProperties() + .setDuration(StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS.toLong()) + + private fun topAnimatable(roundable: Roundable): AnimatableProperty = + AnimatableProperty.from( + object : FloatProperty<View>("topRoundness") { + override fun get(view: View): Float = roundable.topRoundness + + override fun setValue(view: View, value: Float) { + roundable.roundableState.topRoundness = value + roundable.applyRoundness() + } + }, + R.id.top_roundess_animator_tag, + R.id.top_roundess_animator_end_tag, + R.id.top_roundess_animator_start_tag, + ) + + private fun bottomAnimatable(roundable: Roundable): AnimatableProperty = + AnimatableProperty.from( + object : FloatProperty<View>("bottomRoundness") { + override fun get(view: View): Float = roundable.bottomRoundness + + override fun setValue(view: View, value: Float) { + roundable.roundableState.bottomRoundness = value + roundable.applyRoundness() + } + }, + R.id.bottom_roundess_animator_tag, + R.id.bottom_roundess_animator_end_tag, + R.id.bottom_roundess_animator_start_tag, + ) + } +} + +enum class SourceType { + DefaultValue, + OnDismissAnimation, + OnScroll, +} 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 f8449ae8807b..84ab0d1190f0 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 @@ -68,6 +68,9 @@ data class ListAttachState private constructor( */ var stableIndex: Int = -1 + /** Access the index of the [section] or -1 if the entry does not have one */ + val sectionIndex: Int get() = section?.index ?: -1 + /** Copies the state of another instance. */ fun clone(other: ListAttachState) { parent = other.parent @@ -95,11 +98,13 @@ data class ListAttachState private constructor( * 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() { + fun detach(includingStableIndex: Boolean) { parent = null section = null promoter = null - // stableIndex = -1 // TODO(b/241229236): Clear this once we fix the stability fragility + if (includingStableIndex) { + stableIndex = -1 + } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index 2887f975d46c..df35c9e6832a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -602,7 +602,7 @@ public class NotifCollection implements Dumpable, PipelineDumpable { mInconsistencyTracker.logNewMissingNotifications(rankingMap); mInconsistencyTracker.logNewInconsistentRankings(currentEntriesWithoutRankings, rankingMap); - if (currentEntriesWithoutRankings != null && mNotifPipelineFlags.removeUnrankedNotifs()) { + if (currentEntriesWithoutRankings != null) { for (NotificationEntry entry : currentEntriesWithoutRankings.values()) { entry.mCancellationReason = REASON_UNKNOWN; tryRemoveNotification(entry); 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 e129ee45817a..3ae2545e4e10 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 @@ -54,6 +54,9 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener; import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState; +import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort; +import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder; +import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper; import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator; @@ -96,11 +99,14 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated // TODO replace temp with collection pool for readability private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>(); + private NotifPipelineFlags mFlags; private final boolean mAlwaysLogList; private List<ListEntry> mNotifList = new ArrayList<>(); private List<ListEntry> mNewNotifList = new ArrayList<>(); + private final SemiStableSort mSemiStableSort = new SemiStableSort(); + private final StableOrder<ListEntry> mStableOrder = this::getStableOrderRank; private final PipelineState mPipelineState = new PipelineState(); private final Map<String, GroupEntry> mGroups = new ArrayMap<>(); private Collection<NotificationEntry> mAllEntries = Collections.emptyList(); @@ -141,6 +147,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { ) { mSystemClock = systemClock; mLogger = logger; + mFlags = flags; mAlwaysLogList = flags.isDevLoggingEnabled(); mInteractionTracker = interactionTracker; mChoreographer = pipelineChoreographer; @@ -527,7 +534,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { List<NotifFilter> filters) { Trace.beginSection("ShadeListBuilder.filterNotifs"); final long now = mSystemClock.uptimeMillis(); - for (ListEntry entry : entries) { + for (ListEntry entry : entries) { if (entry instanceof GroupEntry) { final GroupEntry groupEntry = (GroupEntry) entry; @@ -958,7 +965,8 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { * filtered out during any of the filtering steps. */ private void annulAddition(ListEntry entry) { - entry.getAttachState().detach(); + // NOTE(b/241229236): Don't clear stableIndex until we fix stability fragility + entry.getAttachState().detach(/* includingStableIndex= */ mFlags.isSemiStableSortEnabled()); } private void assignSections() { @@ -978,7 +986,16 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private void sortListAndGroups() { Trace.beginSection("ShadeListBuilder.sortListAndGroups"); - // Assign sections to top-level elements and sort their children + if (mFlags.isSemiStableSortEnabled()) { + sortWithSemiStableSort(); + } else { + sortWithLegacyStability(); + } + Trace.endSection(); + } + + private void sortWithLegacyStability() { + // Sort all groups and the top level list for (ListEntry entry : mNotifList) { if (entry instanceof GroupEntry) { GroupEntry parent = (GroupEntry) entry; @@ -991,16 +1008,15 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // Check for suppressed order changes if (!getStabilityManager().isEveryChangeAllowed()) { mForceReorderable = true; - boolean isSorted = isShadeSorted(); + boolean isSorted = isShadeSortedLegacy(); mForceReorderable = false; if (!isSorted) { getStabilityManager().onEntryReorderSuppressed(); } } - Trace.endSection(); } - private boolean isShadeSorted() { + private boolean isShadeSortedLegacy() { if (!isSorted(mNotifList, mTopLevelComparator)) { return false; } @@ -1014,6 +1030,43 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { return true; } + private void sortWithSemiStableSort() { + // Sort each group's children + boolean allSorted = true; + for (ListEntry entry : mNotifList) { + if (entry instanceof GroupEntry) { + GroupEntry parent = (GroupEntry) entry; + allSorted &= sortGroupChildren(parent.getRawChildren()); + } + } + // Sort each section within the top level list + mNotifList.sort(mTopLevelComparator); + if (!getStabilityManager().isEveryChangeAllowed()) { + for (List<ListEntry> subList : getSectionSubLists(mNotifList)) { + allSorted &= mSemiStableSort.stabilizeTo(subList, mStableOrder, mNewNotifList); + } + applyNewNotifList(); + } + assignIndexes(mNotifList); + if (!allSorted) { + // Report suppressed order changes + getStabilityManager().onEntryReorderSuppressed(); + } + } + + private Iterable<List<ListEntry>> getSectionSubLists(List<ListEntry> entries) { + return ShadeListBuilderHelper.INSTANCE.getSectionSubLists(entries); + } + + private boolean sortGroupChildren(List<NotificationEntry> entries) { + if (getStabilityManager().isEveryChangeAllowed()) { + entries.sort(mGroupChildrenComparator); + return true; + } else { + return mSemiStableSort.sort(entries, mStableOrder, mGroupChildrenComparator); + } + } + /** Determine whether the items in the list are sorted according to the comparator */ @VisibleForTesting public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) { @@ -1036,27 +1089,41 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { /** * Assign the index of each notification relative to the total order */ - private static void assignIndexes(List<ListEntry> notifList) { + private void assignIndexes(List<ListEntry> notifList) { if (notifList.size() == 0) return; NotifSection currentSection = requireNonNull(notifList.get(0).getSection()); int sectionMemberIndex = 0; for (int i = 0; i < notifList.size(); i++) { - ListEntry entry = notifList.get(i); + final ListEntry entry = notifList.get(i); NotifSection section = requireNonNull(entry.getSection()); if (section.getIndex() != currentSection.getIndex()) { sectionMemberIndex = 0; currentSection = section; } - entry.getAttachState().setStableIndex(sectionMemberIndex); - if (entry instanceof GroupEntry) { - GroupEntry parent = (GroupEntry) entry; - for (int j = 0; j < parent.getChildren().size(); j++) { - entry = parent.getChildren().get(j); - entry.getAttachState().setStableIndex(sectionMemberIndex); - sectionMemberIndex++; + if (mFlags.isStabilityIndexFixEnabled()) { + entry.getAttachState().setStableIndex(sectionMemberIndex++); + if (entry instanceof GroupEntry) { + final GroupEntry parent = (GroupEntry) entry; + final NotificationEntry summary = parent.getSummary(); + if (summary != null) { + summary.getAttachState().setStableIndex(sectionMemberIndex++); + } + for (NotificationEntry child : parent.getChildren()) { + child.getAttachState().setStableIndex(sectionMemberIndex++); + } + } + } else { + // This old implementation uses the same index number for the group as the first + // child, and fails to assign an index to the summary. Remove once tested. + entry.getAttachState().setStableIndex(sectionMemberIndex); + if (entry instanceof GroupEntry) { + final GroupEntry parent = (GroupEntry) entry; + for (NotificationEntry child : parent.getChildren()) { + child.getAttachState().setStableIndex(sectionMemberIndex++); + } } + sectionMemberIndex++; } - sectionMemberIndex++; } } @@ -1196,7 +1263,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { o2.getSectionIndex()); if (cmp != 0) return cmp; - cmp = Integer.compare( + cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare( getStableOrderIndex(o1), getStableOrderIndex(o2)); if (cmp != 0) return cmp; @@ -1225,7 +1292,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> { - int cmp = Integer.compare( + int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare( getStableOrderIndex(o1), getStableOrderIndex(o2)); if (cmp != 0) return cmp; @@ -1256,9 +1323,25 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // let the stability manager constrain or allow reordering return -1; } + // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility return entry.getPreviousAttachState().getStableIndex(); } + @Nullable + private Integer getStableOrderRank(ListEntry entry) { + if (getStabilityManager().isEntryReorderingAllowed(entry)) { + // let the stability manager constrain or allow reordering + return null; + } + if (entry.getAttachState().getSectionIndex() + != entry.getPreviousAttachState().getSectionIndex()) { + // stable index is only valid within the same section; otherwise we allow reordering + return null; + } + final int stableIndex = entry.getPreviousAttachState().getStableIndex(); + return stableIndex == -1 ? null : stableIndex; + } + private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) { final NotifFilter filter = findRejectingFilter(entry, now, filters); entry.getAttachState().setExcludingFilter(filter); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt index 211e37473a70..68d1319699d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.coalescer -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject class GroupCoalescerLogger @Inject constructor( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt index e8f352f60da0..2919def16304 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.row.NotificationGuts import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 8f3eb4f7e223..8a31ed9271ad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification import android.app.Notification.GROUP_ALERT_SUMMARY import android.util.ArrayMap +import android.util.ArraySet +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.collection.GroupEntry @@ -70,6 +72,7 @@ class HeadsUpCoordinator @Inject constructor( @Main private val mExecutor: DelayableExecutor, ) : Coordinator { private val mEntriesBindingUntil = ArrayMap<String, Long>() + private val mEntriesUpdateTimes = ArrayMap<String, Long>() private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null private lateinit var mNotifPipeline: NotifPipeline private var mNow: Long = -1 @@ -264,6 +267,9 @@ class HeadsUpCoordinator @Inject constructor( } // After this method runs, all posted entries should have been handled (or skipped). mPostedEntries.clear() + + // Also take this opportunity to clean up any stale entry update times + cleanUpEntryUpdateTimes() } /** @@ -378,6 +384,9 @@ class HeadsUpCoordinator @Inject constructor( isAlerting = false, isBinding = false, ) + + // Record the last updated time for this key + setUpdateTime(entry, mSystemClock.currentTimeMillis()) } /** @@ -419,6 +428,9 @@ class HeadsUpCoordinator @Inject constructor( cancelHeadsUpBind(posted.entry) } } + + // Update last updated time for this entry + setUpdateTime(entry, mSystemClock.currentTimeMillis()) } /** @@ -426,6 +438,7 @@ class HeadsUpCoordinator @Inject constructor( */ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { mPostedEntries.remove(entry.key) + mEntriesUpdateTimes.remove(entry.key) cancelHeadsUpBind(entry) val entryKey = entry.key if (mHeadsUpManager.isAlerting(entryKey)) { @@ -454,7 +467,12 @@ class HeadsUpCoordinator @Inject constructor( // never) in mPostedEntries to need to alert, we need to check every notification // known to the pipeline. for (entry in mNotifPipeline.allNotifs) { - // The only entries we can consider alerting for here are entries that have never + // Only consider entries that are recent enough, since we want to apply a fairly + // strict threshold for when an entry should be updated via only ranking and not an + // app-provided notification update. + if (!isNewEnoughForRankingUpdate(entry)) continue + + // The only entries we consider alerting for here are entries that have never // interrupted and that now say they should heads up; if they've alerted in the // past, we don't want to incorrectly alert a second time if there wasn't an // explicit notification update. @@ -486,6 +504,41 @@ class HeadsUpCoordinator @Inject constructor( (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0) } + /** + * Sets the updated time for the given entry to the specified time. + */ + @VisibleForTesting + fun setUpdateTime(entry: NotificationEntry, time: Long) { + mEntriesUpdateTimes[entry.key] = time + } + + /** + * Checks whether the entry is new enough to be updated via ranking update. + * We want to avoid updating an entry too long after it was originally posted/updated when we're + * only reacting to a ranking change, as relevant ranking updates are expected to come in + * fairly soon after the posting of a notification. + */ + private fun isNewEnoughForRankingUpdate(entry: NotificationEntry): Boolean { + // If we don't have an update time for this key, default to "too old" + if (!mEntriesUpdateTimes.containsKey(entry.key)) return false + + val updateTime = mEntriesUpdateTimes[entry.key] ?: return false + return (mSystemClock.currentTimeMillis() - updateTime) <= MAX_RANKING_UPDATE_DELAY_MS + } + + private fun cleanUpEntryUpdateTimes() { + // Because we won't update entries that are older than this amount of time anyway, clean + // up any entries that are too old to notify. + val toRemove = ArraySet<String>() + for ((key, updateTime) in mEntriesUpdateTimes) { + if (updateTime == null || + (mSystemClock.currentTimeMillis() - updateTime) > MAX_RANKING_UPDATE_DELAY_MS) { + toRemove.add(key) + } + } + mEntriesUpdateTimes.removeAll(toRemove) + } + /** When an action is pressed on a notification, end HeadsUp lifetime extension. */ private val mActionPressListener = Consumer<NotificationEntry> { entry -> if (mNotifsExtendingLifetime.contains(entry)) { @@ -597,6 +650,9 @@ class HeadsUpCoordinator @Inject constructor( companion object { private const val TAG = "HeadsUpCoordinator" private const val BIND_TIMEOUT = 1000L + + // This value is set to match MAX_SOUND_DELAY_MS in NotificationRecord. + private const val MAX_RANKING_UPDATE_DELAY_MS: Long = 2000 } data class PostedEntry( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt index 8625cdbc89d5..dfaa291c6bb6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt @@ -1,9 +1,10 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.util.Log -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel + import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject private const val TAG = "HeadsUpCoordinator" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java index 2480ff65d8fc..0be4bde749f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java @@ -16,14 +16,14 @@ package com.android.systemui.statusbar.notification.collection.coordinator; -import static com.android.systemui.media.MediaDataManagerKt.isMediaNotification; +import static com.android.systemui.media.controls.pipeline.MediaDataManagerKt.isMediaNotification; import android.os.RemoteException; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; import com.android.internal.statusbar.IStatusBarService; -import com.android.systemui.media.MediaFeatureFlag; +import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index 93146f9b3bf3..d2db6224ef52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -410,7 +410,7 @@ public class PreparationCoordinator implements Coordinator { // Only delay release if the summary is not inflated. // TODO(253454977): Once we ensure that all other pipeline filtering and pruning has been // done by this point, we can revert back to checking for mInflatingNotifs.contains(...) - if (!isInflated(group.getSummary())) { + if (group.getSummary() != null && !isInflated(group.getSummary())) { mLogger.logDelayingGroupRelease(group, group.getSummary()); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt index c4f4ed54e2fa..9558f47af795 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.coordinator -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt index c687e1bacbc9..d80445491bda 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.coordinator -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject private const val TAG = "ShadeEventCoordinator" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt new file mode 100644 index 000000000000..9ec8e07e73b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt @@ -0,0 +1,200 @@ +/* + * 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.notification.collection.listbuilder + +import androidx.annotation.VisibleForTesting +import kotlin.math.sign + +class SemiStableSort { + val preallocatedWorkspace by lazy { ArrayList<Any>() } + val preallocatedAdditions by lazy { ArrayList<Any>() } + val preallocatedMapToIndex by lazy { HashMap<Any, Int>() } + val preallocatedMapToIndexComparator: Comparator<Any> by lazy { + Comparator.comparingInt { item -> preallocatedMapToIndex[item] ?: -1 } + } + + /** + * Sort the given [items] such that items which have a [stableOrder] will all be in that order, + * items without a [stableOrder] will be sorted according to the comparator, and the two sets of + * items will be combined to have the fewest elements out of order according to the [comparator] + * . The result will be placed into the original [items] list. + */ + fun <T : Any> sort( + items: MutableList<T>, + stableOrder: StableOrder<in T>, + comparator: Comparator<in T>, + ): Boolean = + withWorkspace<T, Boolean> { workspace -> + val ordered = + sortTo( + items, + stableOrder, + comparator, + workspace, + ) + items.clear() + items.addAll(workspace) + return ordered + } + + /** + * Sort the given [items] such that items which have a [stableOrder] will all be in that order, + * items without a [stableOrder] will be sorted according to the comparator, and the two sets of + * items will be combined to have the fewest elements out of order according to the [comparator] + * . The result will be put into [output]. + */ + fun <T : Any> sortTo( + items: Iterable<T>, + stableOrder: StableOrder<in T>, + comparator: Comparator<in T>, + output: MutableList<T>, + ): Boolean { + if (DEBUG) println("\n> START from ${items.map { it to stableOrder.getRank(it) }}") + // If array already has elements, use subList to ensure we only append + val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size) + items.filterTo(result) { stableOrder.getRank(it) != null } + result.sortBy { stableOrder.getRank(it)!! } + val isOrdered = result.isSorted(comparator) + withAdditions<T> { additions -> + items.filterTo(additions) { stableOrder.getRank(it) == null } + additions.sortWith(comparator) + insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator) + } + return isOrdered + } + + /** + * Rearrange the [sortedItems] to enforce that items are in the [stableOrder], and store the + * result in [output]. Items with a [stableOrder] will be in that order, items without a + * [stableOrder] will remain in same relative order as the input, and the two sets of items will + * be combined to have the fewest elements moved from their locations in the original. + */ + fun <T : Any> stabilizeTo( + sortedItems: Iterable<T>, + stableOrder: StableOrder<in T>, + output: MutableList<T>, + ): Boolean { + // Append to the output array if present + val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size) + sortedItems.filterTo(result) { stableOrder.getRank(it) != null } + val stableRankComparator = compareBy<T> { stableOrder.getRank(it)!! } + val isOrdered = result.isSorted(stableRankComparator) + if (!isOrdered) { + result.sortWith(stableRankComparator) + } + if (result.isEmpty()) { + sortedItems.filterTo(result) { stableOrder.getRank(it) == null } + return isOrdered + } + withAdditions<T> { additions -> + sortedItems.filterTo(additions) { stableOrder.getRank(it) == null } + if (additions.isNotEmpty()) { + withIndexOfComparator(sortedItems) { comparator -> + insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator) + } + } + } + return isOrdered + } + + private inline fun <T : Any, R> withWorkspace(block: (ArrayList<T>) -> R): R { + preallocatedWorkspace.clear() + val result = block(preallocatedWorkspace as ArrayList<T>) + preallocatedWorkspace.clear() + return result + } + + private inline fun <T : Any> withAdditions(block: (ArrayList<T>) -> Unit) { + preallocatedAdditions.clear() + block(preallocatedAdditions as ArrayList<T>) + preallocatedAdditions.clear() + } + + private inline fun <T : Any> withIndexOfComparator( + sortedItems: Iterable<T>, + block: (Comparator<in T>) -> Unit + ) { + preallocatedMapToIndex.clear() + sortedItems.forEachIndexed { i, item -> preallocatedMapToIndex[item] = i } + block(preallocatedMapToIndexComparator as Comparator<in T>) + preallocatedMapToIndex.clear() + } + + companion object { + + /** + * This is the core of the algorithm. + * + * Insert [preSortedAdditions] (the elements to be inserted) into [existing] without + * changing the relative order of any elements already in [existing], even though those + * elements may be mis-ordered relative to the [comparator], such that the total number of + * elements which are ordered incorrectly according to the [comparator] is fewest. + */ + private fun <T> insertPreSortedElementsWithFewestMisOrderings( + existing: MutableList<T>, + preSortedAdditions: Iterable<T>, + comparator: Comparator<in T>, + ) { + if (DEBUG) println(" To $existing insert $preSortedAdditions with fewest misordering") + var iStart = 0 + preSortedAdditions.forEach { toAdd -> + if (DEBUG) println(" need to add $toAdd to $existing, starting at $iStart") + var cmpSum = 0 + var cmpSumMax = 0 + var iCmpSumMax = iStart + if (DEBUG) print(" ") + for (i in iCmpSumMax until existing.size) { + val cmp = comparator.compare(toAdd, existing[i]).sign + cmpSum += cmp + if (cmpSum > cmpSumMax) { + cmpSumMax = cmpSum + iCmpSumMax = i + 1 + } + if (DEBUG) print("sum[$i]=$cmpSum, ") + } + if (DEBUG) println("inserting $toAdd at $iCmpSumMax") + existing.add(iCmpSumMax, toAdd) + iStart = iCmpSumMax + 1 + } + } + + /** Determines if a list is correctly sorted according to the given comparator */ + @VisibleForTesting + fun <T> List<T>.isSorted(comparator: Comparator<T>): Boolean { + if (this.size <= 1) { + return true + } + val iterator = this.iterator() + var previous = iterator.next() + var current: T? + while (iterator.hasNext()) { + current = iterator.next() + if (comparator.compare(previous, current) > 0) { + return false + } + previous = current + } + return true + } + } + + fun interface StableOrder<T> { + fun getRank(item: T): Int? + } +} + +val DEBUG = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt new file mode 100644 index 000000000000..d8f75f61c05a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt @@ -0,0 +1,53 @@ +/* + * 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.notification.collection.listbuilder + +import com.android.systemui.statusbar.notification.collection.ListEntry + +object ShadeListBuilderHelper { + fun getSectionSubLists(entries: List<ListEntry>): Iterable<List<ListEntry>> = + getContiguousSubLists(entries, minLength = 1) { it.sectionIndex } + + inline fun <T : Any, K : Any> getContiguousSubLists( + itemList: List<T>, + minLength: Int = 1, + key: (T) -> K, + ): Iterable<List<T>> { + val subLists = mutableListOf<List<T>>() + val numEntries = itemList.size + var currentSectionStartIndex = 0 + var currentSectionKey: K? = null + for (i in 0 until numEntries) { + val sectionKey = key(itemList[i]) + if (currentSectionKey == null) { + currentSectionKey = sectionKey + } else if (currentSectionKey != sectionKey) { + val length = i - currentSectionStartIndex + if (length >= minLength) { + subLists.add(itemList.subList(currentSectionStartIndex, i)) + } + currentSectionStartIndex = i + currentSectionKey = sectionKey + } + } + val length = numEntries - currentSectionStartIndex + if (length >= minLength) { + subLists.add(itemList.subList(currentSectionStartIndex, numEntries)) + } + return subLists + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt index d8dae5d23f42..8e052c7dcc5d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt @@ -16,11 +16,11 @@ package com.android.systemui.statusbar.notification.collection.listbuilder -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.WARNING import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt index aa27e1e407f0..911a2d0c2b36 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt @@ -20,13 +20,13 @@ import android.os.RemoteException import android.service.notification.NotificationListenerService import android.service.notification.NotificationListenerService.RankingMap import android.service.notification.StatusBarNotification -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.WARNING -import com.android.systemui.log.LogLevel.WTF import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.WARNING +import com.android.systemui.plugins.log.LogLevel.WTF import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason import com.android.systemui.statusbar.notification.collection.NotifCollection.FutureDismissal diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt index 38e3d496a60c..9c71e5c1054c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.render -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.util.Compile diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt index b6278d1d5f01..fde4ecb7bcaa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.collection.render +import javax.inject.Inject + /** An interface by which the pipeline can make updates to the notification root view. */ interface NotifStackController { /** Provides stats about the list of notifications attached to the shade */ @@ -42,6 +44,6 @@ data class NotifStats( * methods, rather than forcing us to add no-op implementations in their implementation every time * a method is added. */ -open class DefaultNotifStackController : NotifStackController { +open class DefaultNotifStackController @Inject constructor() : NotifStackController { override fun setNotifStats(stats: NotifStats) {} }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt index 6d1071c283e3..b4b9438cd6be 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.render -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import java.lang.RuntimeException import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt index b2cb23bd11aa..a5278c3d0ad3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt @@ -23,6 +23,7 @@ import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.stack.NotificationListContainer +import com.android.systemui.statusbar.phone.CentralSurfaces /** * The master controller for all notifications-related work @@ -32,6 +33,7 @@ import com.android.systemui.statusbar.notification.stack.NotificationListContain */ interface NotificationsController { fun initialize( + centralSurfaces: CentralSurfaces, presenter: NotificationPresenter, listContainer: NotificationListContainer, stackController: NotifStackController, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index 8e646a37a4b3..8eef3f36433d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -24,6 +24,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.people.widget.PeopleSpaceWidgetManager import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationListener +import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.AnimatedImageNotificationManager import com.android.systemui.statusbar.notification.NotificationActivityStarter @@ -38,6 +39,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Co import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder +import com.android.systemui.statusbar.notification.logging.NotificationLogger import com.android.systemui.statusbar.notification.logging.NotificationMemoryMonitor import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer import com.android.systemui.statusbar.notification.stack.NotificationListContainer @@ -56,7 +58,6 @@ import javax.inject.Inject */ @SysUISingleton class NotificationsControllerImpl @Inject constructor( - private val centralSurfaces: Lazy<CentralSurfaces>, private val notificationListener: NotificationListener, private val commonNotifCollection: Lazy<CommonNotifCollection>, private val notifPipeline: Lazy<NotifPipeline>, @@ -64,7 +65,9 @@ class NotificationsControllerImpl @Inject constructor( private val targetSdkResolver: TargetSdkResolver, private val notifPipelineInitializer: Lazy<NotifPipelineInitializer>, private val notifBindPipelineInitializer: NotifBindPipelineInitializer, + private val notificationLogger: NotificationLogger, private val notificationRowBinder: NotificationRowBinderImpl, + private val notificationsMediaManager: NotificationMediaManager, private val headsUpViewBinder: HeadsUpViewBinder, private val clickerBuilder: NotificationClicker.Builder, private val animatedImageNotificationManager: AnimatedImageNotificationManager, @@ -76,6 +79,7 @@ class NotificationsControllerImpl @Inject constructor( ) : NotificationsController { override fun initialize( + centralSurfaces: CentralSurfaces, presenter: NotificationPresenter, listContainer: NotificationListContainer, stackController: NotifStackController, @@ -92,8 +96,8 @@ class NotificationsControllerImpl @Inject constructor( notificationRowBinder.setNotificationClicker( clickerBuilder.build( - Optional.of( - centralSurfaces.get()), bubblesOptional, notificationActivityStarter)) + Optional.ofNullable(centralSurfaces), bubblesOptional, + notificationActivityStarter)) notificationRowBinder.setUpWithPresenter( presenter, listContainer, @@ -109,7 +113,8 @@ class NotificationsControllerImpl @Inject constructor( stackController) targetSdkResolver.initialize(notifPipeline.get()) - + notificationsMediaManager.setUpWithPresenter(presenter) + notificationLogger.setUpWithContainer(listContainer) peopleSpaceWidgetManager.attach(notificationListener) fgsNotifListener.init() if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_MONITOR_ENABLED)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt index 744166d87907..14856dafdb11 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.stack.NotificationListContainer +import com.android.systemui.statusbar.phone.CentralSurfaces import javax.inject.Inject /** @@ -34,6 +35,7 @@ class NotificationsControllerStub @Inject constructor( ) : NotificationsController { override fun initialize( + centralSurfaces: CentralSurfaces, presenter: NotificationPresenter, listContainer: NotificationListContainer, stackController: NotifStackController, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt index 5dbec8dcba20..d4f11fc141f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.statusbar.notification.interruption -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt index 99d320d1c7ca..073b6b041b81 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt @@ -16,11 +16,11 @@ package com.android.systemui.statusbar.notification.interruption -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.WARNING import com.android.systemui.log.dagger.NotificationInterruptLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index c5a69217a1ac..c4f5a3a30608 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.interruption; import static com.android.systemui.statusbar.StatusBarState.SHADE; +import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD; +import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR; import android.app.NotificationManager; import android.content.ContentResolver; @@ -32,6 +34,8 @@ import android.service.notification.StatusBarNotification; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -68,10 +72,30 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter private final NotificationInterruptLogger mLogger; private final NotifPipelineFlags mFlags; private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider; + private final UiEventLogger mUiEventLogger; @VisibleForTesting protected boolean mUseHeadsUp = false; + public enum NotificationInterruptEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "FSI suppressed for suppressive GroupAlertBehavior") + FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(1235), + + @UiEvent(doc = "FSI suppressed for requiring neither HUN nor keyguard") + FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236); + + private final int mId; + + NotificationInterruptEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } + @Inject public NotificationInterruptStateProviderImpl( ContentResolver contentResolver, @@ -85,7 +109,8 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter NotificationInterruptLogger logger, @Main Handler mainHandler, NotifPipelineFlags flags, - KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) { + KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider, + UiEventLogger uiEventLogger) { mContentResolver = contentResolver; mPowerManager = powerManager; mDreamManager = dreamManager; @@ -97,6 +122,7 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter mLogger = logger; mFlags = flags; mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider; + mUiEventLogger = uiEventLogger; ContentObserver headsUpObserver = new ContentObserver(mainHandler) { @Override public void onChange(boolean selfChange) { @@ -203,7 +229,9 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter // b/231322873: Detect and report an event when a notification has both an FSI and a // suppressive groupAlertBehavior, and now correctly block the FSI from firing. final int uid = entry.getSbn().getUid(); + final String packageName = entry.getSbn().getPackageName(); android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "groupAlertBehavior"); + mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, uid, packageName); mLogger.logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN"); return false; } @@ -249,7 +277,9 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter // Detect the case determined by b/231322873 to launch FSI while device is in use, // as blocked by the correct implementation, and report the event. final int uid = entry.getSbn().getUid(); + final String packageName = entry.getSbn().getPackageName(); android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "no hun or keyguard"); + mUiEventLogger.log(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, uid, packageName); mLogger.logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard"); return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt index 832a739a9080..0380fff1e2af 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt @@ -20,8 +20,9 @@ package com.android.systemui.statusbar.notification.logging /** Describes usage of a notification. */ data class NotificationMemoryUsage( val packageName: String, - val notificationId: String, + val notificationKey: String, val objectUsage: NotificationObjectUsage, + val viewUsage: List<NotificationViewUsage> ) /** @@ -39,3 +40,26 @@ data class NotificationObjectUsage( val extender: Int, val hasCustomView: Boolean, ) + +enum class ViewType { + PUBLIC_VIEW, + PRIVATE_CONTRACTED_VIEW, + PRIVATE_EXPANDED_VIEW, + PRIVATE_HEADS_UP_VIEW, + TOTAL +} + +/** + * Describes current memory of a notification view hierarchy. + * + * The values are in bytes. + */ +data class NotificationViewUsage( + val viewType: ViewType, + val smallIcon: Int, + val largeIcon: Int, + val systemIcons: Int, + val style: Int, + val customViews: Int, + val softwareBitmapsPenalty: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt new file mode 100644 index 000000000000..7d39e18ab349 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt @@ -0,0 +1,212 @@ +package com.android.systemui.statusbar.notification.logging + +import android.app.Notification +import android.app.Person +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import androidx.annotation.WorkerThread +import com.android.systemui.statusbar.notification.NotificationUtils +import com.android.systemui.statusbar.notification.collection.NotificationEntry + +/** Calculates estimated memory usage of [Notification] and [NotificationEntry] objects. */ +internal object NotificationMemoryMeter { + + private const val CAR_EXTENSIONS = "android.car.EXTENSIONS" + private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon" + private const val TV_EXTENSIONS = "android.tv.EXTENSIONS" + private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS" + private const val WEARABLE_EXTENSIONS_BACKGROUND = "background" + + /** Returns a list of memory use entries for currently shown notifications. */ + @WorkerThread + fun notificationMemoryUse( + notifications: Collection<NotificationEntry>, + ): List<NotificationMemoryUsage> { + return notifications + .asSequence() + .map { entry -> + val packageName = entry.sbn.packageName + val notificationObjectUsage = + notificationMemoryUse(entry.sbn.notification, hashSetOf()) + val notificationViewUsage = NotificationMemoryViewWalker.getViewUsage(entry.row) + NotificationMemoryUsage( + packageName, + NotificationUtils.logKey(entry.sbn.key), + notificationObjectUsage, + notificationViewUsage + ) + } + .toList() + } + + @WorkerThread + fun notificationMemoryUse( + entry: NotificationEntry, + seenBitmaps: HashSet<Int> = hashSetOf(), + ): NotificationMemoryUsage { + return NotificationMemoryUsage( + entry.sbn.packageName, + NotificationUtils.logKey(entry.sbn.key), + notificationMemoryUse(entry.sbn.notification, seenBitmaps), + NotificationMemoryViewWalker.getViewUsage(entry.row) + ) + } + + /** + * Computes the estimated memory usage of a given [Notification] object. It'll attempt to + * inspect Bitmaps in the object and provide summary of memory usage. + */ + @WorkerThread + fun notificationMemoryUse( + notification: Notification, + seenBitmaps: HashSet<Int> = hashSetOf(), + ): NotificationObjectUsage { + val extras = notification.extras + val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps) + val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps) + + // Collect memory usage of extra styles + + // Big Picture + val bigPictureIconUse = + computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps) + val bigPictureUse = + computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) + + computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) + + // People + val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST) + val peopleUse = + peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0 + + // Calling + val callingPersonUse = + computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps) + val verificationIconUse = + computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps) + + // Messages + val messages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + extras.getParcelableArray(Notification.EXTRA_MESSAGES) + ) + val messagesUse = + messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } + val historicMessages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES) + ) + val historyicMessagesUse = + historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } + + // Extenders + val carExtender = extras.getBundle(CAR_EXTENSIONS) + val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0 + val carExtenderIcon = + computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps) + + val tvExtender = extras.getBundle(TV_EXTENSIONS) + val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0 + + val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS) + val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0 + val wearExtenderBackground = + computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps) + + val style = notification.notificationStyle + val hasCustomView = notification.contentView != null || notification.bigContentView != null + val extrasSize = computeBundleSize(extras) + + return NotificationObjectUsage( + smallIcon = smallIconUse, + largeIcon = largeIconUse, + extras = extrasSize, + style = style?.simpleName, + styleIcon = + bigPictureIconUse + + peopleUse + + callingPersonUse + + verificationIconUse + + messagesUse + + historyicMessagesUse, + bigPicture = bigPictureUse, + extender = + carExtenderSize + + carExtenderIcon + + tvExtenderSize + + wearExtenderSize + + wearExtenderBackground, + hasCustomView = hasCustomView + ) + } + + /** + * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem + * bitmaps). Can be slow. + */ + private fun computeBundleSize(extras: Bundle): Int { + val parcel = Parcel.obtain() + try { + extras.writeToParcel(parcel, 0) + return parcel.dataSize() + } finally { + parcel.recycle() + } + } + + /** + * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0 + * if the key does not exist in extras. + */ + private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int { + return when (val parcelable = extras?.getParcelable<Parcelable>(key)) { + is Bitmap -> computeBitmapUse(parcelable, seenBitmaps) + is Icon -> computeIconUse(parcelable, seenBitmaps) + is Person -> computeIconUse(parcelable.icon, seenBitmaps) + else -> 0 + } + } + + /** + * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is + * defined via Uri or a resource. + * + * @return memory usage in bytes or 0 if the icon is Uri/Resource based + */ + private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) = + when (icon?.type) { + Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) + Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) + Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps) + else -> 0 + } + + /** + * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of + * seenBitmaps set, this method returns 0 to avoid double counting. + * + * @return memory usage of the bitmap in bytes + */ + private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int { + val refId = System.identityHashCode(bitmap) + if (seenBitmaps?.contains(refId) == true) { + return 0 + } + + seenBitmaps?.add(refId) + return bitmap.allocationByteCount + } + + private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int { + val refId = System.identityHashCode(icon.dataBytes) + if (seenBitmaps.contains(refId)) { + return 0 + } + + seenBitmaps.add(refId) + return icon.dataLength + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt index 958978ecd858..c09cc4306ced 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt @@ -17,22 +17,11 @@ package com.android.systemui.statusbar.notification.logging -import android.app.Notification -import android.app.Person -import android.graphics.Bitmap -import android.graphics.drawable.Icon -import android.os.Bundle -import android.os.Parcel -import android.os.Parcelable import android.util.Log -import androidx.annotation.WorkerThread -import androidx.core.util.contains import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager -import com.android.systemui.statusbar.notification.NotificationUtils import com.android.systemui.statusbar.notification.collection.NotifPipeline -import com.android.systemui.statusbar.notification.collection.NotificationEntry import java.io.PrintWriter import javax.inject.Inject @@ -46,12 +35,7 @@ constructor( ) : Dumpable { companion object { - private const val TAG = "NotificationMemMonitor" - private const val CAR_EXTENSIONS = "android.car.EXTENSIONS" - private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon" - private const val TV_EXTENSIONS = "android.tv.EXTENSIONS" - private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS" - private const val WEARABLE_EXTENSIONS_BACKGROUND = "background" + private const val TAG = "NotificationMemory" } fun init() { @@ -60,184 +44,123 @@ constructor( } override fun dump(pw: PrintWriter, args: Array<out String>) { - currentNotificationMemoryUse().forEach { use -> pw.println(use.toString()) } + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(notificationPipeline.allNotifs) + .sortedWith(compareBy({ it.packageName }, { it.notificationKey })) + dumpNotificationObjects(pw, memoryUse) + dumpNotificationViewUsage(pw, memoryUse) } - @WorkerThread - fun currentNotificationMemoryUse(): List<NotificationMemoryUsage> { - return notificationMemoryUse(notificationPipeline.allNotifs) - } - - /** Returns a list of memory use entries for currently shown notifications. */ - @WorkerThread - fun notificationMemoryUse( - notifications: Collection<NotificationEntry> - ): List<NotificationMemoryUsage> { - return notifications - .asSequence() - .map { entry -> - val packageName = entry.sbn.packageName - val notificationObjectUsage = - computeNotificationObjectUse(entry.sbn.notification, hashSetOf()) - NotificationMemoryUsage( - packageName, - NotificationUtils.logKey(entry.sbn.key), - notificationObjectUsage - ) - } - .toList() - } - - /** - * Computes the estimated memory usage of a given [Notification] object. It'll attempt to - * inspect Bitmaps in the object and provide summary of memory usage. - */ - private fun computeNotificationObjectUse( - notification: Notification, - seenBitmaps: HashSet<Int> - ): NotificationObjectUsage { - val extras = notification.extras - val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps) - val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps) - - // Collect memory usage of extra styles - - // Big Picture - val bigPictureIconUse = - computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) + - computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps) - val bigPictureUse = - computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) + - computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) - - // People - val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST) - val peopleUse = - peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0 - - // Calling - val callingPersonUse = - computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps) - val verificationIconUse = - computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps) - - // Messages - val messages = - Notification.MessagingStyle.Message.getMessagesFromBundleArray( - extras.getParcelableArray(Notification.EXTRA_MESSAGES) - ) - val messagesUse = - messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } - val historicMessages = - Notification.MessagingStyle.Message.getMessagesFromBundleArray( - extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES) + /** Renders a table of notification object usage into passed [PrintWriter]. */ + private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List<NotificationMemoryUsage>) { + pw.println("Notification Object Usage") + pw.println("-----------") + pw.println( + "Package".padEnd(35) + + "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom" + ) + pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView") + pw.println() + + memoryUse.forEach { use -> + pw.println( + use.packageName.padEnd(35) + + "\t\t" + + "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" + + (use.objectUsage.style?.take(15) ?: "").padEnd(15) + + "\t\t${use.objectUsage.styleIcon}\t" + + "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" + + "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" + + use.notificationKey ) - val historyicMessagesUse = - historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } - - // Extenders - val carExtender = extras.getBundle(CAR_EXTENSIONS) - val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0 - val carExtenderIcon = - computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps) - - val tvExtender = extras.getBundle(TV_EXTENSIONS) - val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0 - - val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS) - val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0 - val wearExtenderBackground = - computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps) - - val style = notification.notificationStyle - val hasCustomView = notification.contentView != null || notification.bigContentView != null - val extrasSize = computeBundleSize(extras) - - return NotificationObjectUsage( - smallIconUse, - largeIconUse, - extrasSize, - style?.simpleName, - bigPictureIconUse + - peopleUse + - callingPersonUse + - verificationIconUse + - messagesUse + - historyicMessagesUse, - bigPictureUse, - carExtenderSize + - carExtenderIcon + - tvExtenderSize + - wearExtenderSize + - wearExtenderBackground, - hasCustomView + } + + // Calculate totals for easily glanceable summary. + data class Totals( + var smallIcon: Int = 0, + var largeIcon: Int = 0, + var styleIcon: Int = 0, + var bigPicture: Int = 0, + var extender: Int = 0, + var extras: Int = 0, ) - } - /** - * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem - * bitmaps). Can be slow. - */ - private fun computeBundleSize(extras: Bundle): Int { - val parcel = Parcel.obtain() - try { - extras.writeToParcel(parcel, 0) - return parcel.dataSize() - } finally { - parcel.recycle() - } - } + val totals = + memoryUse.fold(Totals()) { t, usage -> + t.smallIcon += usage.objectUsage.smallIcon + t.largeIcon += usage.objectUsage.largeIcon + t.styleIcon += usage.objectUsage.styleIcon + t.bigPicture += usage.objectUsage.bigPicture + t.extender += usage.objectUsage.extender + t.extras += usage.objectUsage.extras + t + } - /** - * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0 - * if the key does not exist in extras. - */ - private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int { - return when (val parcelable = extras?.getParcelable<Parcelable>(key)) { - is Bitmap -> computeBitmapUse(parcelable, seenBitmaps) - is Icon -> computeIconUse(parcelable, seenBitmaps) - is Person -> computeIconUse(parcelable.icon, seenBitmaps) - else -> 0 - } + pw.println() + pw.println("TOTALS") + pw.println( + "".padEnd(35) + + "\t\t" + + "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" + + "".padEnd(15) + + "\t\t${toKb(totals.styleIcon)}\t" + + "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" + + toKb(totals.extras) + ) + pw.println() } - /** - * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is - * defined via Uri or a resource. - * - * @return memory usage in bytes or 0 if the icon is Uri/Resource based - */ - private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) = - when (icon?.type) { - Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) - Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) - Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps) - else -> 0 - } - - /** - * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of - * seenBitmaps set, this method returns 0 to avoid double counting. - * - * @return memory usage of the bitmap in bytes - */ - private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int { - val refId = System.identityHashCode(bitmap) - if (seenBitmaps?.contains(refId) == true) { - return 0 - } + /** Renders a table of notification view usage into passed [PrintWriter] */ + private fun dumpNotificationViewUsage( + pw: PrintWriter, + memoryUse: List<NotificationMemoryUsage>, + ) { + + data class Totals( + var smallIcon: Int = 0, + var largeIcon: Int = 0, + var style: Int = 0, + var customViews: Int = 0, + var softwareBitmapsPenalty: Int = 0, + ) - seenBitmaps?.add(refId) - return bitmap.allocationByteCount + val totals = Totals() + pw.println("Notification View Usage") + pw.println("-----------") + pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware") + pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps") + pw.println() + memoryUse + .filter { it.viewUsage.isNotEmpty() } + .forEach { use -> + pw.println(use.packageName + " " + use.notificationKey) + use.viewUsage.forEach { view -> + pw.println( + " ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" + + "\t${view.largeIcon}\t${view.style}" + + "\t${view.customViews}\t${view.softwareBitmapsPenalty}" + ) + + if (view.viewType == ViewType.TOTAL) { + totals.smallIcon += view.smallIcon + totals.largeIcon += view.largeIcon + totals.style += view.style + totals.customViews += view.customViews + totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty + } + } + } + pw.println() + pw.println("TOTALS") + pw.println( + " ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" + + "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" + + "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}" + ) + pw.println() } - private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int { - val refId = System.identityHashCode(icon.dataBytes) - if (seenBitmaps.contains(refId)) { - return 0 - } - - seenBitmaps.add(refId) - return icon.dataLength + private fun toKb(bytes: Int): String { + return (bytes / 1024).toString() + " KB" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt new file mode 100644 index 000000000000..a0bee1502f51 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt @@ -0,0 +1,173 @@ +package com.android.systemui.statusbar.notification.logging + +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import com.android.internal.R +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.util.children + +/** Walks view hiearchy of a given notification to estimate its memory use. */ +internal object NotificationMemoryViewWalker { + + private const val TAG = "NotificationMemory" + + /** Builder for [NotificationViewUsage] objects. */ + private class UsageBuilder { + private var smallIcon: Int = 0 + private var largeIcon: Int = 0 + private var systemIcons: Int = 0 + private var style: Int = 0 + private var customViews: Int = 0 + private var softwareBitmaps = 0 + + fun addSmallIcon(smallIconUse: Int) = apply { smallIcon += smallIconUse } + fun addLargeIcon(largeIconUse: Int) = apply { largeIcon += largeIconUse } + fun addSystem(systemIconUse: Int) = apply { systemIcons += systemIconUse } + fun addStyle(styleUse: Int) = apply { style += styleUse } + fun addSoftwareBitmapPenalty(softwareBitmapUse: Int) = apply { + softwareBitmaps += softwareBitmapUse + } + + fun addCustomViews(customViewsUse: Int) = apply { customViews += customViewsUse } + + fun build(viewType: ViewType): NotificationViewUsage { + return NotificationViewUsage( + viewType = viewType, + smallIcon = smallIcon, + largeIcon = largeIcon, + systemIcons = systemIcons, + style = style, + customViews = customViews, + softwareBitmapsPenalty = softwareBitmaps, + ) + } + } + + /** + * Returns memory usage of public and private views contained in passed + * [ExpandableNotificationRow] + */ + fun getViewUsage(row: ExpandableNotificationRow?): List<NotificationViewUsage> { + if (row == null) { + return listOf() + } + + // The ordering here is significant since it determines deduplication of seen drawables. + return listOf( + getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild), + getViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, row.privateLayout?.contractedChild), + getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild), + getViewUsage(ViewType.PUBLIC_VIEW, row.publicLayout), + getTotalUsage(row) + ) + } + + /** + * Calculate total usage of all views - we need to do a separate traversal to make sure we don't + * double count fields. + */ + private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage { + val totalUsage = UsageBuilder() + val seenObjects = hashSetOf<Int>() + + row.publicLayout?.let { computeViewHierarchyUse(it, totalUsage, seenObjects) } + row.privateLayout?.let { child -> + for (view in listOf(child.expandedChild, child.contractedChild, child.headsUpChild)) { + (view as? ViewGroup)?.let { v -> + computeViewHierarchyUse(v, totalUsage, seenObjects) + } + } + } + return totalUsage.build(ViewType.TOTAL) + } + + private fun getViewUsage( + type: ViewType, + rootView: View?, + seenObjects: HashSet<Int> = hashSetOf() + ): NotificationViewUsage { + val usageBuilder = UsageBuilder() + (rootView as? ViewGroup)?.let { computeViewHierarchyUse(it, usageBuilder, seenObjects) } + return usageBuilder.build(type) + } + + private fun computeViewHierarchyUse( + rootView: ViewGroup, + builder: UsageBuilder, + seenObjects: HashSet<Int> = hashSetOf(), + ) { + for (child in rootView.children) { + if (child is ViewGroup) { + computeViewHierarchyUse(child, builder, seenObjects) + } else { + computeViewUse(child, builder, seenObjects) + } + } + } + + private fun computeViewUse(view: View, builder: UsageBuilder, seenObjects: HashSet<Int>) { + if (view !is ImageView) return + val drawable = view.drawable ?: return + val drawableRef = System.identityHashCode(drawable) + if (seenObjects.contains(drawableRef)) return + val drawableUse = computeDrawableUse(drawable, seenObjects) + // TODO(b/235451049): We need to make sure we traverse large icon before small icon - + // sometimes the large icons are assigned to small icon views and we want to + // attribute them to large view in those cases. + when (view.id) { + R.id.left_icon, + R.id.icon, + R.id.conversation_icon -> builder.addSmallIcon(drawableUse) + R.id.right_icon -> builder.addLargeIcon(drawableUse) + R.id.big_picture -> builder.addStyle(drawableUse) + // Elements that are part of platform with resources + R.id.phishing_alert, + R.id.feedback, + R.id.alerted_icon, + R.id.expand_button_icon, + R.id.remote_input_send -> builder.addSystem(drawableUse) + // Custom view ImageViews + else -> { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Custom view: ${identifierForView(view)}") + } + builder.addCustomViews(drawableUse) + } + } + + if (isDrawableSoftwareBitmap(drawable)) { + builder.addSoftwareBitmapPenalty(drawableUse) + } + + seenObjects.add(drawableRef) + } + + private fun computeDrawableUse(drawable: Drawable, seenObjects: HashSet<Int>): Int = + when (drawable) { + is BitmapDrawable -> { + val ref = System.identityHashCode(drawable.bitmap) + if (seenObjects.contains(ref)) { + 0 + } else { + seenObjects.add(ref) + drawable.bitmap.allocationByteCount + } + } + else -> 0 + } + + private fun isDrawableSoftwareBitmap(drawable: Drawable) = + drawable is BitmapDrawable && drawable.bitmap.config != Bitmap.Config.HARDWARE + + private fun identifierForView(view: View) = + if (view.id == View.NO_ID) { + "no-id" + } else { + view.resources.getResourceName(view.id) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt index fe03b2ad6a32..10197a38527e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.logging -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationRenderLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.stack.NotificationSection diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 755e3e1a208e..d29298a2f637 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -613,22 +613,21 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView protected void resetAllContentAlphas() {} @Override - protected void applyRoundness() { + public void applyRoundness() { super.applyRoundness(); - applyBackgroundRoundness(getCurrentBackgroundRadiusTop(), - getCurrentBackgroundRadiusBottom()); + applyBackgroundRoundness(getTopCornerRadius(), getBottomCornerRadius()); } @Override - public float getCurrentBackgroundRadiusTop() { + public float getTopCornerRadius() { float fraction = getInterpolatedAppearAnimationFraction(); - return MathUtils.lerp(0, super.getCurrentBackgroundRadiusTop(), fraction); + return MathUtils.lerp(0, super.getTopCornerRadius(), fraction); } @Override - public float getCurrentBackgroundRadiusBottom() { + public float getBottomCornerRadius() { float fraction = getInterpolatedAppearAnimationFraction(); - return MathUtils.lerp(0, super.getCurrentBackgroundRadiusBottom(), fraction); + return MathUtils.lerp(0, super.getBottomCornerRadius(), fraction); } private void applyBackgroundRoundness(float topRadius, float bottomRadius) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 1b006485c83d..9e7717caf69c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -93,6 +93,7 @@ import com.android.systemui.statusbar.notification.LaunchAnimationParameters; import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController; import com.android.systemui.statusbar.notification.NotificationUtils; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; @@ -154,7 +155,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView void onLayout(); } - /** Listens for changes to the expansion state of this row. */ + /** + * Listens for changes to the expansion state of this row. + */ public interface OnExpansionChangedListener { void onExpansionChanged(boolean isExpanded); } @@ -183,22 +186,34 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private int mNotificationLaunchHeight; private boolean mMustStayOnScreen; - /** Does this row contain layouts that can adapt to row expansion */ + /** + * Does this row contain layouts that can adapt to row expansion + */ private boolean mExpandable; - /** Has the user actively changed the expansion state of this row */ + /** + * Has the user actively changed the expansion state of this row + */ private boolean mHasUserChangedExpansion; - /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */ + /** + * If {@link #mHasUserChangedExpansion}, has the user expanded this row + */ private boolean mUserExpanded; - /** Whether the blocking helper is showing on this notification (even if dismissed) */ + /** + * Whether the blocking helper is showing on this notification (even if dismissed) + */ private boolean mIsBlockingHelperShowing; /** * Has this notification been expanded while it was pinned */ private boolean mExpandedWhenPinned; - /** Is the user touching this row */ + /** + * Is the user touching this row + */ private boolean mUserLocked; - /** Are we showing the "public" version */ + /** + * Are we showing the "public" version + */ private boolean mShowingPublic; private boolean mSensitive; private boolean mSensitiveHiddenInGeneral; @@ -351,11 +366,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mWasChildInGroupWhenRemoved; private NotificationInlineImageResolver mImageResolver; private NotificationMediaManager mMediaManager; - @Nullable private OnExpansionChangedListener mExpansionChangedListener; - @Nullable private Runnable mOnIntrinsicHeightReachedRunnable; + @Nullable + private OnExpansionChangedListener mExpansionChangedListener; + @Nullable + private Runnable mOnIntrinsicHeightReachedRunnable; private float mTopRoundnessDuringLaunchAnimation; private float mBottomRoundnessDuringLaunchAnimation; + private boolean mIsNotificationGroupCornerEnabled; /** * Returns whether the given {@code statusBarNotification} is a system notification. @@ -574,14 +592,18 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - /** Called when the notification's ranking was changed (but nothing else changed). */ + /** + * Called when the notification's ranking was changed (but nothing else changed). + */ public void onNotificationRankingUpdated() { if (mMenuRow != null) { mMenuRow.onNotificationUpdated(mEntry.getSbn()); } } - /** Call when bubble state has changed and the button on the notification should be updated. */ + /** + * Call when bubble state has changed and the button on the notification should be updated. + */ public void updateBubbleButton() { for (NotificationContentView l : mLayouts) { l.updateBubbleButton(mEntry); @@ -620,6 +642,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Sets a supplier that can determine whether the keyguard is secure or not. + * * @param secureStateProvider A function that returns true if keyguard is secure. */ public void setSecureStateProvider(BooleanSupplier secureStateProvider) { @@ -781,7 +804,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.setUntruncatedChildCount(childCount); } - /** Called after children have been attached to set the expansion states */ + /** + * Called after children have been attached to set the expansion states + */ public void resetChildSystemExpandedStates() { if (isSummaryWithChildren()) { mChildrenContainer.updateExpansionStates(); @@ -791,7 +816,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Add a child notification to this view. * - * @param row the row to add + * @param row the row to add * @param childIndex the index to add it at, if -1 it will be added at the end */ public void addChildNotification(ExpandableNotificationRow row, int childIndex) { @@ -809,10 +834,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } onAttachedChildrenCountChanged(); row.setIsChildInGroup(false, null); - row.setBottomRoundness(0.0f, false /* animate */); + row.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue); } - /** Returns the child notification at [index], or null if no such child. */ + /** + * Returns the child notification at [index], or null if no such child. + */ @Nullable public ExpandableNotificationRow getChildNotificationAt(int index) { if (mChildrenContainer == null @@ -834,7 +861,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * @param isChildInGroup Is this notification now in a group - * @param parent the new parent notification + * @param parent the new parent notification */ public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) { if (mExpandAnimationRunning && !isChildInGroup && mNotificationParent != null) { @@ -898,7 +925,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mChildrenContainer == null ? null : mChildrenContainer.getAttachedChildren(); } - /** Updates states of all children. */ + /** + * Updates states of all children. + */ public void updateChildrenStates(AmbientState ambientState) { if (mIsSummaryWithChildren) { ExpandableViewState parentState = getViewState(); @@ -906,21 +935,27 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - /** Applies children states. */ + /** + * Applies children states. + */ public void applyChildrenState() { if (mIsSummaryWithChildren) { mChildrenContainer.applyState(); } } - /** Prepares expansion changed. */ + /** + * Prepares expansion changed. + */ public void prepareExpansionChanged() { if (mIsSummaryWithChildren) { mChildrenContainer.prepareExpansionChanged(); } } - /** Starts child animations. */ + /** + * Starts child animations. + */ public void startChildAnimation(AnimationProperties properties) { if (mIsSummaryWithChildren) { mChildrenContainer.startAnimationToState(properties); @@ -984,7 +1019,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren) { return mChildrenContainer.getIntrinsicHeight(); } - if(mExpandedWhenPinned) { + if (mExpandedWhenPinned) { return Math.max(getMaxExpandHeight(), getHeadsUpHeight()); } else if (atLeastMinHeight) { return Math.max(getCollapsedHeight(), getHeadsUpHeight()); @@ -1079,18 +1114,22 @@ public class ExpandableNotificationRow extends ActivatableNotificationView updateClickAndFocus(); } - /** The click listener for the bubble button. */ + /** + * The click listener for the bubble button. + */ public View.OnClickListener getBubbleClickListener() { return v -> { if (mBubblesManagerOptional.isPresent()) { mBubblesManagerOptional.get() - .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */); + .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */); } mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */); }; } - /** The click listener for the snooze button. */ + /** + * The click listener for the snooze button. + */ public View.OnClickListener getSnoozeClickListener(MenuItem item) { return v -> { // Dismiss a snoozed notification if one is still left behind @@ -1252,7 +1291,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public void setContentBackground(int customBackgroundColor, boolean animate, - NotificationContentView notificationContentView) { + NotificationContentView notificationContentView) { if (getShowingLayout() == notificationContentView) { setTintColor(customBackgroundColor, animate); } @@ -1487,7 +1526,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView l.setAlpha(alpha); } if (mChildrenContainer != null) { - mChildrenContainer.setContentAlpha(alpha); + mChildrenContainer.setAlpha(alpha); } } @@ -1637,7 +1676,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView setTargetPoint(null); } - /** Shows the given feedback icon, or hides the icon if null. */ + /** + * Shows the given feedback icon, or hides the icon if null. + */ public void setFeedbackIcon(@Nullable FeedbackIcon icon) { if (mIsSummaryWithChildren) { mChildrenContainer.setFeedbackIcon(icon); @@ -1646,7 +1687,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mPublicLayout.setFeedbackIcon(icon); } - /** Sets the last time the notification being displayed audibly alerted the user. */ + /** + * Sets the last time the notification being displayed audibly alerted the user. + */ public void setLastAudiblyAlertedMs(long lastAudiblyAlertedMs) { long timeSinceAlertedAudibly = System.currentTimeMillis() - lastAudiblyAlertedMs; boolean alertedRecently = timeSinceAlertedAudibly < RECENTLY_ALERTED_THRESHOLD_MS; @@ -1700,7 +1743,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView Trace.endSection(); } - /** Generates and appends "(MessagingStyle)" type tag to passed string for tracing. */ + /** + * Generates and appends "(MessagingStyle)" type tag to passed string for tracing. + */ @NonNull private String appendTraceStyleTag(@NonNull String traceTag) { if (!Trace.isEnabled()) { @@ -1721,7 +1766,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView super.onFinishInflate(); mPublicLayout = findViewById(R.id.expandedPublic); mPrivateLayout = findViewById(R.id.expanded); - mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout}; + mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout}; for (NotificationContentView l : mLayouts) { l.setExpandClickListener(mExpandClickListener); @@ -1740,6 +1785,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.setIsLowPriority(mIsLowPriority); mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this); mChildrenContainer.onNotificationUpdated(); + mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled); mTranslateableViews.add(mChildrenContainer); }); @@ -1796,6 +1842,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Perform a smart action which triggers a longpress (expose guts). * Based on the semanticAction passed, may update the state of the guts view. + * * @param semanticAction associated with this smart action click */ public void doSmartActionClick(int x, int y, int semanticAction) { @@ -1939,9 +1986,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Set the dismiss behavior of the view. + * * @param usingRowTranslationX {@code true} if the view should translate using regular - * translationX, otherwise the contents will be - * translated. + * translationX, otherwise the contents will be + * translated. */ @Override public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) { @@ -1955,6 +2003,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (previousTranslation != 0) { setTranslation(previousTranslation); } + if (mChildrenContainer != null) { + List<ExpandableNotificationRow> notificationChildren = + mChildrenContainer.getAttachedChildren(); + for (int i = 0; i < notificationChildren.size(); i++) { + ExpandableNotificationRow child = notificationChildren.get(i); + child.setDismissUsingRowTranslationX(usingRowTranslationX); + } + } } } @@ -2009,7 +2065,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public Animator getTranslateViewAnimator(final float leftTarget, - AnimatorUpdateListener listener) { + AnimatorUpdateListener listener) { if (mTranslateAnim != null) { mTranslateAnim.cancel(); } @@ -2115,7 +2171,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView NotificationLaunchAnimatorController.ANIMATION_DURATION_TOP_ROUNDING)); float startTop = params.getStartNotificationTop(); top = (int) Math.min(MathUtils.lerp(startTop, - params.getTop(), expandProgress), + params.getTop(), expandProgress), startTop); } else { top = params.getTop(); @@ -2151,29 +2207,30 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } setTranslationY(top); - mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / mOutlineRadius; - mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / mOutlineRadius; + final float maxRadius = getMaxRadius(); + mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / maxRadius; + mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / maxRadius; invalidateOutline(); mBackgroundNormal.setExpandAnimationSize(params.getWidth(), actualHeight); } @Override - public float getCurrentTopRoundness() { + public float getTopRoundness() { if (mExpandAnimationRunning) { return mTopRoundnessDuringLaunchAnimation; } - return super.getCurrentTopRoundness(); + return super.getTopRoundness(); } @Override - public float getCurrentBottomRoundness() { + public float getBottomRoundness() { if (mExpandAnimationRunning) { return mBottomRoundnessDuringLaunchAnimation; } - return super.getCurrentBottomRoundness(); + return super.getBottomRoundness(); } public void setExpandAnimationRunning(boolean expandAnimationRunning) { @@ -2284,7 +2341,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Set this notification to be expanded by the user * - * @param userExpanded whether the user wants this notification to be expanded + * @param userExpanded whether the user wants this notification to be expanded * @param allowChildExpansion whether a call to this method allows expanding children */ public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) { @@ -2434,7 +2491,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * @return {@code true} if the notification can show it's heads up layout. This is mostly true - * except for legacy use cases. + * except for legacy use cases. */ public boolean canShowHeadsUp() { if (mOnKeyguard && !isDozing() && !isBypassEnabled()) { @@ -2625,7 +2682,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public void setHideSensitive(boolean hideSensitive, boolean animated, long delay, - long duration) { + long duration) { if (getVisibility() == GONE) { // If we are GONE, the hideSensitive parameter will not be calculated and always be // false, which is incorrect, let's wait until a real call comes in later. @@ -2658,9 +2715,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void animateShowingPublic(long delay, long duration, boolean showingPublic) { View[] privateViews = mIsSummaryWithChildren - ? new View[] {mChildrenContainer} - : new View[] {mPrivateLayout}; - View[] publicViews = new View[] {mPublicLayout}; + ? new View[]{mChildrenContainer} + : new View[]{mPrivateLayout}; + View[] publicViews = new View[]{mPublicLayout}; View[] hiddenChildren = showingPublic ? privateViews : publicViews; View[] shownChildren = showingPublic ? publicViews : privateViews; for (final View hiddenView : hiddenChildren) { @@ -2693,8 +2750,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * @return Whether this view is allowed to be dismissed. Only valid for visible notifications as - * otherwise some state might not be updated. To request about the general clearability - * see {@link NotificationEntry#isDismissable()}. + * otherwise some state might not be updated. To request about the general clearability + * see {@link NotificationEntry#isDismissable()}. */ public boolean canViewBeDismissed() { return mEntry.isDismissable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral); @@ -2777,8 +2834,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } @Override - public long performRemoveAnimation(long duration, long delay, float translationDirection, - boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable, + public long performRemoveAnimation( + long duration, + long delay, + float translationDirection, + boolean isHeadsUpAnimation, + float endLocation, + Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener) { if (mMenuRow != null && mMenuRow.isMenuVisible()) { Animator anim = getTranslateViewAnimator(0f, null /* listener */); @@ -2828,7 +2890,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - /** Gets the last value set with {@link #setNotificationFaded(boolean)} */ + /** + * Gets the last value set with {@link #setNotificationFaded(boolean)} + */ @Override public boolean isNotificationFaded() { return mIsFaded; @@ -2843,7 +2907,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * notifications return false from {@link #hasOverlappingRendering()} and delegate the * layerType to child views which really need it in order to render correctly, such as icon * views or the conversation face pile. - * + * <p> * Another compounding factor for notifications is that we change clipping on each frame of the * animation, so the hardware layer isn't able to do any caching at the top level, but the * individual elements we render with hardware layers (e.g. icons) cache wonderfully because we @@ -2869,7 +2933,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - /** Private helper for iterating over the layouts and children containers to set faded state */ + /** + * Private helper for iterating over the layouts and children containers to set faded state + */ private void setNotificationFadedOnChildren(boolean faded) { delegateNotificationFaded(mChildrenContainer, faded); for (NotificationContentView layout : mLayouts) { @@ -2897,7 +2963,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * Because RemoteInputView is designed to be an opaque view that overlaps the Actions row, the * row should require overlapping rendering to ensure that the overlapped view doesn't bleed * through when alpha fading. - * + * <p> * Note that this currently works for top-level notifications which squish their height down * while collapsing the shade, but does not work for children inside groups, because the * accordion affect does not apply to those views, so super.hasOverlappingRendering() will @@ -2976,7 +3042,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mGuts.getIntrinsicHeight(); } else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp && mHeadsUpManager.isTrackingHeadsUp()) { - return getPinnedHeadsUpHeight(false /* atLeastMinHeight */); + return getPinnedHeadsUpHeight(false /* atLeastMinHeight */); } else if (mIsSummaryWithChildren && !isGroupExpanded() && !shouldShowPublic()) { return mChildrenContainer.getMinHeight(); } else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp) { @@ -3218,8 +3284,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView MenuItem snoozeMenu = provider.getSnoozeMenuItem(getContext()); if (snoozeMenu != null) { AccessibilityAction action = new AccessibilityAction(R.id.action_snooze, - getContext().getResources() - .getString(R.string.notification_menu_snooze_action)); + getContext().getResources() + .getString(R.string.notification_menu_snooze_action)); info.addAction(action); } } @@ -3280,17 +3346,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView NotificationContentView contentView = (NotificationContentView) child; if (isClippingNeeded()) { return true; - } else if (!hasNoRounding() - && contentView.shouldClipToRounding(getCurrentTopRoundness() != 0.0f, - getCurrentBottomRoundness() != 0.0f)) { + } else if (hasRoundedCorner() + && contentView.shouldClipToRounding(getTopRoundness() != 0.0f, + getBottomRoundness() != 0.0f)) { return true; } } else if (child == mChildrenContainer) { - if (isClippingNeeded() || !hasNoRounding()) { + if (isClippingNeeded() || hasRoundedCorner()) { return true; } } else if (child instanceof NotificationGuts) { - return !hasNoRounding(); + return hasRoundedCorner(); } return super.childNeedsClipping(child); } @@ -3316,14 +3382,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } @Override - protected void applyRoundness() { + public void applyRoundness() { super.applyRoundness(); applyChildrenRoundness(); } private void applyChildrenRoundness() { if (mIsSummaryWithChildren) { - mChildrenContainer.setCurrentBottomRoundness(getCurrentBottomRoundness()); + mChildrenContainer.requestBottomRoundness( + getBottomRoundness(), + /* animate = */ false, + SourceType.DefaultValue); } } @@ -3335,10 +3404,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return super.getCustomClipPath(child); } - private boolean hasNoRounding() { - return getCurrentBottomRoundness() == 0.0f && getCurrentTopRoundness() == 0.0f; - } - public boolean isMediaRow() { return mEntry.getSbn().getNotification().isMediaNotification(); } @@ -3434,6 +3499,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public interface LongPressListener { /** * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates + * * @return whether the longpress was handled */ boolean onLongPress(View v, int x, int y, MenuItem item); @@ -3455,6 +3521,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public interface CoordinateOnClickListener { /** * Equivalent to {@link View.OnClickListener#onClick(View)} with coordinates + * * @return whether the click was handled */ boolean onClick(View v, int x, int y, MenuItem item); @@ -3511,7 +3578,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void setTargetPoint(Point p) { mTargetPoint = p; } + public Point getTargetPoint() { return mTargetPoint; } + + /** + * Enable the support for rounded corner in notification group + * @param enabled true if is supported + */ + public void enableNotificationGroupCorner(boolean enabled) { + mIsNotificationGroupCornerEnabled = enabled; + if (mChildrenContainer != null) { + mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index a493a676e3d8..842526ee0371 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -231,6 +231,8 @@ public class ExpandableNotificationRowController implements NotifViewController mStatusBarStateController.removeCallback(mStatusBarStateListener); } }); + mView.enableNotificationGroupCorner( + mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER)); } private final StatusBarStateController.StateListener mStatusBarStateListener = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java index d58fe3b3c4a3..4fde5d06f816 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java @@ -28,46 +28,21 @@ import android.view.View; import android.view.ViewOutlineProvider; import com.android.systemui.R; -import com.android.systemui.statusbar.notification.AnimatableProperty; -import com.android.systemui.statusbar.notification.PropertyAnimator; -import com.android.systemui.statusbar.notification.stack.AnimationProperties; -import com.android.systemui.statusbar.notification.stack.StackStateAnimator; +import com.android.systemui.statusbar.notification.RoundableState; +import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer; /** * Like {@link ExpandableView}, but setting an outline for the height and clipping. */ public abstract class ExpandableOutlineView extends ExpandableView { - private static final AnimatableProperty TOP_ROUNDNESS = AnimatableProperty.from( - "topRoundness", - ExpandableOutlineView::setTopRoundnessInternal, - ExpandableOutlineView::getCurrentTopRoundness, - R.id.top_roundess_animator_tag, - R.id.top_roundess_animator_end_tag, - R.id.top_roundess_animator_start_tag); - private static final AnimatableProperty BOTTOM_ROUNDNESS = AnimatableProperty.from( - "bottomRoundness", - ExpandableOutlineView::setBottomRoundnessInternal, - ExpandableOutlineView::getCurrentBottomRoundness, - R.id.bottom_roundess_animator_tag, - R.id.bottom_roundess_animator_end_tag, - R.id.bottom_roundess_animator_start_tag); - private static final AnimationProperties ROUNDNESS_PROPERTIES = - new AnimationProperties().setDuration( - StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS); + private RoundableState mRoundableState; private static final Path EMPTY_PATH = new Path(); - private final Rect mOutlineRect = new Rect(); - private final Path mClipPath = new Path(); private boolean mCustomOutline; private float mOutlineAlpha = -1f; - protected float mOutlineRadius; private boolean mAlwaysRoundBothCorners; private Path mTmpPath = new Path(); - private float mCurrentBottomRoundness; - private float mCurrentTopRoundness; - private float mBottomRoundness; - private float mTopRoundness; private int mBackgroundTop; /** @@ -80,8 +55,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { private final ViewOutlineProvider mProvider = new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { - if (!mCustomOutline && getCurrentTopRoundness() == 0.0f - && getCurrentBottomRoundness() == 0.0f && !mAlwaysRoundBothCorners) { + if (!mCustomOutline && !hasRoundedCorner() && !mAlwaysRoundBothCorners) { // Only when translating just the contents, does the outline need to be shifted. int translation = !mDismissUsingRowTranslationX ? (int) getTranslation() : 0; int left = Math.max(translation, 0); @@ -99,14 +73,18 @@ public abstract class ExpandableOutlineView extends ExpandableView { } }; + @Override + public RoundableState getRoundableState() { + return mRoundableState; + } + protected Path getClipPath(boolean ignoreTranslation) { int left; int top; int right; int bottom; int height; - float topRoundness = mAlwaysRoundBothCorners - ? mOutlineRadius : getCurrentBackgroundRadiusTop(); + float topRoundness = mAlwaysRoundBothCorners ? getMaxRadius() : getTopCornerRadius(); if (!mCustomOutline) { // The outline just needs to be shifted if we're translating the contents. Otherwise // it's already in the right place. @@ -130,12 +108,11 @@ public abstract class ExpandableOutlineView extends ExpandableView { if (height == 0) { return EMPTY_PATH; } - float bottomRoundness = mAlwaysRoundBothCorners - ? mOutlineRadius : getCurrentBackgroundRadiusBottom(); + float bottomRoundness = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius(); if (topRoundness + bottomRoundness > height) { float overShoot = topRoundness + bottomRoundness - height; - float currentTopRoundness = getCurrentTopRoundness(); - float currentBottomRoundness = getCurrentBottomRoundness(); + float currentTopRoundness = getTopRoundness(); + float currentBottomRoundness = getBottomRoundness(); topRoundness -= overShoot * currentTopRoundness / (currentTopRoundness + currentBottomRoundness); bottomRoundness -= overShoot * currentBottomRoundness @@ -145,8 +122,18 @@ public abstract class ExpandableOutlineView extends ExpandableView { return mTmpPath; } - public void getRoundedRectPath(int left, int top, int right, int bottom, - float topRoundness, float bottomRoundness, Path outPath) { + /** + * Add a round rect in {@code outPath} + * @param outPath destination path + */ + public void getRoundedRectPath( + int left, + int top, + int right, + int bottom, + float topRoundness, + float bottomRoundness, + Path outPath) { outPath.reset(); mTmpCornerRadii[0] = topRoundness; mTmpCornerRadii[1] = topRoundness; @@ -168,15 +155,28 @@ public abstract class ExpandableOutlineView extends ExpandableView { @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { canvas.save(); + Path clipPath = null; + Path childClipPath = null; if (childNeedsClipping(child)) { - Path clipPath = getCustomClipPath(child); + clipPath = getCustomClipPath(child); if (clipPath == null) { clipPath = getClipPath(false /* ignoreTranslation */); } - if (clipPath != null) { - canvas.clipPath(clipPath); + // If the notification uses "RowTranslationX" as dismiss behavior, we should clip the + // children instead. + if (mDismissUsingRowTranslationX && child instanceof NotificationChildrenContainer) { + childClipPath = clipPath; + clipPath = null; } } + + if (child instanceof NotificationChildrenContainer) { + ((NotificationChildrenContainer) child).setChildClipPath(childClipPath); + } + if (clipPath != null) { + canvas.clipPath(clipPath); + } + boolean result = super.drawChild(canvas, child, drawingTime); canvas.restore(); return result; @@ -207,73 +207,21 @@ public abstract class ExpandableOutlineView extends ExpandableView { private void initDimens() { Resources res = getResources(); - mOutlineRadius = res.getDimension(R.dimen.notification_shadow_radius); mAlwaysRoundBothCorners = res.getBoolean(R.bool.config_clipNotificationsToOutline); - if (!mAlwaysRoundBothCorners) { - mOutlineRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius); + float maxRadius; + if (mAlwaysRoundBothCorners) { + maxRadius = res.getDimension(R.dimen.notification_shadow_radius); + } else { + maxRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius); } + mRoundableState = new RoundableState(this, this, maxRadius); setClipToOutline(mAlwaysRoundBothCorners); } @Override - public boolean setTopRoundness(float topRoundness, boolean animate) { - if (mTopRoundness != topRoundness) { - float diff = Math.abs(topRoundness - mTopRoundness); - mTopRoundness = topRoundness; - boolean shouldAnimate = animate; - if (PropertyAnimator.isAnimating(this, TOP_ROUNDNESS) && diff > 0.5f) { - // Fail safe: - // when we've been animating previously and we're now getting an update in the - // other direction, make sure to animate it too, otherwise, the localized updating - // may make the start larger than 1.0. - shouldAnimate = true; - } - PropertyAnimator.setProperty(this, TOP_ROUNDNESS, topRoundness, - ROUNDNESS_PROPERTIES, shouldAnimate); - return true; - } - return false; - } - - protected void applyRoundness() { + public void applyRoundness() { invalidateOutline(); - invalidate(); - } - - public float getCurrentBackgroundRadiusTop() { - return getCurrentTopRoundness() * mOutlineRadius; - } - - public float getCurrentTopRoundness() { - return mCurrentTopRoundness; - } - - public float getCurrentBottomRoundness() { - return mCurrentBottomRoundness; - } - - public float getCurrentBackgroundRadiusBottom() { - return getCurrentBottomRoundness() * mOutlineRadius; - } - - @Override - public boolean setBottomRoundness(float bottomRoundness, boolean animate) { - if (mBottomRoundness != bottomRoundness) { - float diff = Math.abs(bottomRoundness - mBottomRoundness); - mBottomRoundness = bottomRoundness; - boolean shouldAnimate = animate; - if (PropertyAnimator.isAnimating(this, BOTTOM_ROUNDNESS) && diff > 0.5f) { - // Fail safe: - // when we've been animating previously and we're now getting an update in the - // other direction, make sure to animate it too, otherwise, the localized updating - // may make the start larger than 1.0. - shouldAnimate = true; - } - PropertyAnimator.setProperty(this, BOTTOM_ROUNDNESS, bottomRoundness, - ROUNDNESS_PROPERTIES, shouldAnimate); - return true; - } - return false; + super.applyRoundness(); } protected void setBackgroundTop(int backgroundTop) { @@ -283,16 +231,6 @@ public abstract class ExpandableOutlineView extends ExpandableView { } } - private void setTopRoundnessInternal(float topRoundness) { - mCurrentTopRoundness = topRoundness; - applyRoundness(); - } - - private void setBottomRoundnessInternal(float bottomRoundness) { - mCurrentBottomRoundness = bottomRoundness; - applyRoundness(); - } - public void onDensityOrFontScaleChanged() { initDimens(); applyRoundness(); @@ -348,9 +286,10 @@ public abstract class ExpandableOutlineView extends ExpandableView { /** * Set the dismiss behavior of the view. + * * @param usingRowTranslationX {@code true} if the view should translate using regular - * translationX, otherwise the contents will be - * translated. + * translationX, otherwise the contents will be + * translated. */ public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) { mDismissUsingRowTranslationX = usingRowTranslationX; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index 38f0c550d4fc..955d7c18f870 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -36,6 +36,8 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.statusbar.StatusBarIconView; +import com.android.systemui.statusbar.notification.Roundable; +import com.android.systemui.statusbar.notification.RoundableState; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.util.DumpUtilsKt; @@ -47,9 +49,10 @@ import java.util.List; /** * An abstract view for expandable views. */ -public abstract class ExpandableView extends FrameLayout implements Dumpable { +public abstract class ExpandableView extends FrameLayout implements Dumpable, Roundable { private static final String TAG = "ExpandableView"; + private RoundableState mRoundableState = null; protected OnHeightChangedListener mOnHeightChangedListener; private int mActualHeight; protected int mClipTopAmount; @@ -78,6 +81,14 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { initDimens(); } + @Override + public RoundableState getRoundableState() { + if (mRoundableState == null) { + mRoundableState = new RoundableState(this, this, 0f); + } + return mRoundableState; + } + private void initDimens() { mContentShift = getResources().getDimensionPixelSize( R.dimen.shelf_transform_content_shift); @@ -440,8 +451,7 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { int top = getClipTopAmount(); int bottom = Math.max(Math.max(getActualHeight() + getExtraBottomPadding() - mClipBottomAmount, top), mMinimumHeightForClipping); - int halfExtraWidth = (int) (mExtraWidthForClipping / 2.0f); - mClipRect.set(-halfExtraWidth, top, getWidth() + halfExtraWidth, bottom); + mClipRect.set(Integer.MIN_VALUE, top, Integer.MAX_VALUE, bottom); setClipBounds(mClipRect); } else { setClipBounds(null); @@ -455,7 +465,6 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { public void setExtraWidthForClipping(float extraWidthForClipping) { mExtraWidthForClipping = extraWidthForClipping; - updateClipping(); } public float getHeaderVisibleAmount() { @@ -844,22 +853,6 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { return mFirstInSection; } - /** - * Set the topRoundness of this view. - * @return Whether the roundness was changed. - */ - public boolean setTopRoundness(float topRoundness, boolean animate) { - return false; - } - - /** - * Set the bottom roundness of this view. - * @return Whether the roundness was changed. - */ - public boolean setBottomRoundness(float bottomRoundness, boolean animate) { - return false; - } - public int getHeadsUpHeightWithoutHeader() { return getHeight(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt index ab91926d466a..46fef3f973a7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.row -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 4c693045bc88..c534860d12c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -40,7 +40,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.ImageMessageConsumer; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.media.MediaFeatureFlag; +import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 8de036542c8f..277ad8e54016 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -1374,13 +1374,8 @@ public class NotificationContentView extends FrameLayout implements Notification if (bubbleButton == null || actionContainer == null) { return; } - boolean isPersonWithShortcut = - mPeopleIdentifier.getPeopleNotificationType(entry) - >= PeopleNotificationIdentifier.TYPE_FULL_PERSON; - boolean showButton = BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser()) - && isPersonWithShortcut - && entry.getBubbleMetadata() != null; - if (showButton) { + + if (shouldShowBubbleButton(entry)) { // explicitly resolve drawable resource using SystemUI's theme Drawable d = mContext.getDrawable(entry.isBubble() ? R.drawable.bubble_ic_stop_bubble @@ -1410,6 +1405,16 @@ public class NotificationContentView extends FrameLayout implements Notification } } + @VisibleForTesting + boolean shouldShowBubbleButton(NotificationEntry entry) { + boolean isPersonWithShortcut = + mPeopleIdentifier.getPeopleNotificationType(entry) + >= PeopleNotificationIdentifier.TYPE_FULL_PERSON; + return BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser()) + && isPersonWithShortcut + && entry.getBubbleMetadata() != null; + } + private void applySnoozeAction(View layout) { if (layout == null || mContainingNotification == null) { return; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt index f9923b2254d7..8a5d29a1ae2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.row -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index 7a654365e0ae..f13e48d55ae4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -35,12 +35,15 @@ import androidx.annotation.Nullable; import com.android.internal.widget.CachingIconView; import com.android.internal.widget.NotificationExpandButton; +import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.statusbar.TransformableView; import com.android.systemui.statusbar.ViewTransformationHelper; import com.android.systemui.statusbar.notification.CustomInterpolatorTransformation; import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.ImageTransformState; +import com.android.systemui.statusbar.notification.Roundable; +import com.android.systemui.statusbar.notification.RoundableState; import com.android.systemui.statusbar.notification.TransformState; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -49,13 +52,12 @@ import java.util.Stack; /** * Wraps a notification view which may or may not include a header. */ -public class NotificationHeaderViewWrapper extends NotificationViewWrapper { +public class NotificationHeaderViewWrapper extends NotificationViewWrapper implements Roundable { + private final RoundableState mRoundableState; private static final Interpolator LOW_PRIORITY_HEADER_CLOSE = new PathInterpolator(0.4f, 0f, 0.7f, 1f); - protected final ViewTransformationHelper mTransformationHelper; - private CachingIconView mIcon; private NotificationExpandButton mExpandButton; private View mAltExpandTarget; @@ -67,12 +69,16 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { private ImageView mWorkProfileImage; private View mAudiblyAlertedIcon; private View mFeedbackIcon; - private boolean mIsLowPriority; private boolean mTransformLowPriorityTitle; protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) { super(ctx, view, row); + mRoundableState = new RoundableState( + mView, + this, + ctx.getResources().getDimension(R.dimen.notification_corner_radius) + ); mTransformationHelper = new ViewTransformationHelper(); // we want to avoid that the header clashes with the other text when transforming @@ -81,7 +87,8 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { new CustomInterpolatorTransformation(TRANSFORMING_VIEW_TITLE) { @Override - public Interpolator getCustomInterpolator(int interpolationType, + public Interpolator getCustomInterpolator( + int interpolationType, boolean isFrom) { boolean isLowPriority = mView instanceof NotificationHeaderView; if (interpolationType == TRANSFORM_Y) { @@ -99,11 +106,17 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { protected boolean hasCustomTransformation() { return mIsLowPriority && mTransformLowPriorityTitle; } - }, TRANSFORMING_VIEW_TITLE); + }, + TRANSFORMING_VIEW_TITLE); resolveHeaderViews(); addFeedbackOnClickListener(row); } + @Override + public RoundableState getRoundableState() { + return mRoundableState; + } + protected void resolveHeaderViews() { mIcon = mView.findViewById(com.android.internal.R.id.icon); mHeaderText = mView.findViewById(com.android.internal.R.id.header_text); @@ -128,7 +141,9 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { } } - /** Shows the given feedback icon, or hides the icon if null. */ + /** + * Shows the given feedback icon, or hides the icon if null. + */ @Override public void setFeedbackIcon(@Nullable FeedbackIcon icon) { if (mFeedbackIcon != null) { @@ -193,7 +208,7 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { // its animation && child.getId() != com.android.internal.R.id.conversation_icon_badge_ring) { ((ImageView) child).setCropToPadding(true); - } else if (child instanceof ViewGroup){ + } else if (child instanceof ViewGroup) { ViewGroup group = (ViewGroup) child; for (int i = 0; i < group.getChildCount(); i++) { stack.push(group.getChildAt(i)); @@ -215,7 +230,9 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { } @Override - public void updateExpandability(boolean expandable, View.OnClickListener onClickListener, + public void updateExpandability( + boolean expandable, + View.OnClickListener onClickListener, boolean requestLayout) { mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE); mExpandButton.setOnClickListener(expandable ? onClickListener : null); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index 2719dd88b7be..b2628e40e77e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -142,6 +142,11 @@ public class AmbientState implements Dumpable { */ private boolean mIsFlingRequiredAfterLockScreenSwipeUp = false; + /** + * Whether the shade is currently closing. + */ + private boolean mIsClosing; + @VisibleForTesting public boolean isFlingRequiredAfterLockScreenSwipeUp() { return mIsFlingRequiredAfterLockScreenSwipeUp; @@ -717,6 +722,20 @@ public class AmbientState implements Dumpable { && mStatusBarKeyguardViewManager.isBouncerInTransit(); } + /** + * @param isClosing Whether the shade is currently closing. + */ + public void setIsClosing(boolean isClosing) { + mIsClosing = isClosing; + } + + /** + * @return Whether the shade is currently closing. + */ + public boolean isClosing() { + return mIsClosing; + } + @Override public void dump(PrintWriter pw, String[] args) { pw.println("mTopPadding=" + mTopPadding); @@ -761,5 +780,6 @@ public class AmbientState implements Dumpable { + mIsFlingRequiredAfterLockScreenSwipeUp); pw.println("mZDistanceBetweenElements=" + mZDistanceBetweenElements); pw.println("mBaseZHeight=" + mBaseZHeight); + pw.println("mIsClosing=" + mIsClosing); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 0dda2632db66..26f0ad9eca87 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -21,6 +21,9 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.Path.Direction; import android.graphics.drawable.ColorDrawable; import android.service.notification.StatusBarNotification; import android.util.AttributeSet; @@ -33,6 +36,7 @@ import android.view.ViewGroup; import android.widget.RemoteViews; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; @@ -43,10 +47,14 @@ import com.android.systemui.statusbar.NotificationGroupingUtil; import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.NotificationUtils; +import com.android.systemui.statusbar.notification.Roundable; +import com.android.systemui.statusbar.notification.RoundableState; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.HybridGroupManager; import com.android.systemui.statusbar.notification.row.HybridNotificationView; +import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import java.util.ArrayList; @@ -56,7 +64,7 @@ import java.util.List; * A container containing child notifications */ public class NotificationChildrenContainer extends ViewGroup - implements NotificationFadeAware { + implements NotificationFadeAware, Roundable { private static final String TAG = "NotificationChildrenContainer"; @@ -100,9 +108,9 @@ public class NotificationChildrenContainer extends ViewGroup private boolean mEnableShadowOnChildNotifications; private NotificationHeaderView mNotificationHeader; - private NotificationViewWrapper mNotificationHeaderWrapper; + private NotificationHeaderViewWrapper mNotificationHeaderWrapper; private NotificationHeaderView mNotificationHeaderLowPriority; - private NotificationViewWrapper mNotificationHeaderWrapperLowPriority; + private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority; private NotificationGroupingUtil mGroupingUtil; private ViewState mHeaderViewState; private int mClipBottomAmount; @@ -110,7 +118,8 @@ public class NotificationChildrenContainer extends ViewGroup private OnClickListener mHeaderClickListener; private ViewGroup mCurrentHeader; private boolean mIsConversation; - + private Path mChildClipPath = null; + private final Path mHeaderPath = new Path(); private boolean mShowGroupCountInExpander; private boolean mShowDividersWhenExpanded; private boolean mHideDividersDuringExpand; @@ -119,6 +128,8 @@ public class NotificationChildrenContainer extends ViewGroup private float mHeaderVisibleAmount = 1.0f; private int mUntruncatedChildCount; private boolean mContainingNotificationIsFaded = false; + private RoundableState mRoundableState; + private boolean mIsNotificationGroupCornerEnabled; public NotificationChildrenContainer(Context context) { this(context, null); @@ -132,10 +143,14 @@ public class NotificationChildrenContainer extends ViewGroup this(context, attrs, defStyleAttr, 0); } - public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr, + public NotificationChildrenContainer( + Context context, + AttributeSet attrs, + int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mHybridGroupManager = new HybridGroupManager(getContext()); + mRoundableState = new RoundableState(this, this, 0f); initDimens(); setClipChildren(false); } @@ -167,6 +182,12 @@ public class NotificationChildrenContainer extends ViewGroup mHybridGroupManager.initDimens(); } + @NonNull + @Override + public RoundableState getRoundableState() { + return mRoundableState; + } + @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = @@ -271,7 +292,7 @@ public class NotificationChildrenContainer extends ViewGroup /** * Add a child notification to this view. * - * @param row the row to add + * @param row the row to add * @param childIndex the index to add it at, if -1 it will be added at the end */ public void addNotification(ExpandableNotificationRow row, int childIndex) { @@ -347,8 +368,11 @@ public class NotificationChildrenContainer extends ViewGroup mNotificationHeader.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); mNotificationHeader.setOnClickListener(mHeaderClickListener); - mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(), - mNotificationHeader, mContainingNotification); + mNotificationHeaderWrapper = + (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( + getContext(), + mNotificationHeader, + mContainingNotification); addView(mNotificationHeader, 0); invalidate(); } else { @@ -381,8 +405,11 @@ public class NotificationChildrenContainer extends ViewGroup mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener); - mNotificationHeaderWrapperLowPriority = NotificationViewWrapper.wrap(getContext(), - mNotificationHeaderLowPriority, mContainingNotification); + mNotificationHeaderWrapperLowPriority = + (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( + getContext(), + mNotificationHeaderLowPriority, + mContainingNotification); addView(mNotificationHeaderLowPriority, 0); invalidate(); } else { @@ -462,20 +489,8 @@ public class NotificationChildrenContainer extends ViewGroup } /** - * Sets the alpha on the content, while leaving the background of the container itself as is. - * - * @param alpha alpha value to apply to the content + * To be called any time the rows have been updated */ - public void setContentAlpha(float alpha) { - for (int i = 0; i < mNotificationHeader.getChildCount(); i++) { - mNotificationHeader.getChildAt(i).setAlpha(alpha); - } - for (ExpandableNotificationRow child : getAttachedChildren()) { - child.setContentAlpha(alpha); - } - } - - /** To be called any time the rows have been updated */ public void updateExpansionStates() { if (mChildrenExpanded || mUserLocked) { // we don't modify it the group is expanded or if we are expanding it @@ -489,7 +504,6 @@ public class NotificationChildrenContainer extends ViewGroup } /** - * * @return the intrinsic size of this children container, i.e the natural fully expanded state */ public int getIntrinsicHeight() { @@ -499,7 +513,7 @@ public class NotificationChildrenContainer extends ViewGroup /** * @return the intrinsic height with a number of children given - * in @param maxAllowedVisibleChildren + * in @param maxAllowedVisibleChildren */ private int getIntrinsicHeight(float maxAllowedVisibleChildren) { if (showingAsLowPriority()) { @@ -553,7 +567,8 @@ public class NotificationChildrenContainer extends ViewGroup /** * Update the state of all its children based on a linear layout algorithm. - * @param parentState the state of the parent + * + * @param parentState the state of the parent * @param ambientState the ambient state containing ambient information */ public void updateState(ExpandableViewState parentState, AmbientState ambientState) { @@ -669,14 +684,17 @@ public class NotificationChildrenContainer extends ViewGroup * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its * height, children in the group after this are gone. * - * @param child the child who's height to adjust. + * @param child the child who's height to adjust. * @param parentHeight the height of the parent. - * @param childState the state to update. - * @param yPosition the yPosition of the view. + * @param childState the state to update. + * @param yPosition the yPosition of the view. * @return true if children after this one should be hidden. */ - private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child, - int parentHeight, ExpandableViewState childState, int yPosition) { + private boolean updateChildStateForExpandedGroup( + ExpandableNotificationRow child, + int parentHeight, + ExpandableViewState childState, + int yPosition) { final int top = yPosition + child.getClipTopAmount(); final int intrinsicHeight = child.getIntrinsicHeight(); final int bottom = top + intrinsicHeight; @@ -704,13 +722,15 @@ public class NotificationChildrenContainer extends ViewGroup if (mIsLowPriority || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded()) || (mContainingNotification.isHeadsUpState() - && mContainingNotification.canShowHeadsUp())) { + && mContainingNotification.canShowHeadsUp())) { return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED; } return NUMBER_OF_CHILDREN_WHEN_COLLAPSED; } - /** Applies state to children. */ + /** + * Applies state to children. + */ public void applyState() { int childCount = mAttachedChildren.size(); ViewState tmpState = new ViewState(); @@ -782,17 +802,73 @@ public class NotificationChildrenContainer extends ViewGroup } } + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + boolean isCanvasChanged = false; + + Path clipPath = mChildClipPath; + if (clipPath != null) { + final float translation; + if (child instanceof ExpandableNotificationRow) { + ExpandableNotificationRow notificationRow = (ExpandableNotificationRow) child; + translation = notificationRow.getTranslation(); + } else { + translation = child.getTranslationX(); + } + + isCanvasChanged = true; + canvas.save(); + if (mIsNotificationGroupCornerEnabled && translation != 0f) { + clipPath.offset(translation, 0f); + canvas.clipPath(clipPath); + clipPath.offset(-translation, 0f); + } else { + canvas.clipPath(clipPath); + } + } + + if (child instanceof NotificationHeaderView + && mNotificationHeaderWrapper.hasRoundedCorner()) { + float[] radii = mNotificationHeaderWrapper.getUpdatedRadii(); + mHeaderPath.reset(); + mHeaderPath.addRoundRect( + child.getLeft(), + child.getTop(), + child.getRight(), + child.getBottom(), + radii, + Direction.CW + ); + if (!isCanvasChanged) { + isCanvasChanged = true; + canvas.save(); + } + canvas.clipPath(mHeaderPath); + } + + if (isCanvasChanged) { + boolean result = super.drawChild(canvas, child, drawingTime); + canvas.restore(); + return result; + } else { + // If there have been no changes to the canvas we can proceed as usual + return super.drawChild(canvas, child, drawingTime); + } + } + + /** * This is called when the children expansion has changed and positions the children properly * for an appear animation. - * */ public void prepareExpansionChanged() { // TODO: do something that makes sense, like placing the invisible views correctly return; } - /** Animate to a given state. */ + /** + * Animate to a given state. + */ public void startAnimationToState(AnimationProperties properties) { int childCount = mAttachedChildren.size(); ViewState tmpState = new ViewState(); @@ -1116,7 +1192,8 @@ public class NotificationChildrenContainer extends ViewGroup * Get the minimum Height for this group. * * @param maxAllowedVisibleChildren the number of children that should be visible - * @param likeHighPriority if the height should be calculated as if it were not low priority + * @param likeHighPriority if the height should be calculated as if it were not low + * priority */ private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) { return getMinHeight(maxAllowedVisibleChildren, likeHighPriority, mCurrentHeaderTranslation); @@ -1126,10 +1203,13 @@ public class NotificationChildrenContainer extends ViewGroup * Get the minimum Height for this group. * * @param maxAllowedVisibleChildren the number of children that should be visible - * @param likeHighPriority if the height should be calculated as if it were not low priority - * @param headerTranslation the translation amount of the header + * @param likeHighPriority if the height should be calculated as if it were not low + * priority + * @param headerTranslation the translation amount of the header */ - private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority, + private int getMinHeight( + int maxAllowedVisibleChildren, + boolean likeHighPriority, int headerTranslation) { if (!likeHighPriority && showingAsLowPriority()) { if (mNotificationHeaderLowPriority == null) { @@ -1288,16 +1368,19 @@ public class NotificationChildrenContainer extends ViewGroup return mUserLocked; } - public void setCurrentBottomRoundness(float currentBottomRoundness) { + @Override + public void applyRoundness() { + Roundable.super.applyRoundness(); boolean last = true; for (int i = mAttachedChildren.size() - 1; i >= 0; i--) { ExpandableNotificationRow child = mAttachedChildren.get(i); if (child.getVisibility() == View.GONE) { continue; } - float bottomRoundness = last ? currentBottomRoundness : 0.0f; - child.setBottomRoundness(bottomRoundness, isShown() /* animate */); - child.setTopRoundness(0.0f, false /* animate */); + child.requestBottomRoundness( + last ? getBottomRoundness() : 0f, + /* animate = */ isShown(), + SourceType.DefaultValue); last = false; } } @@ -1307,7 +1390,9 @@ public class NotificationChildrenContainer extends ViewGroup mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader); } - /** Shows the given feedback icon, or hides the icon if null. */ + /** + * Shows the given feedback icon, or hides the icon if null. + */ public void setFeedbackIcon(@Nullable FeedbackIcon icon) { if (mNotificationHeaderWrapper != null) { mNotificationHeaderWrapper.setFeedbackIcon(icon); @@ -1339,4 +1424,26 @@ public class NotificationChildrenContainer extends ViewGroup child.setNotificationFaded(faded); } } + + /** + * Allow to define a path the clip the children in #drawChild() + * + * @param childClipPath path used to clip the children + */ + public void setChildClipPath(@Nullable Path childClipPath) { + mChildClipPath = childClipPath; + invalidate(); + } + + public NotificationHeaderViewWrapper getNotificationHeaderWrapper() { + return mNotificationHeaderWrapper; + } + + /** + * Enable the support for rounded corner in notification group + * @param enabled true if is supported + */ + public void enableNotificationGroupCorner(boolean enabled) { + mIsNotificationGroupCornerEnabled = enabled; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java index 2015c87aac2b..6810055ad3bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java @@ -26,6 +26,8 @@ import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; +import com.android.systemui.statusbar.notification.Roundable; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.logging.NotificationRoundnessLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -59,8 +61,8 @@ public class NotificationRoundnessManager implements Dumpable { private boolean mIsClearAllInProgress; private ExpandableView mSwipedView = null; - private ExpandableView mViewBeforeSwipedView = null; - private ExpandableView mViewAfterSwipedView = null; + private Roundable mViewBeforeSwipedView = null; + private Roundable mViewAfterSwipedView = null; @Inject NotificationRoundnessManager( @@ -101,11 +103,12 @@ public class NotificationRoundnessManager implements Dumpable { public boolean isViewAffectedBySwipe(ExpandableView expandableView) { return expandableView != null && (expandableView == mSwipedView - || expandableView == mViewBeforeSwipedView - || expandableView == mViewAfterSwipedView); + || expandableView == mViewBeforeSwipedView + || expandableView == mViewAfterSwipedView); } - boolean updateViewWithoutCallback(ExpandableView view, + boolean updateViewWithoutCallback( + ExpandableView view, boolean animate) { if (view == null || view == mViewBeforeSwipedView @@ -113,11 +116,15 @@ public class NotificationRoundnessManager implements Dumpable { return false; } - final float topRoundness = getRoundnessFraction(view, true /* top */); - final float bottomRoundness = getRoundnessFraction(view, false /* top */); + final boolean isTopChanged = view.requestTopRoundness( + getRoundnessDefaultValue(view, true /* top */), + animate, + SourceType.DefaultValue); - final boolean topChanged = view.setTopRoundness(topRoundness, animate); - final boolean bottomChanged = view.setBottomRoundness(bottomRoundness, animate); + final boolean isBottomChanged = view.requestBottomRoundness( + getRoundnessDefaultValue(view, /* top = */ false), + animate, + SourceType.DefaultValue); final boolean isFirstInSection = isFirstInSection(view); final boolean isLastInSection = isLastInSection(view); @@ -126,9 +133,9 @@ public class NotificationRoundnessManager implements Dumpable { view.setLastInSection(isLastInSection); mNotifLogger.onCornersUpdated(view, isFirstInSection, - isLastInSection, topChanged, bottomChanged); + isLastInSection, isTopChanged, isBottomChanged); - return (isFirstInSection || isLastInSection) && (topChanged || bottomChanged); + return (isFirstInSection || isLastInSection) && (isTopChanged || isBottomChanged); } private boolean isFirstInSection(ExpandableView view) { @@ -150,42 +157,46 @@ public class NotificationRoundnessManager implements Dumpable { } void setViewsAffectedBySwipe( - ExpandableView viewBefore, + Roundable viewBefore, ExpandableView viewSwiped, - ExpandableView viewAfter) { + Roundable viewAfter) { final boolean animate = true; + final SourceType source = SourceType.OnDismissAnimation; + + // This method requires you to change the roundness of the current View targets and reset + // the roundness of the old View targets (if any) to 0f. + // To avoid conflicts, it generates a set of old Views and removes the current Views + // from this set. + HashSet<Roundable> oldViews = new HashSet<>(); + if (mViewBeforeSwipedView != null) oldViews.add(mViewBeforeSwipedView); + if (mSwipedView != null) oldViews.add(mSwipedView); + if (mViewAfterSwipedView != null) oldViews.add(mViewAfterSwipedView); - ExpandableView oldViewBefore = mViewBeforeSwipedView; mViewBeforeSwipedView = viewBefore; - if (oldViewBefore != null) { - final float bottomRoundness = getRoundnessFraction(oldViewBefore, false /* top */); - oldViewBefore.setBottomRoundness(bottomRoundness, animate); - } if (viewBefore != null) { - viewBefore.setBottomRoundness(1f, animate); + oldViews.remove(viewBefore); + viewBefore.requestTopRoundness(0f, animate, source); + viewBefore.requestBottomRoundness(1f, animate, source); } - ExpandableView oldSwipedview = mSwipedView; mSwipedView = viewSwiped; - if (oldSwipedview != null) { - final float bottomRoundness = getRoundnessFraction(oldSwipedview, false /* top */); - final float topRoundness = getRoundnessFraction(oldSwipedview, true /* top */); - oldSwipedview.setTopRoundness(topRoundness, animate); - oldSwipedview.setBottomRoundness(bottomRoundness, animate); - } if (viewSwiped != null) { - viewSwiped.setTopRoundness(1f, animate); - viewSwiped.setBottomRoundness(1f, animate); + oldViews.remove(viewSwiped); + viewSwiped.requestTopRoundness(1f, animate, source); + viewSwiped.requestBottomRoundness(1f, animate, source); } - ExpandableView oldViewAfter = mViewAfterSwipedView; mViewAfterSwipedView = viewAfter; - if (oldViewAfter != null) { - final float topRoundness = getRoundnessFraction(oldViewAfter, true /* top */); - oldViewAfter.setTopRoundness(topRoundness, animate); - } if (viewAfter != null) { - viewAfter.setTopRoundness(1f, animate); + oldViews.remove(viewAfter); + viewAfter.requestTopRoundness(1f, animate, source); + viewAfter.requestBottomRoundness(0f, animate, source); + } + + // After setting the current Views, reset the views that are still present in the set. + for (Roundable oldView : oldViews) { + oldView.requestTopRoundness(0f, animate, source); + oldView.requestBottomRoundness(0f, animate, source); } } @@ -193,7 +204,7 @@ public class NotificationRoundnessManager implements Dumpable { mIsClearAllInProgress = isClearingAll; } - private float getRoundnessFraction(ExpandableView view, boolean top) { + private float getRoundnessDefaultValue(Roundable view, boolean top) { if (view == null) { return 0f; } @@ -207,28 +218,35 @@ public class NotificationRoundnessManager implements Dumpable { && mIsClearAllInProgress) { return 1.0f; } - if ((view.isPinned() - || (view.isHeadsUpAnimatingAway()) && !mExpanded)) { - return 1.0f; - } - if (isFirstInSection(view) && top) { - return 1.0f; - } - if (isLastInSection(view) && !top) { - return 1.0f; - } + if (view instanceof ExpandableView) { + ExpandableView expandableView = (ExpandableView) view; + if ((expandableView.isPinned() + || (expandableView.isHeadsUpAnimatingAway()) && !mExpanded)) { + return 1.0f; + } + if (isFirstInSection(expandableView) && top) { + return 1.0f; + } + if (isLastInSection(expandableView) && !top) { + return 1.0f; + } - if (view == mTrackedHeadsUp) { - // If we're pushing up on a headsup the appear fraction is < 0 and it needs to still be - // rounded. - return MathUtils.saturate(1.0f - mAppearFraction); - } - if (view.showingPulsing() && mRoundForPulsingViews) { - return 1.0f; + if (view == mTrackedHeadsUp) { + // If we're pushing up on a headsup the appear fraction is < 0 and it needs to + // still be rounded. + return MathUtils.saturate(1.0f - mAppearFraction); + } + if (expandableView.showingPulsing() && mRoundForPulsingViews) { + return 1.0f; + } + if (expandableView.isChildInGroup()) { + return 0f; + } + final Resources resources = expandableView.getResources(); + return resources.getDimension(R.dimen.notification_corner_radius_small) + / resources.getDimension(R.dimen.notification_corner_radius); } - final Resources resources = view.getResources(); - return resources.getDimension(R.dimen.notification_corner_radius_small) - / resources.getDimension(R.dimen.notification_corner_radius); + return 0f; } public void setExpanded(float expandedHeight, float appearFraction) { @@ -258,8 +276,10 @@ public class NotificationRoundnessManager implements Dumpable { mNotifLogger.onSectionCornersUpdated(sections, anyChanged); } - private boolean handleRemovedOldViews(NotificationSection[] sections, - ExpandableView[] oldViews, boolean first) { + private boolean handleRemovedOldViews( + NotificationSection[] sections, + ExpandableView[] oldViews, + boolean first) { boolean anyChanged = false; for (ExpandableView oldView : oldViews) { if (oldView != null) { @@ -289,8 +309,10 @@ public class NotificationRoundnessManager implements Dumpable { return anyChanged; } - private boolean handleAddedNewViews(NotificationSection[] sections, - ExpandableView[] oldViews, boolean first) { + private boolean handleAddedNewViews( + NotificationSection[] sections, + ExpandableView[] oldViews, + boolean first) { boolean anyChanged = false; for (NotificationSection section : sections) { ExpandableView newView = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt index cb7dfe87f7fb..b61c55edadcd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt @@ -17,9 +17,9 @@ package com.android.systemui.statusbar.notification.stack import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationSectionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject private const val TAG = "NotifSections" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt index 91a28139c775..a1b77acb9a5e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt @@ -19,11 +19,10 @@ import android.annotation.ColorInt import android.util.Log import android.view.View import com.android.internal.annotations.VisibleForTesting -import com.android.systemui.media.KeyguardMediaController +import com.android.systemui.media.controls.ui.KeyguardMediaController import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager import com.android.systemui.statusbar.notification.collection.render.MediaContainerController import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController -import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager import com.android.systemui.statusbar.notification.dagger.AlertingHeader import com.android.systemui.statusbar.notification.dagger.IncomingHeader import com.android.systemui.statusbar.notification.dagger.PeopleHeader diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 55c577f1ea39..df705c5afeef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -255,7 +255,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private boolean mClearAllInProgress; private FooterClearAllListener mFooterClearAllListener; private boolean mFlingAfterUpEvent; - /** * Was the scroller scrolled to the top when the down motion was observed? */ @@ -1189,7 +1188,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return; } for (int i = 0; i < getChildCount(); i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (mChildrenToAddAnimated.contains(child)) { final int startingPosition = getPositionInLinearLayout(child); final int childHeight = getIntrinsicHeight(child) + mPaddingBetweenElements; @@ -1659,7 +1658,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable // find the view under the pointer, accounting for GONE views final int count = getChildCount(); for (int childIdx = 0; childIdx < count; childIdx++) { - ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx); + ExpandableView slidingChild = getChildAtIndex(childIdx); if (slidingChild.getVisibility() != VISIBLE || (ignoreDecors && slidingChild instanceof StackScrollerDecorView)) { continue; @@ -1692,6 +1691,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return null; } + private ExpandableView getChildAtIndex(int index) { + return (ExpandableView) getChildAt(index); + } + public ExpandableView getChildAtRawPosition(float touchX, float touchY) { getLocationOnScreen(mTempInt2); return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]); @@ -2277,7 +2280,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable int childCount = getChildCount(); int count = 0; for (int i = 0; i < childCount; i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child.getVisibility() != View.GONE && !child.willBeGone() && child != mShelf) { count++; } @@ -2497,7 +2500,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private ExpandableView getLastChildWithBackground() { int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView) && child != mShelf) { return child; @@ -2510,7 +2513,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private ExpandableView getFirstChildWithBackground() { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView) && child != mShelf) { return child; @@ -2524,7 +2527,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable ArrayList<ExpandableView> children = new ArrayList<>(); int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView) && child != mShelf) { @@ -2883,7 +2886,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } int position = 0; for (int i = 0; i < getChildCount(); i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); boolean notGone = child.getVisibility() != View.GONE; if (notGone && !child.hasNoContentHeight()) { if (position != 0) { @@ -2937,7 +2940,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } mAmbientState.setLastVisibleBackgroundChild(lastChild); // TODO: Refactor SectionManager and put the RoundnessManager there. - mController.getNoticationRoundessManager().updateRoundedChildren(mSections); + mController.getNotificationRoundnessManager().updateRoundedChildren(mSections); mAnimateBottomOnLayout = false; invalidate(); } @@ -3969,7 +3972,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) private void clearUserLockedViews() { for (int i = 0; i < getChildCount(); i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) child; row.setUserLocked(false); @@ -3982,7 +3985,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable // lets make sure nothing is transient anymore clearTemporaryViewsInGroup(this); for (int i = 0; i < getChildCount(); i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) child; clearTemporaryViewsInGroup(row.getChildrenContainer()); @@ -4020,8 +4023,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable setOwnScrollY(0); } + @VisibleForTesting @ShadeViewRefactor(RefactorComponent.COORDINATOR) - private void setIsExpanded(boolean isExpanded) { + void setIsExpanded(boolean isExpanded) { boolean changed = isExpanded != mIsExpanded; mIsExpanded = isExpanded; mStackScrollAlgorithm.setIsExpanded(isExpanded); @@ -4230,7 +4234,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (hideSensitive != mAmbientState.isHideSensitive()) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { - ExpandableView v = (ExpandableView) getChildAt(i); + ExpandableView v = getChildAtIndex(i); v.setHideSensitiveForIntrinsicHeight(hideSensitive); } mAmbientState.setHideSensitive(hideSensitive); @@ -4265,7 +4269,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private void applyCurrentState() { int numChildren = getChildCount(); for (int i = 0; i < numChildren; i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); child.applyViewState(); } @@ -4285,7 +4289,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable // Lefts first sort by Z difference for (int i = 0; i < getChildCount(); i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child.getVisibility() != GONE) { mTmpSortedChildren.add(child); } @@ -4512,7 +4516,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable public void setClearAllInProgress(boolean clearAllInProgress) { mClearAllInProgress = clearAllInProgress; mAmbientState.setClearAllInProgress(clearAllInProgress); - mController.getNoticationRoundessManager().setClearAllInProgress(clearAllInProgress); + mController.getNotificationRoundnessManager().setClearAllInProgress(clearAllInProgress); } boolean getClearAllInProgress() { @@ -4555,7 +4559,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable final int count = getChildCount(); float max = 0; for (int childIdx = 0; childIdx < count; childIdx++) { - ExpandableView child = (ExpandableView) getChildAt(childIdx); + ExpandableView child = getChildAtIndex(childIdx); if (child.getVisibility() == GONE) { continue; } @@ -4586,7 +4590,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable public boolean isBelowLastNotification(float touchX, float touchY) { int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child.getVisibility() != View.GONE) { float childTop = child.getY(); if (childTop > touchY) { @@ -4842,13 +4846,21 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } } + @VisibleForTesting @ShadeViewRefactor(RefactorComponent.COORDINATOR) - private void setOwnScrollY(int ownScrollY) { + void setOwnScrollY(int ownScrollY) { setOwnScrollY(ownScrollY, false /* animateScrollChangeListener */); } @ShadeViewRefactor(RefactorComponent.COORDINATOR) private void setOwnScrollY(int ownScrollY, boolean animateStackYChangeListener) { + // Avoid Flicking during clear all + // when the shade finishes closing, onExpansionStopped will call + // resetScrollPosition to setOwnScrollY to 0 + if (mAmbientState.isClosing()) { + return; + } + if (ownScrollY != mOwnScrollY) { // We still want to call the normal scrolled changed for accessibility reasons onScrollChanged(mScrollX, ownScrollY, mScrollX, mOwnScrollY); @@ -5044,7 +5056,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable pw.println(); for (int i = 0; i < childCount; i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); child.dump(pw, args); pw.println(); } @@ -5333,7 +5345,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable float wakeUplocation = -1f; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { - ExpandableView view = (ExpandableView) getChildAt(i); + ExpandableView view = getChildAtIndex(i); if (view.getVisibility() == View.GONE) { continue; } @@ -5372,7 +5384,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable public void setController( NotificationStackScrollLayoutController notificationStackScrollLayoutController) { mController = notificationStackScrollLayoutController; - mController.getNoticationRoundessManager().setAnimatedChildren(mChildrenToAddAnimated); + mController.getNotificationRoundnessManager().setAnimatedChildren(mChildrenToAddAnimated); } void addSwipedOutView(View v) { @@ -5383,31 +5395,22 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (!(viewSwiped instanceof ExpandableNotificationRow)) { return; } - final int indexOfSwipedView = indexOfChild(viewSwiped); - if (indexOfSwipedView < 0) { - return; - } mSectionsManager.updateFirstAndLastViewsForAllSections( - mSections, getChildrenWithBackground()); - View viewBefore = null; - if (indexOfSwipedView > 0) { - viewBefore = getChildAt(indexOfSwipedView - 1); - if (mSectionsManager.beginsSection(viewSwiped, viewBefore)) { - viewBefore = null; - } - } - View viewAfter = null; - if (indexOfSwipedView < getChildCount()) { - viewAfter = getChildAt(indexOfSwipedView + 1); - if (mSectionsManager.beginsSection(viewAfter, viewSwiped)) { - viewAfter = null; - } - } - mController.getNoticationRoundessManager() + mSections, + getChildrenWithBackground() + ); + + RoundableTargets targets = mController.getNotificationTargetsHelper().findRoundableTargets( + (ExpandableNotificationRow) viewSwiped, + this, + mSectionsManager + ); + + mController.getNotificationRoundnessManager() .setViewsAffectedBySwipe( - (ExpandableView) viewBefore, - (ExpandableView) viewSwiped, - (ExpandableView) viewAfter); + targets.getBefore(), + targets.getSwiped(), + targets.getAfter()); updateFirstAndLastBackgroundViews(); requestDisallowInterceptTouchEvent(true); @@ -5418,7 +5421,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable void onSwipeEnd() { updateFirstAndLastBackgroundViews(); - mController.getNoticationRoundessManager() + mController.getNotificationRoundnessManager() .setViewsAffectedBySwipe(null, null, null); // Round bottom corners for notification right before shelf. mShelf.updateAppearance(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 5c09d618403d..e13378269ba6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -63,7 +63,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; -import com.android.systemui.media.KeyguardMediaController; +import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener; @@ -180,6 +180,7 @@ public class NotificationStackScrollLayoutController { private int mBarState; private HeadsUpAppearanceController mHeadsUpAppearanceController; private final FeatureFlags mFeatureFlags; + private final NotificationTargetsHelper mNotificationTargetsHelper; private View mLongPressedView; @@ -642,7 +643,8 @@ public class NotificationStackScrollLayoutController { StackStateLogger stackLogger, NotificationStackScrollLogger logger, NotificationStackSizeCalculator notificationStackSizeCalculator, - FeatureFlags featureFlags) { + FeatureFlags featureFlags, + NotificationTargetsHelper notificationTargetsHelper) { mStackStateLogger = stackLogger; mLogger = logger; mAllowLongPress = allowLongPress; @@ -679,6 +681,7 @@ public class NotificationStackScrollLayoutController { mRemoteInputManager = remoteInputManager; mShadeController = shadeController; mFeatureFlags = featureFlags; + mNotificationTargetsHelper = notificationTargetsHelper; updateResources(); } @@ -1380,7 +1383,7 @@ public class NotificationStackScrollLayoutController { return mView.calculateGapHeight(previousView, child, count); } - NotificationRoundnessManager getNoticationRoundessManager() { + NotificationRoundnessManager getNotificationRoundnessManager() { return mNotificationRoundnessManager; } @@ -1537,6 +1540,10 @@ public class NotificationStackScrollLayoutController { mNotificationActivityStarter = activityStarter; } + public NotificationTargetsHelper getNotificationTargetsHelper() { + return mNotificationTargetsHelper; + } + /** * Enum for UiEvent logged from this class */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt index 5f79c0e3913a..4c52db7f8732 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.statusbar.notification.stack -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt new file mode 100644 index 000000000000..991a14bb9c2a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt @@ -0,0 +1,100 @@ +package com.android.systemui.statusbar.notification.stack + +import androidx.core.view.children +import androidx.core.view.isVisible +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.statusbar.notification.Roundable +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.row.ExpandableView +import javax.inject.Inject + +/** + * Utility class that helps us find the targets of an animation, often used to find the notification + * ([Roundable]) above and below the current one (see [findRoundableTargets]). + */ +@SysUISingleton +class NotificationTargetsHelper +@Inject +constructor( + featureFlags: FeatureFlags, +) { + private val isNotificationGroupCornerEnabled = + featureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER) + + /** + * This method looks for views that can be rounded (and implement [Roundable]) during a + * notification swipe. + * @return The [Roundable] targets above/below the [viewSwiped] (if available). The + * [RoundableTargets.before] and [RoundableTargets.after] parameters can be `null` if there is + * no above/below notification or the notification is not part of the same section. + */ + fun findRoundableTargets( + viewSwiped: ExpandableNotificationRow, + stackScrollLayout: NotificationStackScrollLayout, + sectionsManager: NotificationSectionsManager, + ): RoundableTargets { + val viewBefore: Roundable? + val viewAfter: Roundable? + + val notificationParent = viewSwiped.notificationParent + val childrenContainer = notificationParent?.childrenContainer + val visibleStackChildren = + stackScrollLayout.children + .filterIsInstance<ExpandableView>() + .filter { it.isVisible } + .toList() + if (notificationParent != null && childrenContainer != null) { + // We are inside a notification group + + if (!isNotificationGroupCornerEnabled) { + return RoundableTargets(null, null, null) + } + + val visibleGroupChildren = childrenContainer.attachedChildren.filter { it.isVisible } + val indexOfParentSwipedView = visibleGroupChildren.indexOf(viewSwiped) + + viewBefore = + visibleGroupChildren.getOrNull(indexOfParentSwipedView - 1) + ?: childrenContainer.notificationHeaderWrapper + + viewAfter = + visibleGroupChildren.getOrNull(indexOfParentSwipedView + 1) + ?: visibleStackChildren.indexOf(notificationParent).let { + visibleStackChildren.getOrNull(it + 1) + } + } else { + // Assumption: we are inside the NotificationStackScrollLayout + + val indexOfSwipedView = visibleStackChildren.indexOf(viewSwiped) + + viewBefore = + visibleStackChildren.getOrNull(indexOfSwipedView - 1)?.takeIf { + !sectionsManager.beginsSection(viewSwiped, it) + } + + viewAfter = + visibleStackChildren.getOrNull(indexOfSwipedView + 1)?.takeIf { + !sectionsManager.beginsSection(it, viewSwiped) + } + } + + return RoundableTargets( + before = viewBefore, + swiped = viewSwiped, + after = viewAfter, + ) + } +} + +/** + * This object contains targets above/below the [swiped] (if available). The [before] and [after] + * parameters can be `null` if there is no above/below notification or the notification is not part + * of the same section. + */ +data class RoundableTargets( + val before: Roundable?, + val swiped: ExpandableNotificationRow?, + val after: Roundable?, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 0502159f46cd..eea1d9118fb7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -31,6 +31,7 @@ import com.android.systemui.R; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; @@ -804,7 +805,7 @@ public class StackScrollAlgorithm { row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius); final float roundness = computeCornerRoundnessForPinnedHun(mHostView.getHeight(), ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius); - row.setBottomRoundness(roundness, /* animate= */ false); + row.requestBottomRoundness(roundness, /* animate = */ false, SourceType.OnScroll); } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt index cb4a0884fea4..f5de678a8536 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.statusbar.notification.stack -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index fa7bfaeb6c4d..169c90780926 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -262,8 +262,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn @Override void startActivity(Intent intent, boolean dismissShade, Callback callback); - void setQsExpanded(boolean expanded); - boolean isWakeUpComingFromTouch(); boolean isFalsingThresholdNeeded(); @@ -455,6 +453,9 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn void collapseShade(); + /** Collapse the shade, but conditional on a flag specific to the trigger of a bugreport. */ + void collapseShadeForBugreport(); + int getWakefulnessState(); boolean isScreenFullyOff(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 34935db72467..9da502767a45 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -885,6 +885,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mBubblesOptional.get().setExpandListener(mBubbleExpandListener); } + // Do not restart System UI when the bugreport flag changes. + mFeatureFlags.addListener(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, event -> { + event.requestNoRestart(); + }); + mStatusBarSignalPolicy.init(); mKeyguardIndicationController.init(); @@ -1136,7 +1141,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // TODO: Deal with the ugliness that comes from having some of the status bar broken out // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot. - mNotificationLogger.setUpWithContainer(mNotifListContainer); mNotificationIconAreaController.setupShelf(mNotificationShelfController); mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator); mUserSwitcherController.init(mNotificationShadeWindowView); @@ -1421,6 +1425,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mStackScrollerController.setNotificationActivityStarter(mNotificationActivityStarter); mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter); mNotificationsController.initialize( + this, mPresenter, mNotifListContainer, mStackScrollerController.getNotifStackController(), @@ -1790,18 +1795,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } @Override - public void setQsExpanded(boolean expanded) { - mNotificationShadeWindowController.setQsExpanded(expanded); - mNotificationPanelViewController.setStatusAccessibilityImportance(expanded - ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); - mNotificationPanelViewController.updateSystemUiStateFlags(); - if (getNavigationBarView() != null) { - getNavigationBarView().onStatusBarPanelStateChanged(); - } - } - - @Override public boolean isWakeUpComingFromTouch() { return mWakeUpComingFromTouch; } @@ -2987,7 +2980,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // * When phone is unlocked: we still don't want to execute hiding of the keyguard // as the animation could prepare 'fake AOD' interface (without actually // transitioning to keyguard state) and this might reset the view states - if (!mScreenOffAnimationController.isKeyguardHideDelayed()) { + if (!mScreenOffAnimationController.isKeyguardHideDelayed() + // If we're animating occluded, there's an activity launching over the keyguard + // UI. Wait to hide it until after the animation concludes. + && !mKeyguardViewMediator.isOccludeAnimationPlaying()) { return hideKeyguardImpl(forceStateChange); } } @@ -3323,7 +3319,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public boolean onBackPressed() { if (mStatusBarKeyguardViewManager.canHandleBackPressed()) { - mStatusBarKeyguardViewManager.onBackPressed(false /* unused */); + mStatusBarKeyguardViewManager.onBackPressed(); return true; } if (mNotificationPanelViewController.isQsCustomizing()) { @@ -3580,6 +3576,13 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } } + @Override + public void collapseShadeForBugreport() { + if (!mFeatureFlags.isEnabled(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT)) { + collapseShade(); + } + } + @VisibleForTesting final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt index b987f6815000..b965ac97cc1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt @@ -26,6 +26,7 @@ import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm @@ -95,14 +96,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr var bouncerShowing: Boolean = false var altBouncerShowing: Boolean = false var launchingAffordance: Boolean = false - var qSExpanded = false - set(value) { - val changed = field != value - field = value - if (changed && !value) { - maybePerformPendingUnlock() - } - } + var qsExpanded = false @Inject constructor( @@ -111,6 +105,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr statusBarStateController: StatusBarStateController, lockscreenUserManager: NotificationLockscreenUserManager, keyguardStateController: KeyguardStateController, + shadeExpansionStateManager: ShadeExpansionStateManager, dumpManager: DumpManager ) { this.mKeyguardStateController = keyguardStateController @@ -132,6 +127,14 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr } }) + shadeExpansionStateManager.addQsExpansionListener { isQsExpanded -> + val changed = qsExpanded != isQsExpanded + qsExpanded = isQsExpanded + if (changed && !isQsExpanded) { + maybePerformPendingUnlock() + } + } + val dismissByDefault = if (context.resources.getBoolean( com.android.internal.R.bool.config_faceAuthDismissesKeyguard)) 1 else 0 tunerService.addTunable(object : TunerService.Tunable { @@ -160,7 +163,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr ): Boolean { if (biometricSourceType == BiometricSourceType.FACE && bypassEnabled) { val can = canBypass() - if (!can && (isPulseExpanding || qSExpanded)) { + if (!can && (isPulseExpanding || qsExpanded)) { pendingUnlock = PendingUnlock(biometricSourceType, isStrongBiometric) } return can @@ -189,7 +192,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr altBouncerShowing -> true statusBarStateController.state != StatusBarState.KEYGUARD -> false launchingAffordance -> false - isPulseExpanding || qSExpanded -> false + isPulseExpanding || qsExpanded -> false else -> true } } @@ -214,7 +217,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr pw.println(" altBouncerShowing: $altBouncerShowing") pw.println(" isPulseExpanding: $isPulseExpanding") pw.println(" launchingAffordance: $launchingAffordance") - pw.println(" qSExpanded: $qSExpanded") + pw.println(" qSExpanded: $qsExpanded") pw.println(" hasFaceFeature: $hasFaceFeature") } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt index 02b235493715..4839fe6a7bef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt @@ -19,9 +19,9 @@ package com.android.systemui.statusbar.phone import android.util.DisplayMetrics import android.view.View import com.android.internal.logging.nano.MetricsProto.MetricsEvent -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.LSShadeTransitionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import javax.inject.Inject 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 00c3e8fac0b4..5e2a7c8ca540 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java @@ -26,6 +26,7 @@ import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.animation.Expandable; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ActivityStarter; @@ -67,7 +68,7 @@ public class MultiUserSwitchController extends ViewController<MultiUserSwitch> { ActivityLaunchAnimator.Controller.fromView(v, null), true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM); } else { - mUserSwitchDialogController.showDialog(v); + mUserSwitchDialogController.showDialog(v.getContext(), Expandable.fromView(v)); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 9f932238007a..cf3a48cf5000 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -53,6 +53,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.scrim.ScrimView; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.statusbar.notification.stack.ViewState; @@ -204,6 +205,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private final ScreenOffAnimationController mScreenOffAnimationController; private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + private KeyguardViewMediator mKeyguardViewMediator; private GradientColors mColors; private boolean mNeedsDrawableColorUpdate; @@ -249,6 +251,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private Callback mCallback; private boolean mWallpaperSupportsAmbientMode; private boolean mScreenOn; + private boolean mTransparentScrimBackground; // Scrim blanking callbacks private Runnable mPendingFrameCallback; @@ -272,7 +275,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump @Main Executor mainExecutor, ScreenOffAnimationController screenOffAnimationController, KeyguardUnlockAnimationController keyguardUnlockAnimationController, - StatusBarKeyguardViewManager statusBarKeyguardViewManager) { + StatusBarKeyguardViewManager statusBarKeyguardViewManager, + KeyguardViewMediator keyguardViewMediator) { mScrimStateListener = lightBarController::setScrimState; mDefaultScrimAlpha = BUSY_SCRIM_ALPHA; @@ -311,6 +315,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } }); mColors = new GradientColors(); + + mKeyguardViewMediator = keyguardViewMediator; } /** @@ -341,6 +347,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mScrimBehind.setDefaultFocusHighlightEnabled(false); mNotificationsScrim.setDefaultFocusHighlightEnabled(false); mScrimInFront.setDefaultFocusHighlightEnabled(false); + mTransparentScrimBackground = notificationsScrim.getResources() + .getBoolean(R.bool.notification_scrim_transparent); updateScrims(); mKeyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback); } @@ -777,13 +785,16 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump float behindFraction = getInterpolatedFraction(); behindFraction = (float) Math.pow(behindFraction, 0.8f); if (mClipsQsScrim) { - mBehindAlpha = 1; - mNotificationsAlpha = behindFraction * mDefaultScrimAlpha; + mBehindAlpha = mTransparentScrimBackground ? 0 : 1; + mNotificationsAlpha = + mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha; } else { - mBehindAlpha = behindFraction * mDefaultScrimAlpha; + mBehindAlpha = + mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha; // Delay fade-in of notification scrim a bit further, to coincide with the // view fade in. Otherwise the empty panel can be quite jarring. - mNotificationsAlpha = MathUtils.constrainedMap(0f, 1f, 0.3f, 0.75f, + mNotificationsAlpha = mTransparentScrimBackground + ? 0 : MathUtils.constrainedMap(0f, 1f, 0.3f, 0.75f, mPanelExpansionFraction); } mBehindTint = mState.getBehindTint(); @@ -801,6 +812,13 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mBehindTint, interpolatedFraction); } + + // If we're unlocked but still playing the occlude animation, remain at the keyguard + // alpha temporarily. + if (mKeyguardViewMediator.isOccludeAnimationPlaying() + || mState.mLaunchingAffordanceWithPreview) { + mNotificationsAlpha = KEYGUARD_SCRIM_ALPHA; + } } else if (mState == ScrimState.AUTH_SCRIMMED_SHADE) { float behindFraction = getInterpolatedFraction(); behindFraction = (float) Math.pow(behindFraction, 0.8f); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index ece7ee0ec98a..86f6ff850409 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -372,7 +372,7 @@ public interface StatusBarIconController { mIconSize = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_icon_size); - if (statusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + if (statusBarPipelineFlags.useNewMobileIcons()) { // This starts the flow for the new pipeline, and will notify us of changes mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel(); MobileIconsBinder.bind(mGroup, mMobileIconsViewModel); @@ -451,7 +451,7 @@ public interface StatusBarIconController { @VisibleForTesting protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) { final BaseStatusBarFrameLayout view; - if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + if (mStatusBarPipelineFlags.useNewWifiIcon()) { view = onCreateModernStatusBarWifiView(slot); // When [ModernStatusBarWifiView] is created, it will automatically apply the // correct view state so we don't need to call applyWifiState. @@ -474,9 +474,9 @@ public interface StatusBarIconController { String slot, MobileIconState state ) { - if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + if (mStatusBarPipelineFlags.useNewMobileIcons()) { throw new IllegalStateException("Attempting to add a mobile icon while the new " - + "pipeline is enabled is not supported"); + + "icons are enabled is not supported"); } // Use the `subId` field as a key to query for the correct context @@ -497,7 +497,7 @@ public interface StatusBarIconController { String slot, int subId ) { - if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + if (!mStatusBarPipelineFlags.useNewMobileIcons()) { throw new IllegalStateException("Attempting to add a mobile icon using the new" + "pipeline, but the enabled flag is false."); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java index e106b9e327ef..31e960ad7d69 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java @@ -224,9 +224,9 @@ public class StatusBarIconControllerImpl implements Tunable, */ @Override public void setMobileIcons(String slot, List<MobileIconState> iconStates) { - if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + if (mStatusBarPipelineFlags.useNewMobileIcons()) { Log.d(TAG, "ignoring old pipeline callbacks, because the new " - + "pipeline frontend is enabled"); + + "icons are enabled"); return; } Slot mobileSlot = mStatusBarIconList.getSlot(slot); @@ -249,9 +249,9 @@ public class StatusBarIconControllerImpl implements Tunable, @Override public void setNewMobileIconSubIds(List<Integer> subIds) { - if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + if (!mStatusBarPipelineFlags.useNewMobileIcons()) { Log.d(TAG, "ignoring new pipeline callback, " - + "since the frontend is disabled"); + + "since the new icons are disabled"); return; } Slot mobileSlot = mStatusBarIconList.getSlot("mobile"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index ebc79ecde134..ccb5d8800ddb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -60,7 +60,6 @@ import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.data.BouncerView; -import com.android.systemui.keyguard.data.BouncerViewDelegate; import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor; import com.android.systemui.keyguard.domain.interactor.BouncerInteractor; import com.android.systemui.navigationbar.NavigationBarView; @@ -136,7 +135,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private KeyguardMessageAreaController<AuthKeyguardMessageArea> mKeyguardMessageAreaController; private final BouncerCallbackInteractor mBouncerCallbackInteractor; private final BouncerInteractor mBouncerInteractor; - private final BouncerViewDelegate mBouncerViewDelegate; + private final BouncerView mBouncerView; private final Lazy<com.android.systemui.shade.ShadeController> mShadeController; private final BouncerExpansionCallback mExpansionCallback = new BouncerExpansionCallback() { @@ -203,7 +202,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (DEBUG) { Log.d(TAG, "onBackInvokedCallback() called, invoking onBackPressed()"); } - onBackPressed(false /* unused */); + onBackPressed(); }; private boolean mIsBackCallbackRegistered = false; @@ -327,7 +326,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mKeyguardSecurityModel = keyguardSecurityModel; mBouncerCallbackInteractor = bouncerCallbackInteractor; mBouncerInteractor = bouncerInteractor; - mBouncerViewDelegate = bouncerView.getDelegate(); + mBouncerView = bouncerView; mFoldAodAnimationController = sysUIUnfoldComponent .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null); mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER); @@ -804,7 +803,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private void setDozing(boolean dozing) { if (mDozing != dozing) { mDozing = dozing; - if (dozing || mBouncer.needsFullscreenBouncer() + if (dozing || needsFullscreenBouncer() || mKeyguardStateController.isOccluded()) { reset(dozing /* hideBouncerWhenShowing */); } @@ -1082,27 +1081,20 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * @return whether a back press can be handled right now. */ public boolean canHandleBackPressed() { - return mBouncer.isShowing(); + return bouncerIsShowing(); } /** * Notifies this manager that the back button has been pressed. */ - // TODO(b/244635782): This "accept boolean and ignore it, and always return false" was done - // to make it possible to check this in *and* allow merging to master, - // where ArcStatusBarKeyguardViewManager inherits this class, and its - // build will break if we change this interface. - // So, overall, while this function refactors the behavior of onBackPressed, - // (it now handles the back press, and no longer returns *whether* it did so) - // its interface is not changing right now (but will, in a follow-up CL). - public boolean onBackPressed(boolean ignored) { + public void onBackPressed() { if (!canHandleBackPressed()) { - return false; + return; } mCentralSurfaces.endAffordanceLaunch(); // The second condition is for SIM card locked bouncer - if (bouncerIsScrimmed() && needsFullscreenBouncer()) { + if (bouncerIsScrimmed() && !needsFullscreenBouncer()) { hideBouncer(false); updateStates(); } else { @@ -1118,7 +1110,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mNotificationPanelViewController.expandWithoutQs(); } } - return false; + return; } @Override @@ -1132,8 +1124,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } public boolean isFullscreenBouncer() { - if (mBouncerViewDelegate != null) { - return mBouncerViewDelegate.isFullScreenBouncer(); + if (mBouncerView.getDelegate() != null) { + return mBouncerView.getDelegate().isFullScreenBouncer(); } return mBouncer != null && mBouncer.isFullscreenBouncer(); } @@ -1292,15 +1284,15 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } public boolean shouldDismissOnMenuPressed() { - if (mBouncerViewDelegate != null) { - return mBouncerViewDelegate.shouldDismissOnMenuPressed(); + if (mBouncerView.getDelegate() != null) { + return mBouncerView.getDelegate().shouldDismissOnMenuPressed(); } return mBouncer != null && mBouncer.shouldDismissOnMenuPressed(); } public boolean interceptMediaKey(KeyEvent event) { - if (mBouncerViewDelegate != null) { - return mBouncerViewDelegate.interceptMediaKey(event); + if (mBouncerView.getDelegate() != null) { + return mBouncerView.getDelegate().interceptMediaKey(event); } return mBouncer != null && mBouncer.interceptMediaKey(event); } @@ -1309,8 +1301,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * @return true if the pre IME back event should be handled */ public boolean dispatchBackKeyEventPreIme() { - if (mBouncerViewDelegate != null) { - return mBouncerViewDelegate.dispatchBackKeyEventPreIme(); + if (mBouncerView.getDelegate() != null) { + return mBouncerView.getDelegate().dispatchBackKeyEventPreIme(); } return mBouncer != null && mBouncer.dispatchBackKeyEventPreIme(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt index b9a1413ff791..81edff45c505 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt @@ -17,12 +17,12 @@ package com.android.systemui.statusbar.phone import android.app.PendingIntent -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.WARNING import com.android.systemui.log.dagger.NotifInteractionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index 64ca270558e8..a1e0c5067ef3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -179,7 +179,6 @@ class StatusBarNotificationPresenter implements NotificationPresenter, mNotifShadeEventSource.setNotifRemovedByUserCallback(this::maybeEndAmbientPulse); notificationInterruptStateProvider.addSuppressor(mInterruptSuppressor); mLockscreenUserManager.setUpWithPresenter(this); - mMediaManager.setUpWithPresenter(this); mGutsManager.setUpWithPresenter( this, mNotifListContainer, mOnSettingsClickListener); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt index 9ae378f34fc0..0e6b7f253bcf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.phone.fragment -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.CollapsedSbFragmentLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.DisableFlagsLogger import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt index 0d52f46e571f..e498ae451400 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.phone.userswitcher import android.content.Intent import android.os.UserHandle import android.view.View +import com.android.systemui.animation.Expandable import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.plugins.ActivityStarter @@ -75,7 +76,7 @@ class StatusBarUserSwitcherControllerImpl @Inject constructor( null /* ActivityLaunchAnimator.Controller */, true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM) } else { - userSwitcherDialogController.showDialog(view) + userSwitcherDialogController.showDialog(view.context, Expandable.fromView(view)) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt index 9b8b6434827e..06cd12dd1a0d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt @@ -24,29 +24,19 @@ import javax.inject.Inject /** All flagging methods related to the new status bar pipeline (see b/238425913). */ @SysUISingleton class StatusBarPipelineFlags @Inject constructor(private val featureFlags: FeatureFlags) { - /** - * Returns true if we should run the new pipeline backend. - * - * The new pipeline backend hooks up to all our external callbacks, logs those callback inputs, - * and logs the output state. - */ - fun isNewPipelineBackendEnabled(): Boolean = - featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_BACKEND) + /** True if we should display the mobile icons using the new status bar data pipeline. */ + fun useNewMobileIcons(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS) - /** - * Returns true if we should run the new pipeline frontend *and* backend. - * - * The new pipeline frontend will use the outputted state from the new backend and will make the - * correct changes to the UI. - */ - fun isNewPipelineFrontendEnabled(): Boolean = - isNewPipelineBackendEnabled() && - featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_FRONTEND) + /** True if we should display the wifi icon using the new status bar data pipeline. */ + fun useNewWifiIcon(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON) + + // TODO(b/238425913): Add flags to only run the mobile backend or wifi backend so we get the + // logging without getting the UI effects. /** - * Returns true if we should apply some coloring to icons that were rendered with the new + * Returns true if we should apply some coloring to the wifi icon that was rendered with the new * pipeline to help with debugging. */ - // For now, just always apply the debug coloring if we've enabled frontend rendering. - fun useNewPipelineDebugColoring(): Boolean = isNewPipelineFrontendEnabled() + // For now, just always apply the debug coloring if we've enabled the new icon. + fun useWifiDebugColoring(): Boolean = useNewWifiIcon() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt new file mode 100644 index 000000000000..7aa5ee1389f3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt @@ -0,0 +1,93 @@ +/* + * 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.pipeline.airplane.data.repository + +import android.os.Handler +import android.os.UserHandle +import android.provider.Settings.Global +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.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange +import com.android.systemui.util.settings.GlobalSettings +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.stateIn + +/** + * Provides data related to airplane mode. + * + * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. It is + * only used to help [com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel] + * determine what parts of the wifi icon view should be shown. + * + * TODO(b/238425913): Consider migrating the status bar airplane mode icon to use this repo. + */ +interface AirplaneModeRepository { + /** Observable for whether the device is currently in airplane mode. */ + val isAirplaneMode: StateFlow<Boolean> +} + +@SysUISingleton +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class AirplaneModeRepositoryImpl +@Inject +constructor( + @Background private val bgHandler: Handler, + private val globalSettings: GlobalSettings, + logger: ConnectivityPipelineLogger, + @Application scope: CoroutineScope, +) : AirplaneModeRepository { + // TODO(b/254848912): Replace this with a generic SettingObserver coroutine once we have it. + override val isAirplaneMode: StateFlow<Boolean> = + conflatedCallbackFlow { + val observer = + object : + SettingObserver( + globalSettings, + bgHandler, + Global.AIRPLANE_MODE_ON, + UserHandle.USER_ALL + ) { + override fun handleValueChanged(value: Int, observedChange: Boolean) { + trySend(value == 1) + } + } + + observer.isListening = true + trySend(observer.value == 1) + awaitClose { observer.isListening = false } + } + .distinctUntilChanged() + .logInputChange(logger, "isAirplaneMode") + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + // When the observer starts listening, the flow will emit the current value so the + // initialValue here is irrelevant. + initialValue = false, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt new file mode 100644 index 000000000000..3e9b2c2ae809 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt @@ -0,0 +1,46 @@ +/* + * 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.pipeline.airplane.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * The business logic layer for airplane mode. + * + * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. See + * [AirplaneModeRepository] for more details. + */ +@SysUISingleton +class AirplaneModeInteractor +@Inject +constructor( + airplaneModeRepository: AirplaneModeRepository, + connectivityRepository: ConnectivityRepository, +) { + /** True if the device is currently in airplane mode. */ + val isAirplaneMode: Flow<Boolean> = airplaneModeRepository.isAirplaneMode + + /** True if we're configured to force-hide the airplane mode icon and false otherwise. */ + val isForceHidden: Flow<Boolean> = + connectivityRepository.forceHiddenSlots.map { it.contains(ConnectivitySlot.AIRPLANE) } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt new file mode 100644 index 000000000000..fe30c0169021 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt @@ -0,0 +1,57 @@ +/* + * 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.pipeline.airplane.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.stateIn + +/** + * Models the UI state for the status bar airplane mode icon. + * + * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. See + * [com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository] for + * more details. + */ +@SysUISingleton +class AirplaneModeViewModel +@Inject +constructor( + interactor: AirplaneModeInteractor, + logger: ConnectivityPipelineLogger, + @Application private val scope: CoroutineScope, +) { + /** True if the airplane mode icon is currently visible in the status bar. */ + val isAirplaneModeIconVisible: StateFlow<Boolean> = + combine(interactor.isAirplaneMode, interactor.isForceHidden) { + isAirplaneMode, + isAirplaneIconForceHidden -> + isAirplaneMode && !isAirplaneIconForceHidden + } + .distinctUntilChanged() + .logOutputChange(logger, "isAirplaneModeIconVisible") + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 06d554232565..fcd1b8abefe4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -16,10 +16,16 @@ package com.android.systemui.statusbar.pipeline.dagger -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository @@ -30,16 +36,25 @@ import dagger.Module @Module abstract class StatusBarPipelineModule { @Binds + abstract fun airplaneModeRepository(impl: AirplaneModeRepositoryImpl): AirplaneModeRepository + + @Binds abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository @Binds - abstract fun mobileSubscriptionRepository( - impl: MobileSubscriptionRepositoryImpl - ): MobileSubscriptionRepository + abstract fun mobileConnectionsRepository( + impl: MobileConnectionsRepositoryImpl + ): MobileConnectionsRepository @Binds abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository + + @Binds + abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy + + @Binds + abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt index 46ccf32cc7f9..eaba0e93e750 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt @@ -27,6 +27,7 @@ import android.telephony.TelephonyCallback.ServiceStateListener import android.telephony.TelephonyCallback.SignalStrengthsListener import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyManager +import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN /** * Data class containing all of the relevant information for a particular line of service, known as @@ -57,6 +58,11 @@ data class MobileSubscriptionModel( /** From [CarrierNetworkListener.onCarrierNetworkChange] */ val carrierNetworkChangeActive: Boolean? = null, - /** From [DisplayInfoListener.onDisplayInfoChanged] */ - val displayInfo: TelephonyDisplayInfo? = null + /** + * From [DisplayInfoListener.onDisplayInfoChanged]. + * + * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or + * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon + */ + val resolvedNetworkType: ResolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN), ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt new file mode 100644 index 000000000000..f385806c1b22 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.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.statusbar.pipeline.mobile.data.model + +import android.telephony.Annotation.NetworkType +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy + +/** + * A SysUI type to represent the [NetworkType] that we pull out of [TelephonyDisplayInfo]. Depending + * on whether or not the display info contains an override type, we may have to call different + * methods on [MobileMappingsProxy] to generate an icon lookup key. + */ +sealed interface ResolvedNetworkType { + @NetworkType val type: Int +} + +data class DefaultNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType + +data class OverrideNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index 36de2a254160..45284cf0332b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -21,23 +21,18 @@ import android.telephony.CellSignalStrengthCdma import android.telephony.ServiceState import android.telephony.SignalStrength import android.telephony.SubscriptionInfo -import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback -import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener -import android.telephony.TelephonyCallback.CarrierNetworkListener -import android.telephony.TelephonyCallback.DataActivityListener -import android.telephony.TelephonyCallback.DataConnectionStateListener -import android.telephony.TelephonyCallback.DisplayInfoListener -import android.telephony.TelephonyCallback.ServiceStateListener -import android.telephony.TelephonyCallback.SignalStrengthsListener import android.telephony.TelephonyDisplayInfo +import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE import android.telephony.TelephonyManager -import androidx.annotation.VisibleForTesting 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.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import java.lang.IllegalStateException import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -47,110 +42,64 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext /** - * Repo for monitoring the complete active subscription info list, to be consumed and filtered based - * on various policy + * Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a + * repository for each individual, tracked subscription via [MobileConnectionsRepository], and this + * repository is responsible for setting up a [TelephonyManager] object tied to its subscriptionId + * + * There should only ever be one [MobileConnectionRepository] per subscription, since + * [TelephonyManager] limits the number of callbacks that can be registered per process. + * + * This repository should have all of the relevant information for a single line of service, which + * eventually becomes a single icon in the status bar. */ -interface MobileSubscriptionRepository { - /** Observable list of current mobile subscriptions */ - val subscriptionsFlow: Flow<List<SubscriptionInfo>> - - /** Observable for the subscriptionId of the current mobile data connection */ - val activeMobileDataSubscriptionId: Flow<Int> - - /** Get or create an observable for the given subscription ID */ - fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> +interface MobileConnectionRepository { + /** + * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single + * listener + model. + */ + val subscriptionModelFlow: Flow<MobileSubscriptionModel> } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) -@SysUISingleton -class MobileSubscriptionRepositoryImpl -@Inject -constructor( - private val subscriptionManager: SubscriptionManager, - private val telephonyManager: TelephonyManager, - @Background private val bgDispatcher: CoroutineDispatcher, - @Application private val scope: CoroutineScope, -) : MobileSubscriptionRepository { - private val subIdFlowCache: MutableMap<Int, StateFlow<MobileSubscriptionModel>> = mutableMapOf() - - /** - * State flow that emits the set of mobile data subscriptions, each represented by its own - * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each - * info object, but for now we keep track of the infos themselves. - */ - override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> = - conflatedCallbackFlow { - val callback = - object : SubscriptionManager.OnSubscriptionsChangedListener() { - override fun onSubscriptionsChanged() { - trySend(Unit) - } - } - - subscriptionManager.addOnSubscriptionsChangedListener( - bgDispatcher.asExecutor(), - callback, - ) - - awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } - } - .mapLatest { fetchSubscriptionsList() } - .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) - - /** StateFlow that keeps track of the current active mobile data subscription */ - override val activeMobileDataSubscriptionId: StateFlow<Int> = - conflatedCallbackFlow { - val callback = - object : TelephonyCallback(), ActiveDataSubscriptionIdListener { - override fun onActiveDataSubscriptionIdChanged(subId: Int) { - trySend(subId) - } - } - - telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) - awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } - } - .stateIn( - scope, - started = SharingStarted.WhileSubscribed(), - SubscriptionManager.INVALID_SUBSCRIPTION_ID +class MobileConnectionRepositoryImpl( + private val subId: Int, + telephonyManager: TelephonyManager, + bgDispatcher: CoroutineDispatcher, + logger: ConnectivityPipelineLogger, + scope: CoroutineScope, +) : MobileConnectionRepository { + init { + if (telephonyManager.subscriptionId != subId) { + throw IllegalStateException( + "TelephonyManager should be created with subId($subId). " + + "Found ${telephonyManager.subscriptionId} instead." ) - - /** - * Each mobile subscription needs its own flow, which comes from registering listeners on the - * system. Use this method to create those flows and cache them for reuse - */ - override fun getFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> { - return subIdFlowCache[subId] - ?: createFlowForSubId(subId).also { subIdFlowCache[subId] = it } + } } - @VisibleForTesting fun getSubIdFlowCache() = subIdFlowCache - - private fun createFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> = run { + override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run { var state = MobileSubscriptionModel() conflatedCallbackFlow { - val phony = telephonyManager.createForSubscriptionId(subId) // TODO (b/240569788): log all of these into the connectivity logger val callback = object : TelephonyCallback(), - ServiceStateListener, - SignalStrengthsListener, - DataConnectionStateListener, - DataActivityListener, - CarrierNetworkListener, - DisplayInfoListener { + TelephonyCallback.ServiceStateListener, + TelephonyCallback.SignalStrengthsListener, + TelephonyCallback.DataConnectionStateListener, + TelephonyCallback.DataActivityListener, + TelephonyCallback.CarrierNetworkListener, + TelephonyCallback.DisplayInfoListener { override fun onServiceStateChanged(serviceState: ServiceState) { state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly) trySend(state) } + override fun onSignalStrengthsChanged(signalStrength: SignalStrength) { val cdmaLevel = signalStrength @@ -173,6 +122,7 @@ constructor( ) trySend(state) } + override fun onDataConnectionStateChanged( dataState: Int, networkType: Int @@ -180,31 +130,56 @@ constructor( state = state.copy(dataConnectionState = dataState) trySend(state) } + override fun onDataActivity(direction: Int) { state = state.copy(dataActivityDirection = direction) trySend(state) } + override fun onCarrierNetworkChange(active: Boolean) { state = state.copy(carrierNetworkChangeActive = active) trySend(state) } + override fun onDisplayInfoChanged( telephonyDisplayInfo: TelephonyDisplayInfo ) { - state = state.copy(displayInfo = telephonyDisplayInfo) + val networkType = + if ( + telephonyDisplayInfo.overrideNetworkType == + OVERRIDE_NETWORK_TYPE_NONE + ) { + DefaultNetworkType(telephonyDisplayInfo.networkType) + } else { + OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType) + } + state = state.copy(resolvedNetworkType = networkType) trySend(state) } } - phony.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) - awaitClose { - phony.unregisterTelephonyCallback(callback) - // Release the cached flow - subIdFlowCache.remove(subId) - } + telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) + awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } + .onEach { logger.logOutputChange("mobileSubscriptionModel", it.toString()) } .stateIn(scope, SharingStarted.WhileSubscribed(), state) } - private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> = - withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList } + class Factory + @Inject + constructor( + private val telephonyManager: TelephonyManager, + private val logger: ConnectivityPipelineLogger, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + ) { + fun build(subId: Int): MobileConnectionRepository { + return MobileConnectionRepositoryImpl( + subId, + telephonyManager.createForSubscriptionId(subId), + bgDispatcher, + logger, + scope, + ) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt new file mode 100644 index 000000000000..0e2428ae393a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -0,0 +1,201 @@ +/* + * 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.pipeline.mobile.data.repository + +import android.content.Context +import android.content.IntentFilter +import android.telephony.CarrierConfigManager +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyCallback +import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener +import android.telephony.TelephonyManager +import androidx.annotation.VisibleForTesting +import com.android.settingslib.mobile.MobileMappings +import com.android.settingslib.mobile.MobileMappings.Config +import com.android.systemui.broadcast.BroadcastDispatcher +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.statusbar.pipeline.shared.ConnectivityPipelineLogger +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +/** + * Repo for monitoring the complete active subscription info list, to be consumed and filtered based + * on various policy + */ +interface MobileConnectionsRepository { + /** Observable list of current mobile subscriptions */ + val subscriptionsFlow: Flow<List<SubscriptionInfo>> + + /** Observable for the subscriptionId of the current mobile data connection */ + val activeMobileDataSubscriptionId: Flow<Int> + + /** Observable for [MobileMappings.Config] tracking the defaults */ + val defaultDataSubRatConfig: StateFlow<Config> + + /** Get or create a repository for the line of service for the given subscription ID */ + fun getRepoForSubId(subId: Int): MobileConnectionRepository +} + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class MobileConnectionsRepositoryImpl +@Inject +constructor( + private val subscriptionManager: SubscriptionManager, + private val telephonyManager: TelephonyManager, + private val logger: ConnectivityPipelineLogger, + broadcastDispatcher: BroadcastDispatcher, + private val context: Context, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory +) : MobileConnectionsRepository { + private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf() + + /** + * State flow that emits the set of mobile data subscriptions, each represented by its own + * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each + * info object, but for now we keep track of the infos themselves. + */ + override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> = + conflatedCallbackFlow { + val callback = + object : SubscriptionManager.OnSubscriptionsChangedListener() { + override fun onSubscriptionsChanged() { + trySend(Unit) + } + } + + subscriptionManager.addOnSubscriptionsChangedListener( + bgDispatcher.asExecutor(), + callback, + ) + + awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } + } + .mapLatest { fetchSubscriptionsList() } + .onEach { infos -> dropUnusedReposFromCache(infos) } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) + + /** StateFlow that keeps track of the current active mobile data subscription */ + override val activeMobileDataSubscriptionId: StateFlow<Int> = + conflatedCallbackFlow { + val callback = + object : TelephonyCallback(), ActiveDataSubscriptionIdListener { + override fun onActiveDataSubscriptionIdChanged(subId: Int) { + trySend(subId) + } + } + + telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) + awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } + } + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + SubscriptionManager.INVALID_SUBSCRIPTION_ID + ) + + private val defaultDataSubChangedEvent = + broadcastDispatcher.broadcastFlow( + IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + ) + + private val carrierConfigChangedEvent = + broadcastDispatcher.broadcastFlow( + IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED) + ) + + /** + * [Config] is an object that tracks relevant configuration flags for a given subscription ID. + * In the case of [MobileMappings], it's hard-coded to check the default data subscription's + * config, so this will apply to every icon that we care about. + * + * Relevant bits in the config are things like + * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL] + * + * This flow will produce whenever the default data subscription or the carrier config changes. + */ + override val defaultDataSubRatConfig: StateFlow<Config> = + combine(defaultDataSubChangedEvent, carrierConfigChangedEvent) { _, _ -> + Config.readConfig(context) + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + initialValue = Config.readConfig(context) + ) + + override fun getRepoForSubId(subId: Int): MobileConnectionRepository { + if (!isValidSubId(subId)) { + throw IllegalArgumentException( + "subscriptionId $subId is not in the list of valid subscriptions" + ) + } + + return subIdRepositoryCache[subId] + ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it } + } + + private fun isValidSubId(subId: Int): Boolean { + subscriptionsFlow.value.forEach { + if (it.subscriptionId == subId) { + return true + } + } + + return false + } + + @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache + + private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { + return mobileConnectionRepositoryFactory.build(subId) + } + + private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) { + // Remove any connection repository from the cache that isn't in the new set of IDs. They + // will get garbage collected once their subscribers go away + val currentValidSubscriptionIds = newInfos.map { it.subscriptionId } + + subIdRepositoryCache.keys.forEach { + if (!currentValidSubscriptionIds.contains(it)) { + subIdRepositoryCache.remove(it) + } + } + } + + private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> = + withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 40fe0f3e8fe0..15f4acc1127c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -17,32 +17,58 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CarrierConfigManager -import com.android.settingslib.SignalIcon -import com.android.settingslib.mobile.TelephonyIcons -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map interface MobileIconInteractor { - /** Identifier for RAT type indicator */ - val iconGroup: Flow<SignalIcon.MobileIconGroup> + /** Observable for RAT type (network type) indicator */ + val networkTypeIconGroup: Flow<MobileIconGroup> + /** True if this line of service is emergency-only */ val isEmergencyOnly: Flow<Boolean> + /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */ val level: Flow<Int> + /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */ val numberOfLevels: Flow<Int> + /** True when we want to draw an icon that makes room for the exclamation mark */ val cutOut: Flow<Boolean> } /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ class MobileIconInteractorImpl( - mobileStatusInfo: Flow<MobileSubscriptionModel>, + defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>, + defaultMobileIconGroup: Flow<MobileIconGroup>, + mobileMappingsProxy: MobileMappingsProxy, + connectionRepository: MobileConnectionRepository, ) : MobileIconInteractor { - override val iconGroup: Flow<SignalIcon.MobileIconGroup> = flowOf(TelephonyIcons.THREE_G) + private val mobileStatusInfo = connectionRepository.subscriptionModelFlow + + /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ + override val networkTypeIconGroup: Flow<MobileIconGroup> = + combine( + mobileStatusInfo, + defaultMobileIconMapping, + defaultMobileIconGroup, + ) { info, mapping, defaultGroup -> + val lookupKey = + when (val resolved = info.resolvedNetworkType) { + is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type) + is OverrideNetworkType -> mobileMappingsProxy.toIconKeyOverride(resolved.type) + } + mapping[lookupKey] ?: defaultGroup + } + override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly } override val level: Flow<Int> = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index 8e67e19f3e35..cd411a4a2afe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -19,29 +19,51 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CarrierConfigManager import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn /** - * Business layer logic for mobile subscription icons + * Business layer logic for the set of mobile subscription icons. * - * Mobile indicators represent the UI for the (potentially filtered) list of [SubscriptionInfo]s - * that the system knows about. They obey policy that depends on OEM, carrier, and locale configs + * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]). + * The list of subscriptions is filtered based on the opportunistic flags on the infos. + * + * It provides the default mapping between the telephony display info and the icon group that + * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual + * icon */ +interface MobileIconsInteractor { + val filteredSubscriptions: Flow<List<SubscriptionInfo>> + val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> + val defaultMobileIconGroup: Flow<MobileIconGroup> + val isUserSetup: Flow<Boolean> + fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor +} + @SysUISingleton -class MobileIconsInteractor +class MobileIconsInteractorImpl @Inject constructor( - private val mobileSubscriptionRepo: MobileSubscriptionRepository, + private val mobileSubscriptionRepo: MobileConnectionsRepository, private val carrierConfigTracker: CarrierConfigTracker, + private val mobileMappingsProxy: MobileMappingsProxy, userSetupRepo: UserSetupRepository, -) { + @Application private val scope: CoroutineScope, +) : MobileIconsInteractor { private val activeMobileDataSubscriptionId = mobileSubscriptionRepo.activeMobileDataSubscriptionId @@ -61,7 +83,7 @@ constructor( * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN], * and by checking which subscription is opportunistic, or which one is active. */ - val filteredSubscriptions: Flow<List<SubscriptionInfo>> = + override val filteredSubscriptions: Flow<List<SubscriptionInfo>> = combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId -> // Based on the old logic, @@ -92,15 +114,29 @@ constructor( } } - val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow - - /** Vends out new [MobileIconInteractor] for a particular subId */ - fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = - MobileIconInteractorImpl(mobileSubscriptionFlowForSubId(subId)) - /** - * Create a new flow for a given subscription ID, which usually maps 1:1 with mobile connections + * Mapping from network type to [MobileIconGroup] using the config generated for the default + * subscription Id. This mapping is the same for every subscription. */ - private fun mobileSubscriptionFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> = - mobileSubscriptionRepo.getFlowForSubId(subId) + override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> = + mobileSubscriptionRepo.defaultDataSubRatConfig + .map { mobileMappingsProxy.mapIconSets(it) } + .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf()) + + /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */ + override val defaultMobileIconGroup: StateFlow<MobileIconGroup> = + mobileSubscriptionRepo.defaultDataSubRatConfig + .map { mobileMappingsProxy.getDefaultIcons(it) } + .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G) + + override val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow + + /** Vends out new [MobileIconInteractor] for a particular subId */ + override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = + MobileIconInteractorImpl( + defaultMobileIconMapping, + defaultMobileIconGroup, + mobileMappingsProxy, + mobileSubscriptionRepo.getRepoForSubId(subId), + ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index 1405b050234b..67ea139271fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.binder import android.content.res.ColorStateList +import android.view.View.GONE +import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.ImageView import androidx.core.view.isVisible @@ -24,6 +26,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.settingslib.graph.SignalDrawable import com.android.systemui.R +import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel import kotlinx.coroutines.flow.collect @@ -37,6 +40,7 @@ object MobileIconBinder { view: ViewGroup, viewModel: MobileIconViewModel, ) { + val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type) val iconView = view.requireViewById<ImageView>(R.id.mobile_signal) val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) } @@ -52,10 +56,20 @@ object MobileIconBinder { } } + // Set the network type icon + launch { + viewModel.networkTypeIcon.distinctUntilChanged().collect { dataTypeId -> + dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) } + networkTypeView.visibility = if (dataTypeId != null) VISIBLE else GONE + } + } + // Set the tint launch { viewModel.tint.collect { tint -> - iconView.imageTintList = ColorStateList.valueOf(tint) + val tintList = ColorStateList.valueOf(tint) + iconView.imageTintList = tintList + networkTypeView.imageTintList = tintList } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index cfabeba8432c..cc8f6dd08585 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import android.graphics.Color import com.android.settingslib.graph.SignalDrawable +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger @@ -26,6 +28,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map /** * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over @@ -54,5 +57,15 @@ constructor( .distinctUntilChanged() .logOutputChange(logger, "iconId($subscriptionId)") + /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ + var networkTypeIcon: Flow<Icon?> = + iconInteractor.networkTypeIconGroup.map { + val desc = + if (it.dataContentDescription != 0) + ContentDescription.Resource(it.dataContentDescription) + else null + Icon.Resource(it.dataType, desc) + } + var tint: Flow<Int> = flowOf(Color.CYAN) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt new file mode 100644 index 000000000000..60bd0383f8c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.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.statusbar.pipeline.mobile.util + +import android.telephony.Annotation.NetworkType +import android.telephony.TelephonyDisplayInfo +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.MobileMappings +import com.android.settingslib.mobile.MobileMappings.Config +import javax.inject.Inject + +/** + * [MobileMappings] owns the logic on creating the map from [TelephonyDisplayInfo] to + * [MobileIconGroup]. It creates that hash map and also manages the creation of lookup keys. This + * interface allows us to proxy those calls to the static java methods in SettingsLib and also fake + * them out in tests + */ +interface MobileMappingsProxy { + fun mapIconSets(config: Config): Map<String, MobileIconGroup> + fun getDefaultIcons(config: Config): MobileIconGroup + fun toIconKey(@NetworkType networkType: Int): String + fun toIconKeyOverride(@NetworkType networkType: Int): String +} + +/** Injectable wrapper class for [MobileMappings] */ +class MobileMappingsProxyImpl @Inject constructor() : MobileMappingsProxy { + override fun mapIconSets(config: Config): Map<String, MobileIconGroup> = + MobileMappings.mapIconSets(config) + + override fun getDefaultIcons(config: Config): MobileIconGroup = + MobileMappings.getDefaultIcons(config) + + override fun toIconKey(@NetworkType networkType: Int): String = + MobileMappings.toIconKey(networkType) + + override fun toIconKeyOverride(networkType: Int): String = + MobileMappings.toDisplayIconKey(networkType) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt index dbb1aa54d8ee..d3cf32fb44ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt @@ -18,10 +18,10 @@ package com.android.systemui.statusbar.pipeline.shared import android.net.Network import android.net.NetworkCapabilities -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.StatusBarConnectivityLog +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString import javax.inject.Inject import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt index 681cf7254ae7..93448c1dee0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt @@ -39,7 +39,6 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel import java.util.concurrent.Executor @@ -64,6 +63,9 @@ interface WifiRepository { /** Observable for the current wifi enabled status. */ val isWifiEnabled: StateFlow<Boolean> + /** Observable for the current wifi default status. */ + val isWifiDefault: StateFlow<Boolean> + /** Observable for the current wifi network. */ val wifiNetwork: StateFlow<WifiNetworkModel> @@ -103,7 +105,7 @@ class WifiRepositoryImpl @Inject constructor( merge(wifiNetworkChangeEvents, wifiStateChangeEvents) .mapLatest { wifiManager.isWifiEnabled } .distinctUntilChanged() - .logOutputChange(logger, "enabled") + .logInputChange(logger, "enabled") .stateIn( scope = scope, started = SharingStarted.WhileSubscribed(), @@ -111,6 +113,39 @@ class WifiRepositoryImpl @Inject constructor( ) } + override val isWifiDefault: StateFlow<Boolean> = conflatedCallbackFlow { + // Note: This callback doesn't do any logging because we already log every network change + // in the [wifiNetwork] callback. + val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + // This method will always be called immediately after the network becomes the + // default, in addition to any time the capabilities change while the network is + // the default. + // If this network contains valid wifi info, then wifi is the default network. + val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities) + trySend(wifiInfo != null) + } + + override fun onLost(network: Network) { + // The system no longer has a default network, so wifi is definitely not default. + trySend(false) + } + } + + connectivityManager.registerDefaultNetworkCallback(callback) + awaitClose { connectivityManager.unregisterNetworkCallback(callback) } + } + .distinctUntilChanged() + .logInputChange(logger, "isWifiDefault") + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + initialValue = false + ) + override val wifiNetwork: StateFlow<WifiNetworkModel> = conflatedCallbackFlow { var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt index 04b17ed2924a..3a3e611de96a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt @@ -59,6 +59,9 @@ class WifiInteractor @Inject constructor( /** Our current enabled status. */ val isEnabled: Flow<Boolean> = wifiRepository.isWifiEnabled + /** Our current default status. */ + val isDefault: Flow<Boolean> = wifiRepository.isWifiDefault + /** Our current wifi network. See [WifiNetworkModel]. */ val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt index 273be63eb8a2..25537b948517 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt @@ -91,6 +91,7 @@ object WifiViewBinder { val activityInView = view.requireViewById<ImageView>(R.id.wifi_in) val activityOutView = view.requireViewById<ImageView>(R.id.wifi_out) val activityContainerView = view.requireViewById<View>(R.id.inout_container) + val airplaneSpacer = view.requireViewById<View>(R.id.wifi_airplane_spacer) view.isVisible = true iconView.isVisible = true @@ -142,6 +143,12 @@ object WifiViewBinder { activityContainerView.isVisible = visible } } + + launch { + viewModel.isAirplaneSpacerVisible.distinctUntilChanged().collect { visible -> + airplaneSpacer.isVisible = visible + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt index 40f948f9ee6c..95ab251422b2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt @@ -32,6 +32,7 @@ class HomeWifiViewModel( isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, + isAirplaneSpacerVisible: Flow<Boolean>, ) : LocationBasedWifiViewModel( statusBarPipelineFlags, @@ -40,4 +41,5 @@ class HomeWifiViewModel( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt index 9642ac42972e..86535d63f84f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt @@ -29,6 +29,7 @@ class KeyguardWifiViewModel( isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, + isAirplaneSpacerVisible: Flow<Boolean>, ) : LocationBasedWifiViewModel( statusBarPipelineFlags, @@ -37,4 +38,5 @@ class KeyguardWifiViewModel( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt index e23f8c7e97e0..7cbdf5dbdf2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt @@ -44,11 +44,14 @@ abstract class LocationBasedWifiViewModel( /** True if the activity container view should be visible. */ val isActivityContainerVisible: Flow<Boolean>, + + /** True if the airplane spacer view should be visible. */ + val isAirplaneSpacerVisible: Flow<Boolean>, ) { /** The color that should be used to tint the icon. */ val tint: Flow<Int> = flowOf( - if (statusBarPipelineFlags.useNewPipelineDebugColoring()) { + if (statusBarPipelineFlags.useWifiDebugColoring()) { debugTint } else { DEFAULT_TINT diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt index 0ddf90e21872..fd54c5f5062e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt @@ -29,6 +29,7 @@ class QsWifiViewModel( isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, + isAirplaneSpacerVisible: Flow<Boolean>, ) : LocationBasedWifiViewModel( statusBarPipelineFlags, @@ -37,4 +38,5 @@ class QsWifiViewModel( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt index ebbd77b72014..89b96b7bc75d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange @@ -66,6 +67,7 @@ import kotlinx.coroutines.flow.stateIn class WifiViewModel @Inject constructor( + airplaneModeViewModel: AirplaneModeViewModel, connectivityConstants: ConnectivityConstants, private val context: Context, logger: ConnectivityPipelineLogger, @@ -124,9 +126,10 @@ constructor( private val wifiIcon: StateFlow<Icon.Resource?> = combine( interactor.isEnabled, + interactor.isDefault, interactor.isForceHidden, interactor.wifiNetwork, - ) { isEnabled, isForceHidden, wifiNetwork -> + ) { isEnabled, isDefault, isForceHidden, wifiNetwork -> if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) { return@combine null } @@ -135,6 +138,7 @@ constructor( val icon = Icon.Resource(iconResId, wifiNetwork.contentDescription()) return@combine when { + isDefault -> icon wifiConstants.alwaysShowIconIfEnabled -> icon !connectivityConstants.hasDataCapabilities -> icon wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon @@ -175,6 +179,12 @@ constructor( } .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) + // TODO(b/238425913): It isn't ideal for the wifi icon to need to know about whether the + // airplane icon is visible. Instead, we should have a parent StatusBarSystemIconsViewModel + // that appropriately knows about both icons and sets the padding appropriately. + private val isAirplaneSpacerVisible: Flow<Boolean> = + airplaneModeViewModel.isAirplaneModeIconVisible + /** A view model for the status bar on the home screen. */ val home: HomeWifiViewModel = HomeWifiViewModel( @@ -183,6 +193,7 @@ constructor( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) /** A view model for the status bar on keyguard. */ @@ -193,6 +204,7 @@ constructor( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) /** A view model for the status bar in quick settings. */ @@ -203,6 +215,7 @@ constructor( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt index 2f0ebf752a23..cf4106c508cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt @@ -43,11 +43,7 @@ protected constructor( } override fun getCount(): Int { - return if (controller.isKeyguardShowing) { - users.count { !it.isRestricted } - } else { - users.size - } + return users.size } override fun getItem(position: Int): UserRecord { @@ -65,7 +61,7 @@ protected constructor( * animation to and from the parent dialog. */ @JvmOverloads - fun onUserListItemClicked( + open fun onUserListItemClicked( record: UserRecord, dialogShower: DialogShower? = null, ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt index d7c81af53d8b..df1e80b78c9b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt @@ -16,10 +16,10 @@ package com.android.systemui.statusbar.policy -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.VERBOSE import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.VERBOSE import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java index dc73d1f007c6..f63d65246d9b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java @@ -36,6 +36,7 @@ import com.android.keyguard.KeyguardVisibilityHelper; import com.android.keyguard.dagger.KeyguardUserSwitcherScope; import com.android.settingslib.drawable.CircleFramedDrawable; import com.android.systemui.R; +import com.android.systemui.animation.Expandable; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -190,7 +191,8 @@ public class KeyguardQsUserSwitchController extends ViewController<FrameLayout> mUiEventLogger.log( LockscreenGestureLogger.LockscreenUiEvent.LOCKSCREEN_SWITCH_USER_TAP); - mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground); + mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground.getContext(), + Expandable.fromView(mUserAvatarViewWithBackground)); }); mUserAvatarView.setAccessibilityDelegate(new View.AccessibilityDelegate() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index da6d455fbb97..dd400b3fc0ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -47,6 +47,7 @@ import android.view.OnReceiveContentListener; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; +import android.view.ViewRootImpl; import android.view.WindowInsets; import android.view.WindowInsetsAnimation; import android.view.WindowInsetsController; @@ -61,6 +62,8 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -88,6 +91,7 @@ import java.util.function.Consumer; */ public class RemoteInputView extends LinearLayout implements View.OnClickListener { + private static final boolean DEBUG = false; private static final String TAG = "RemoteInput"; // A marker object that let's us easily find views of this class. @@ -124,6 +128,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene // TODO(b/193539698): remove this; views shouldn't have access to their controller, and places // that need the controller shouldn't have access to the view private RemoteInputViewController mViewController; + private ViewRootImpl mTestableViewRootImpl; /** * Enum for logged notification remote input UiEvents. @@ -430,10 +435,20 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } } + @VisibleForTesting + protected void setViewRootImpl(ViewRootImpl viewRoot) { + mTestableViewRootImpl = viewRoot; + } + + @VisibleForTesting + protected void setEditTextReferenceToSelf() { + mEditText.mRemoteInputView = this; + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - mEditText.mRemoteInputView = this; + setEditTextReferenceToSelf(); mEditText.setOnEditorActionListener(mEditorActionHandler); mEditText.addTextChangedListener(mTextWatcher); if (mEntry.getRow().isChangingPosition()) { @@ -457,7 +472,50 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } @Override + public ViewRootImpl getViewRootImpl() { + if (mTestableViewRootImpl != null) { + return mTestableViewRootImpl; + } + return super.getViewRootImpl(); + } + + private void registerBackCallback() { + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot == null) { + if (DEBUG) { + Log.d(TAG, "ViewRoot was null, NOT registering Predictive Back callback"); + } + return; + } + if (DEBUG) { + Log.d(TAG, "registering Predictive Back callback"); + } + viewRoot.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_OVERLAY, mEditText.mOnBackInvokedCallback); + } + + private void unregisterBackCallback() { + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot == null) { + if (DEBUG) { + Log.d(TAG, "ViewRoot was null, NOT unregistering Predictive Back callback"); + } + return; + } + if (DEBUG) { + Log.d(TAG, "unregistering Predictive Back callback"); + } + viewRoot.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback( + mEditText.mOnBackInvokedCallback); + } + + @Override public void onVisibilityAggregated(boolean isVisible) { + if (isVisible) { + registerBackCallback(); + } else { + unregisterBackCallback(); + } super.onVisibilityAggregated(isVisible); mEditText.setEnabled(isVisible && !mSending); } @@ -822,10 +880,21 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene return super.onKeyDown(keyCode, event); } + private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { + if (DEBUG) { + Log.d(TAG, "Predictive Back Callback dispatched"); + } + respondToKeycodeBack(); + }; + + private void respondToKeycodeBack() { + defocusIfNeeded(true /* animate */); + } + @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { - defocusIfNeeded(true /* animate */); + respondToKeycodeBack(); return true; } return super.onKeyUp(keyCode, event); diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt index 5cbdf7c43a12..f0a50de02b3a 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt @@ -17,7 +17,6 @@ package com.android.systemui.temporarydisplay import android.annotation.LayoutRes -import android.annotation.SuppressLint import android.content.Context import android.graphics.PixelFormat import android.graphics.Rect @@ -67,11 +66,10 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora * Window layout params that will be used as a starting point for the [windowLayoutParams] of * all subclasses. */ - @SuppressLint("WrongConstant") // We're allowed to use TYPE_VOLUME_OVERLAY internal val commonWindowLayoutParams = WindowManager.LayoutParams().apply { width = WindowManager.LayoutParams.WRAP_CONTENT height = WindowManager.LayoutParams.WRAP_CONTENT - type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY + type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL title = windowTitle @@ -131,7 +129,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora ) cancelViewTimeout?.run() cancelViewTimeout = mainExecutor.executeDelayed( - { removeView(TemporaryDisplayRemovalReason.REASON_TIMEOUT) }, + { removeView(REMOVAL_REASON_TIMEOUT) }, timeout.toLong() ) } @@ -175,9 +173,6 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora */ fun removeView(removalReason: String) { val currentDisplayInfo = displayInfo ?: return - if (shouldIgnoreViewRemoval(currentDisplayInfo.info, removalReason)) { - return - } val currentView = currentDisplayInfo.view animateViewOut(currentView) { windowManager.removeView(currentView) } @@ -193,13 +188,6 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora } /** - * Returns true if a view removal request should be ignored and false otherwise. - * - * Allows subclasses to keep the view visible for longer in certain circumstances. - */ - open fun shouldIgnoreViewRemoval(info: T, removalReason: String): Boolean = false - - /** * A method implemented by subclasses to update [currentView] based on [newInfo]. */ abstract fun updateView(newInfo: T, currentView: ViewGroup) @@ -236,10 +224,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora ) } -object TemporaryDisplayRemovalReason { - const val REASON_TIMEOUT = "TIMEOUT" - const val REASON_SCREEN_TAP = "SCREEN_TAP" -} +private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT" private data class IconInfo( val iconName: String, diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt index 606a11a84686..a7185cb18c40 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt @@ -16,8 +16,8 @@ package com.android.systemui.temporarydisplay -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel /** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */ open class TemporaryViewLogger( diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt index a2cd1420a41c..b8930a45cd33 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -18,7 +18,6 @@ package com.android.systemui.temporarydisplay.chipbar import android.content.Context import android.graphics.Rect -import android.media.MediaRoute2Info import android.os.PowerManager import android.view.Gravity import android.view.MotionEvent @@ -27,26 +26,25 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.widget.TextView -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.internal.widget.CachingIconView import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.animation.Interpolators import com.android.systemui.animation.ViewHierarchyAnimator import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Text.Companion.loadText +import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.common.ui.binder.TextViewBinder import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.common.MediaTttUtils -import com.android.systemui.media.taptotransfer.sender.ChipStateSender import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger -import com.android.systemui.media.taptotransfer.sender.TransferStatus import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.temporarydisplay.TemporaryDisplayRemovalReason import com.android.systemui.temporarydisplay.TemporaryViewDisplayController -import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.view.ViewUtil import javax.inject.Inject @@ -79,11 +77,11 @@ open class ChipbarCoordinator @Inject constructor( accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, powerManager: PowerManager, - private val uiEventLogger: MediaTttSenderUiEventLogger, private val falsingManager: FalsingManager, private val falsingCollector: FalsingCollector, private val viewUtil: ViewUtil, -) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>( + private val vibratorHelper: VibratorHelper, +) : TemporaryViewDisplayController<ChipbarInfo, MediaTttLogger>( context, logger, windowManager, @@ -105,15 +103,13 @@ open class ChipbarCoordinator @Inject constructor( override fun start() {} override fun updateView( - newInfo: ChipSenderInfo, + newInfo: ChipbarInfo, currentView: ViewGroup ) { // TODO(b/245610654): Adding logging here. - val chipState = newInfo.state - // Detect falsing touches on the chip. - parent = currentView.requireViewById(R.id.media_ttt_sender_chip) + parent = currentView.requireViewById(R.id.chipbar_root_view) parent.touchHandler = object : Gefingerpoken { override fun onTouchEvent(ev: MotionEvent?): Boolean { falsingCollector.onTouchEvent(ev) @@ -121,47 +117,57 @@ open class ChipbarCoordinator @Inject constructor( } } - // App icon - val iconInfo = MediaTttUtils.getIconInfoFromPackageName( - context, newInfo.routeInfo.clientPackageName, logger - ) - val iconView = currentView.requireViewById<CachingIconView>(R.id.app_icon) - iconView.setImageDrawable(iconInfo.drawable) - iconView.contentDescription = iconInfo.contentDescription + // ---- Start icon ---- + val iconView = currentView.requireViewById<CachingIconView>(R.id.start_icon) + IconViewBinder.bind(newInfo.startIcon, iconView) - // Text - val otherDeviceName = newInfo.routeInfo.name.toString() - val chipText = chipState.getChipTextString(context, otherDeviceName) - currentView.requireViewById<TextView>(R.id.text).text = chipText + // ---- Text ---- + val textView = currentView.requireViewById<TextView>(R.id.text) + TextViewBinder.bind(textView, newInfo.text) + // Updates text view bounds to make sure it perfectly fits the new text + // (If the new text is smaller than the previous text) see b/253228632. + textView.requestLayout() + // ---- End item ---- // Loading currentView.requireViewById<View>(R.id.loading).visibility = - (chipState.transferStatus == TransferStatus.IN_PROGRESS).visibleIfTrue() - - // Undo - val undoView = currentView.requireViewById<View>(R.id.undo) - val undoClickListener = chipState.undoClickListener( - this, - newInfo.routeInfo, - newInfo.undoCallback, - uiEventLogger, - falsingManager, - ) - undoView.setOnClickListener(undoClickListener) - undoView.visibility = (undoClickListener != null).visibleIfTrue() + (newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue() + + // Error + currentView.requireViewById<View>(R.id.error).visibility = + (newInfo.endItem == ChipbarEndItem.Error).visibleIfTrue() - // Failure - currentView.requireViewById<View>(R.id.failure_icon).visibility = - (chipState.transferStatus == TransferStatus.FAILED).visibleIfTrue() + // Button + val buttonView = currentView.requireViewById<TextView>(R.id.end_button) + if (newInfo.endItem is ChipbarEndItem.Button) { + TextViewBinder.bind(buttonView, newInfo.endItem.text) - // For accessibility + val onClickListener = View.OnClickListener { clickedView -> + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener + newInfo.endItem.onClickListener.onClick(clickedView) + } + + buttonView.setOnClickListener(onClickListener) + buttonView.visibility = View.VISIBLE + } else { + buttonView.visibility = View.GONE + } + + // ---- Overall accessibility ---- currentView.requireViewById<ViewGroup>( - R.id.media_ttt_sender_chip_inner - ).contentDescription = "${iconInfo.contentDescription} $chipText" + R.id.chipbar_inner + ).contentDescription = + "${newInfo.startIcon.contentDescription.loadContentDescription(context)} " + + "${newInfo.text.loadText(context)}" + + // ---- Haptics ---- + newInfo.vibrationEffect?.let { + vibratorHelper.vibrate(it) + } } override fun animateViewIn(view: ViewGroup) { - val chipInnerView = view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner) + val chipInnerView = view.requireViewById<ViewGroup>(R.id.chipbar_inner) ViewHierarchyAnimator.animateAddition( chipInnerView, ViewHierarchyAnimator.Hotspot.TOP, @@ -176,7 +182,7 @@ open class ChipbarCoordinator @Inject constructor( override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { ViewHierarchyAnimator.animateRemoval( - view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner), + view.requireViewById<ViewGroup>(R.id.chipbar_inner), ViewHierarchyAnimator.Hotspot.TOP, Interpolators.EMPHASIZED_ACCELERATE, ANIMATION_DURATION, @@ -185,23 +191,6 @@ open class ChipbarCoordinator @Inject constructor( ) } - override fun shouldIgnoreViewRemoval(info: ChipSenderInfo, removalReason: String): Boolean { - // Don't remove the chip if we're in progress or succeeded, since the user should still be - // able to see the status of the transfer. (But do remove it if it's finally timed out.) - val transferStatus = info.state.transferStatus - if ( - (transferStatus == TransferStatus.IN_PROGRESS || - transferStatus == TransferStatus.SUCCEEDED) && - removalReason != TemporaryDisplayRemovalReason.REASON_TIMEOUT - ) { - logger.logRemovalBypass( - removalReason, bypassReason = "transferStatus=${transferStatus.name}" - ) - return true - } - return false - } - override fun getTouchableRegion(view: View, outRect: Rect) { viewUtil.setRectToViewWindowLocation(view, outRect) } @@ -215,13 +204,4 @@ open class ChipbarCoordinator @Inject constructor( } } -data class ChipSenderInfo( - val state: ChipStateSender, - val routeInfo: MediaRoute2Info, - val undoCallback: IUndoMediaTransferCallback? = null -) : TemporaryViewInfo { - override fun getTimeoutMs() = state.timeout -} - -const val SENDER_TAG = "MediaTapToTransferSender" private const val ANIMATION_DURATION = 500L diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt new file mode 100644 index 000000000000..57fde87114d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt @@ -0,0 +1,56 @@ +/* + * 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.temporarydisplay.chipbar + +import android.os.VibrationEffect +import android.view.View +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.temporarydisplay.TemporaryViewInfo + +/** + * A container for all the state needed to display a chipbar via [ChipbarCoordinator]. + * + * @property startIcon the icon to display at the start of the chipbar (on the left in LTR locales; + * on the right in RTL locales). + * @property text the text to display. + * @property endItem an optional end item to display at the end of the chipbar (on the right in LTR + * locales; on the left in RTL locales). + * @property vibrationEffect an optional vibration effect when the chipbar is displayed + */ +data class ChipbarInfo( + val startIcon: Icon, + val text: Text, + val endItem: ChipbarEndItem?, + val vibrationEffect: VibrationEffect? = null, +) : TemporaryViewInfo + +/** The possible items to display at the end of the chipbar. */ +sealed class ChipbarEndItem { + /** A loading icon should be displayed. */ + object Loading : ChipbarEndItem() + + /** An error icon should be displayed. */ + object Error : ChipbarEndItem() + + /** + * A button with the provided [text] and [onClickListener] functionality should be displayed. + */ + data class Button(val text: Text, val onClickListener: View.OnClickListener) : ChipbarEndItem() + + // TODO(b/245610654): Add support for a generic icon. +} diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 3d56f2317660..3ecb15b9d79c 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -79,6 +79,7 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -114,6 +115,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { private final SecureSettings mSecureSettings; private final Executor mMainExecutor; private final Handler mBgHandler; + private final boolean mIsMonochromaticEnabled; private final Context mContext; private final boolean mIsMonetEnabled; private final UserTracker mUserTracker; @@ -363,6 +365,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags, @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) { mContext = context; + mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEMES); mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET); mDeviceProvisionedController = deviceProvisionedController; mBroadcastDispatcher = broadcastDispatcher; @@ -665,8 +668,13 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { // Allow-list of Style objects that can be created from a setting string, i.e. can be // used as a system-wide theme. // - Content intentionally excluded, intended for media player, not system-wide - List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT, - Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT); + List<Style> validStyles = new ArrayList<>(Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, + Style.TONAL_SPOT, Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT)); + + if (mIsMonochromaticEnabled) { + validStyles.add(Style.MONOCHROMATIC); + } + Style style = mThemeStyle; final String overlayPackageJson = mSecureSettings.getStringForUser( Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt index 51541bd3032e..fda511433143 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt @@ -16,11 +16,11 @@ package com.android.systemui.toast -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.ToastLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogMessage import javax.inject.Inject private const val TAG = "ToastLog" diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt index 7f1195b78c77..7da2d47c1226 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt @@ -17,7 +17,8 @@ package com.android.systemui.user import android.os.Bundle -import android.view.View +import android.view.WindowInsets.Type +import android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE import androidx.activity.ComponentActivity import androidx.lifecycle.ViewModelProvider import com.android.systemui.R @@ -38,10 +39,10 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.user_switcher_fullscreen) - window.decorView.systemUiVisibility = - (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) + window.decorView.getWindowInsetsController().apply { + setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) + hide(Type.systemBars()) + } val viewModel = ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java] UserSwitcherViewBinder.bind( diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt index ee785b62bd50..088cd93bdf7e 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt @@ -36,9 +36,7 @@ class UserSwitcherPopupMenu( private var adapter: ListAdapter? = null init { - setBackgroundDrawable( - res.getDrawable(R.drawable.bouncer_user_switcher_popup_bg, context.getTheme()) - ) + setBackgroundDrawable(null) setModal(false) setOverlapAnchor(true) } diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index 919e699652bc..b16dc5403a57 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -220,7 +220,12 @@ constructor( val result = withContext(backgroundDispatcher) { manager.aliveUsers } if (result != null) { - _userInfos.value = result.sortedBy { it.creationTime } + _userInfos.value = + result + // Users should be sorted by ascending creation time. + .sortedBy { it.creationTime } + // The guest user is always last, regardless of creation time. + .sortedBy { it.isGuest } } } } @@ -321,6 +326,7 @@ constructor( return when { isAddUser -> false isAddSupervisedUser -> false + isManageUsers -> false isGuest -> info != null else -> true } @@ -346,6 +352,7 @@ constructor( isAddUser -> UserActionModel.ADD_USER isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER isGuest -> UserActionModel.ENTER_GUEST_MODE + isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT else -> error("Don't know how to convert to UserActionModel: $this") } } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index ba5a82a42d94..dda78aad54c6 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -236,18 +236,7 @@ constructor( } .flatMapLatest { isActionable -> if (isActionable) { - repository.actions.map { actions -> - actions + - if (actions.isNotEmpty()) { - // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT - // because that's a user switcher specific action that is - // not known to the our data source or other features. - listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - } else { - // If no actions, don't add the navigate action. - emptyList() - } - } + repository.actions } else { // If not actionable it means that we're not allowed to show actions // when @@ -440,6 +429,7 @@ constructor( isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), onExitGuestUser = this::exitGuestUser, + dialogShower = dialogShower, ) ) return @@ -454,6 +444,7 @@ constructor( isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), onExitGuestUser = this::exitGuestUser, + dialogShower = dialogShower, ) ) return @@ -488,6 +479,7 @@ constructor( userHandle = currentUser.userHandle, isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, + dialogShower = dialogShower, ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt index 08d7c5a26a25..177356e6b573 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt @@ -18,14 +18,18 @@ package com.android.systemui.user.domain.model import android.os.UserHandle +import com.android.systemui.qs.user.UserSwitchDialogController /** Encapsulates a request to show a dialog. */ -sealed class ShowDialogRequestModel { +sealed class ShowDialogRequestModel( + open val dialogShower: UserSwitchDialogController.DialogShower? = null, +) { data class ShowAddUserDialog( val userHandle: UserHandle, val isKeyguardShowing: Boolean, val showEphemeralMessage: Boolean, - ) : ShowDialogRequestModel() + override val dialogShower: UserSwitchDialogController.DialogShower?, + ) : ShowDialogRequestModel(dialogShower) data class ShowUserCreationDialog( val isGuest: Boolean, @@ -37,5 +41,6 @@ sealed class ShowDialogRequestModel { val isGuestEphemeral: Boolean, val isKeyguardShowing: Boolean, val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit, - ) : ShowDialogRequestModel() + override val dialogShower: UserSwitchDialogController.DialogShower?, + ) : ShowDialogRequestModel(dialogShower) } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt index 938417f9dbe3..968af59e6c45 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt @@ -18,12 +18,15 @@ package com.android.systemui.user.ui.binder import android.content.Context +import android.view.Gravity import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.LinearLayout.SHOW_DIVIDER_MIDDLE import android.widget.TextView import androidx.constraintlayout.helper.widget.Flow as FlowWidget import androidx.core.view.isVisible @@ -36,6 +39,7 @@ import com.android.systemui.R import com.android.systemui.classifier.FalsingCollector import com.android.systemui.user.UserSwitcherPopupMenu import com.android.systemui.user.UserSwitcherRootView +import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.ui.viewmodel.UserActionViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import com.android.systemui.util.children @@ -168,15 +172,10 @@ object UserSwitcherViewBinder { onDismissed: () -> Unit, ): UserSwitcherPopupMenu { return UserSwitcherPopupMenu(context).apply { + this.setDropDownGravity(Gravity.END) this.anchorView = anchorView setAdapter(adapter) setOnDismissListener { onDismissed() } - setOnItemClickListener { _, _, position, _ -> - val itemPositionExcludingHeader = position - 1 - adapter.getItem(itemPositionExcludingHeader).onClicked() - dismiss() - } - show() } } @@ -186,38 +185,67 @@ object UserSwitcherViewBinder { private val layoutInflater: LayoutInflater, ) : BaseAdapter() { - private val items = mutableListOf<UserActionViewModel>() + private var sections = listOf<List<UserActionViewModel>>() override fun getCount(): Int { - return items.size + return sections.size } - override fun getItem(position: Int): UserActionViewModel { - return items[position] + override fun getItem(position: Int): List<UserActionViewModel> { + return sections[position] } override fun getItemId(position: Int): Long { - return getItem(position).viewKey + return position.toLong() } override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = - convertView - ?: layoutInflater.inflate( + val section = getItem(position) + val context = parent.context + val sectionView = + convertView as? LinearLayout + ?: LinearLayout(context, null).apply { + this.orientation = LinearLayout.VERTICAL + this.background = + parent.resources.getDrawable( + R.drawable.bouncer_user_switcher_popup_bg, + context.theme + ) + this.showDividers = SHOW_DIVIDER_MIDDLE + this.dividerDrawable = + context.getDrawable( + R.drawable.fullscreen_userswitcher_menu_item_divider + ) + } + sectionView.removeAllViewsInLayout() + + for (viewModel in section) { + val view = + layoutInflater.inflate( R.layout.user_switcher_fullscreen_popup_item, - parent, - false + /* parent= */ null ) - val viewModel = getItem(position) - view.requireViewById<ImageView>(R.id.icon).setImageResource(viewModel.iconResourceId) - view.requireViewById<TextView>(R.id.text).text = - view.resources.getString(viewModel.textResourceId) - return view + view + .requireViewById<ImageView>(R.id.icon) + .setImageResource(viewModel.iconResourceId) + view.requireViewById<TextView>(R.id.text).text = + view.resources.getString(viewModel.textResourceId) + view.setOnClickListener { viewModel.onClicked() } + sectionView.addView(view) + } + return sectionView } fun setItems(items: List<UserActionViewModel>) { - this.items.clear() - this.items.addAll(items) + val primarySection = + items.filter { + it.viewKey != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong() + } + val secondarySection = + items.filter { + it.viewKey == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong() + } + this.sections = listOf(primarySection, secondarySection) notifyDataSetChanged() } } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt index 91c592177d19..e9217209530b 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -19,8 +19,10 @@ package com.android.systemui.user.ui.dialog import android.app.Dialog import android.content.Context +import com.android.internal.jank.InteractionJankMonitor import com.android.settingslib.users.UserCreatingDialog import com.android.systemui.CoreStartable +import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.SysUISingleton @@ -30,6 +32,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.plugins.FalsingManager import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.domain.model.ShowDialogRequestModel +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect @@ -41,19 +44,19 @@ import kotlinx.coroutines.launch class UserSwitcherDialogCoordinator @Inject constructor( - @Application private val context: Context, - @Application private val applicationScope: CoroutineScope, - private val falsingManager: FalsingManager, - private val broadcastSender: BroadcastSender, - private val dialogLaunchAnimator: DialogLaunchAnimator, - private val interactor: UserInteractor, - private val featureFlags: FeatureFlags, + @Application private val context: Lazy<Context>, + @Application private val applicationScope: Lazy<CoroutineScope>, + private val falsingManager: Lazy<FalsingManager>, + private val broadcastSender: Lazy<BroadcastSender>, + private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>, + private val interactor: Lazy<UserInteractor>, + private val featureFlags: Lazy<FeatureFlags>, ) : CoreStartable { private var currentDialog: Dialog? = null override fun start() { - if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) { + if (featureFlags.get().isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) { return } @@ -62,61 +65,87 @@ constructor( } private fun startHandlingDialogShowRequests() { - applicationScope.launch { - interactor.dialogShowRequests.filterNotNull().collect { request -> + applicationScope.get().launch { + interactor.get().dialogShowRequests.filterNotNull().collect { request -> currentDialog?.let { if (it.isShowing) { it.cancel() } } - currentDialog = + val (dialog, dialogCuj) = when (request) { is ShowDialogRequestModel.ShowAddUserDialog -> - AddUserDialog( - context = context, - userHandle = request.userHandle, - isKeyguardShowing = request.isKeyguardShowing, - showEphemeralMessage = request.showEphemeralMessage, - falsingManager = falsingManager, - broadcastSender = broadcastSender, - dialogLaunchAnimator = dialogLaunchAnimator, + Pair( + AddUserDialog( + context = context.get(), + userHandle = request.userHandle, + isKeyguardShowing = request.isKeyguardShowing, + showEphemeralMessage = request.showEphemeralMessage, + falsingManager = falsingManager.get(), + broadcastSender = broadcastSender.get(), + dialogLaunchAnimator = dialogLaunchAnimator.get(), + ), + DialogCuj( + InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, + INTERACTION_JANK_ADD_NEW_USER_TAG, + ), ) is ShowDialogRequestModel.ShowUserCreationDialog -> - UserCreatingDialog( - context, - request.isGuest, + Pair( + UserCreatingDialog( + context.get(), + request.isGuest, + ), + null, ) is ShowDialogRequestModel.ShowExitGuestDialog -> - ExitGuestDialog( - context = context, - guestUserId = request.guestUserId, - isGuestEphemeral = request.isGuestEphemeral, - targetUserId = request.targetUserId, - isKeyguardShowing = request.isKeyguardShowing, - falsingManager = falsingManager, - dialogLaunchAnimator = dialogLaunchAnimator, - onExitGuestUserListener = request.onExitGuestUser, + Pair( + ExitGuestDialog( + context = context.get(), + guestUserId = request.guestUserId, + isGuestEphemeral = request.isGuestEphemeral, + targetUserId = request.targetUserId, + isKeyguardShowing = request.isKeyguardShowing, + falsingManager = falsingManager.get(), + dialogLaunchAnimator = dialogLaunchAnimator.get(), + onExitGuestUserListener = request.onExitGuestUser, + ), + DialogCuj( + InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, + INTERACTION_JANK_EXIT_GUEST_MODE_TAG, + ), ) } + currentDialog = dialog - currentDialog?.show() - interactor.onDialogShown() + if (request.dialogShower != null && dialogCuj != null) { + request.dialogShower?.showDialog(dialog, dialogCuj) + } else { + dialog.show() + } + + interactor.get().onDialogShown() } } } private fun startHandlingDialogDismissRequests() { - applicationScope.launch { - interactor.dialogDismissRequests.filterNotNull().collect { + applicationScope.get().launch { + interactor.get().dialogDismissRequests.filterNotNull().collect { currentDialog?.let { if (it.isShowing) { it.cancel() } } - interactor.onDialogDismissed() + interactor.get().onDialogDismissed() } } } + + companion object { + private const val INTERACTION_JANK_ADD_NEW_USER_TAG = "add_new_user" + private const val INTERACTION_JANK_EXIT_GUEST_MODE_TAG = "exit_guest_mode" + } } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index 219dae29117f..d857e85bac53 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -62,17 +62,7 @@ private constructor( val isMenuVisible: Flow<Boolean> = _isMenuVisible /** The user action menu. */ val menu: Flow<List<UserActionViewModel>> = - userInteractor.actions.map { actions -> - if (isNewImpl && actions.isNotEmpty()) { - // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user - // switcher specific action that is not known to the our data source or other - // features. - actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - } else { - actions - } - .map { action -> toViewModel(action) } - } + userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } } /** Whether the button to open the user action menu is visible. */ val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() } diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java index ecb365f43e3f..2c317dd391c0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java +++ b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java @@ -172,10 +172,14 @@ public abstract class Condition implements CallbackController<Condition.Callback return Boolean.TRUE.equals(mIsConditionMet); } - private boolean shouldLog() { + protected final boolean shouldLog() { return Log.isLoggable(mTag, Log.DEBUG); } + protected final String getTag() { + return mTag; + } + /** * Callback that receives updates about whether the condition has been fulfilled. */ diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java index 4824f6744c6e..cb430ba454f0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java +++ b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java @@ -117,6 +117,7 @@ public class Monitor { final SubscriptionState state = new SubscriptionState(subscription); mExecutor.execute(() -> { + if (shouldLog()) Log.d(mTag, "adding subscription"); mSubscriptions.put(token, state); // Add and associate conditions. @@ -143,7 +144,7 @@ public class Monitor { */ public void removeSubscription(@NotNull Subscription.Token token) { mExecutor.execute(() -> { - if (shouldLog()) Log.d(mTag, "removing callback"); + if (shouldLog()) Log.d(mTag, "removing subscription"); if (!mSubscriptions.containsKey(token)) { Log.e(mTag, "subscription not present:" + token); return; diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt new file mode 100644 index 000000000000..9653985cb6e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -0,0 +1,40 @@ +/* + * 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.util.kotlin + +import android.view.View +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import java.util.function.Consumer +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect + +/** + * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to + * [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners + * during onViewAttached() and removing during onViewRemoved() + */ +@JvmOverloads +fun <T> collectFlow( + view: View, + flow: Flow<T>, + consumer: Consumer<T>, + state: Lifecycle.State = Lifecycle.State.CREATED, +) { + view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto new file mode 100644 index 000000000000..b7166d96d401 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto @@ -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. + */ + +syntax = "proto3"; + +package com.android.systemui.util; + +option java_multiple_files = true; + +message ComponentNameProto { + string package_name = 1; + string class_name = 2; +} diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index fbc6a582da2e..309f1681b964 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -22,6 +22,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_B import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; @@ -55,6 +56,8 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.tracing.nano.SystemUiTraceProto; +import com.android.wm.shell.desktopmode.DesktopMode; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.floating.FloatingTasks; import com.android.wm.shell.nano.WmShellTraceProto; import com.android.wm.shell.onehanded.OneHanded; @@ -111,6 +114,7 @@ public final class WMShell implements private final Optional<SplitScreen> mSplitScreenOptional; private final Optional<OneHanded> mOneHandedOptional; private final Optional<FloatingTasks> mFloatingTasksOptional; + private final Optional<DesktopMode> mDesktopModeOptional; private final CommandQueue mCommandQueue; private final ConfigurationController mConfigurationController; @@ -173,6 +177,7 @@ public final class WMShell implements Optional<SplitScreen> splitScreenOptional, Optional<OneHanded> oneHandedOptional, Optional<FloatingTasks> floatingTasksOptional, + Optional<DesktopMode> desktopMode, CommandQueue commandQueue, ConfigurationController configurationController, KeyguardStateController keyguardStateController, @@ -194,6 +199,7 @@ public final class WMShell implements mPipOptional = pipOptional; mSplitScreenOptional = splitScreenOptional; mOneHandedOptional = oneHandedOptional; + mDesktopModeOptional = desktopMode; mWakefulnessLifecycle = wakefulnessLifecycle; mProtoTracer = protoTracer; mUserTracker = userTracker; @@ -219,6 +225,7 @@ public final class WMShell implements mPipOptional.ifPresent(this::initPip); mSplitScreenOptional.ifPresent(this::initSplitScreen); mOneHandedOptional.ifPresent(this::initOneHanded); + mDesktopModeOptional.ifPresent(this::initDesktopMode); } @VisibleForTesting @@ -326,6 +333,16 @@ public final class WMShell implements }); } + void initDesktopMode(DesktopMode desktopMode) { + desktopMode.addListener(new DesktopModeTaskRepository.VisibleTasksListener() { + @Override + public void onVisibilityChanged(boolean hasFreeformTasks) { + mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE, hasFreeformTasks) + .commitUpdate(DEFAULT_DISPLAY); + } + }, mSysUiMainExecutor); + } + @Override public void writeToProto(SystemUiTraceProto proto) { if (proto.wmShell == null) { diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index ba2804572ef5..1b404a82145b 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -88,6 +88,11 @@ android:excludeFromRecents="true" /> + <activity android:name=".settings.brightness.BrightnessDialogTest$TestDialog" + android:exported="false" + android:excludeFromRecents="true" + /> + <activity android:name="com.android.systemui.screenshot.ScrollViewActivity" android:exported="false" /> diff --git a/packages/SystemUI/tests/res/layout/custom_view_dark.xml b/packages/SystemUI/tests/res/layout/custom_view_dark.xml index 9e460a5819a9..112d73d2d7f2 100644 --- a/packages/SystemUI/tests/res/layout/custom_view_dark.xml +++ b/packages/SystemUI/tests/res/layout/custom_view_dark.xml @@ -14,6 +14,7 @@ limitations under the License. --> <ImageView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/custom_view_dark_image" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ff000000" diff --git a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt new file mode 100644 index 000000000000..7b9b39f23c29 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard + +import android.content.Context +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.util.AttributeSet +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class BouncerKeyguardMessageAreaTest : SysuiTestCase() { + class FakeBouncerKeyguardMessageArea(context: Context, attrs: AttributeSet?) : + BouncerKeyguardMessageArea(context, attrs) { + override val SHOW_DURATION_MILLIS = 0L + override val HIDE_DURATION_MILLIS = 0L + } + lateinit var underTest: BouncerKeyguardMessageArea + + @Before + fun setup() { + underTest = FakeBouncerKeyguardMessageArea(context, null) + } + + @Test + fun testSetSameMessage() { + val underTestSpy = spy(underTest) + underTestSpy.setMessage("abc") + underTestSpy.setMessage("abc") + verify(underTestSpy, times(1)).text = "abc" + } + + @Test + fun testSetDifferentMessage() { + underTest.setMessage("abc") + underTest.setMessage("def") + assertThat(underTest.text).isEqualTo("def") + } + + @Test + fun testSetNullMessage() { + underTest.setMessage(null) + assertThat(underTest.text).isEqualTo("") + } + + @Test + fun testSetNullClearsPreviousMessage() { + underTest.setMessage("something not null") + assertThat(underTest.text).isEqualTo("something not null") + + underTest.setMessage(null) + assertThat(underTest.text).isEqualTo("") + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 8a2c35410586..1c3656d71d82 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -17,17 +17,22 @@ package com.android.keyguard import android.content.BroadcastReceiver import android.testing.AndroidTestingRunner +import android.view.View import android.widget.TextView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.FeatureFlags +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.plugins.ClockAnimations import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockEvents import com.android.systemui.plugins.ClockFaceController import com.android.systemui.plugins.ClockFaceEvents -import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.mockito.any @@ -37,6 +42,9 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import java.util.TimeZone import java.util.concurrent.Executor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -57,7 +65,7 @@ import org.mockito.junit.MockitoJUnit class ClockEventControllerTest : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() - @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var keyguardInteractor: KeyguardInteractor @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var batteryController: BatteryController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @@ -72,8 +80,11 @@ class ClockEventControllerTest : SysuiTestCase() { @Mock private lateinit var largeClockController: ClockFaceController @Mock private lateinit var smallClockEvents: ClockFaceEvents @Mock private lateinit var largeClockEvents: ClockFaceEvents - - private lateinit var clockEventController: ClockEventController + @Mock private lateinit var parentView: View + @Mock private lateinit var transitionRepository: KeyguardTransitionRepository + private lateinit var repository: FakeKeyguardRepository + @Mock private lateinit var logBuffer: LogBuffer + private lateinit var underTest: ClockEventController @Before fun setUp() { @@ -86,8 +97,11 @@ class ClockEventControllerTest : SysuiTestCase() { whenever(clock.events).thenReturn(events) whenever(clock.animations).thenReturn(animations) - clockEventController = ClockEventController( - statusBarStateController, + repository = FakeKeyguardRepository() + + underTest = ClockEventController( + KeyguardInteractor(repository = repository), + KeyguardTransitionInteractor(repository = transitionRepository), broadcastDispatcher, batteryController, keyguardUpdateMonitor, @@ -96,33 +110,36 @@ class ClockEventControllerTest : SysuiTestCase() { context, mainExecutor, bgExecutor, + logBuffer, featureFlags ) + underTest.clock = clock + + runBlocking(IMMEDIATE) { + underTest.registerListeners(parentView) + + repository.setDozing(true) + repository.setDozeAmount(1f) + } } @Test fun clockSet_validateInitialization() { - clockEventController.clock = clock - verify(clock).initialize(any(), anyFloat(), anyFloat()) } @Test fun clockUnset_validateState() { - clockEventController.clock = clock - clockEventController.clock = null + underTest.clock = null - assertEquals(clockEventController.clock, null) + assertEquals(underTest.clock, null) } @Test - fun themeChanged_verifyClockPaletteUpdated() { - clockEventController.clock = clock + fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) { verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) - clockEventController.registerListeners() - val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() verify(configurationController).addCallback(capture(captor)) captor.value.onThemeChanged() @@ -131,13 +148,10 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun fontChanged_verifyFontSizeUpdated() { - clockEventController.clock = clock + fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) { verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) - clockEventController.registerListeners() - val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() verify(configurationController).addCallback(capture(captor)) captor.value.onDensityOrFontScaleChanged() @@ -146,10 +160,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() @@ -161,26 +172,21 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - - val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() - verify(batteryController).addCallback(capture(batteryCaptor)) - val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() - verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) - keyguardCaptor.value.onKeyguardVisibilityChanged(true) - batteryCaptor.value.onBatteryLevelChanged(10, false, true) - batteryCaptor.value.onBatteryLevelChanged(10, false, true) - - verify(animations, times(1)).charge() - } + fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() = + runBlocking(IMMEDIATE) { + val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(batteryCaptor)) + val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) + keyguardCaptor.value.onKeyguardVisibilityChanged(true) + batteryCaptor.value.onBatteryLevelChanged(10, false, true) + batteryCaptor.value.onBatteryLevelChanged(10, false, true) + + verify(animations, times(1)).charge() + } @Test - fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() @@ -192,25 +198,20 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - - val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() - verify(batteryController).addCallback(capture(batteryCaptor)) - val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() - verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) - keyguardCaptor.value.onKeyguardVisibilityChanged(true) - batteryCaptor.value.onBatteryLevelChanged(10, false, false) - - verify(animations, never()).charge() - } + fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() = + runBlocking(IMMEDIATE) { + val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(batteryCaptor)) + val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) + keyguardCaptor.value.onKeyguardVisibilityChanged(true) + batteryCaptor.value.onBatteryLevelChanged(10, false, false) + + verify(animations, never()).charge() + } @Test - fun localeCallback_verifyClockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun localeCallback_verifyClockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<BroadcastReceiver>() verify(broadcastDispatcher).registerReceiver( capture(captor), any(), eq(null), eq(null), anyInt(), eq(null) @@ -221,10 +222,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_visibilityChanged_clockDozeCalled() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) @@ -236,10 +234,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_timeFormat_clockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_timeFormat_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onTimeFormatChanged("12h") @@ -248,11 +243,8 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_timezoneChanged_clockNotified() { + fun keyguardCallback_timezoneChanged_clockNotified() = runBlocking(IMMEDIATE) { val mockTimeZone = mock<TimeZone>() - clockEventController.clock = clock - clockEventController.registerListeners() - val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onTimeZoneChanged(mockTimeZone) @@ -261,10 +253,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_userSwitched_clockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_userSwitched_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onUserSwitchComplete(10) @@ -273,25 +262,27 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_verifyKeyguardChanged() { - clockEventController.clock = clock - clockEventController.registerListeners() + fun keyguardCallback_verifyKeyguardChanged() = runBlocking(IMMEDIATE) { + val job = underTest.listenForDozeAmount(this) + repository.setDozeAmount(0.4f) - val captor = argumentCaptor<StatusBarStateController.StateListener>() - verify(statusBarStateController).addCallback(capture(captor)) - captor.value.onDozeAmountChanged(0.4f, 0.6f) + yield() verify(animations).doze(0.4f) + + job.cancel() } @Test - fun unregisterListeners_validate() { - clockEventController.clock = clock - clockEventController.unregisterListeners() + fun unregisterListeners_validate() = runBlocking(IMMEDIATE) { + underTest.unregisterListeners() verify(broadcastDispatcher).unregisterReceiver(any()) verify(configurationController).removeCallback(any()) verify(batteryController).removeCallback(any()) verify(keyguardUpdateMonitor).removeCallback(any()) - verify(statusBarStateController).removeCallback(any()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index 400caa3a352a..61c7bb500e6a 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.when; import android.content.res.Resources; import android.database.ContentObserver; +import android.graphics.Rect; import android.net.Uri; import android.os.UserHandle; import android.provider.Settings; @@ -43,8 +44,8 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.plugins.ClockController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.AnimatableClockView; @@ -103,8 +104,6 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { private FrameLayout mLargeClockFrame; @Mock private SecureSettings mSecureSettings; - @Mock - private FeatureFlags mFeatureFlags; private final View mFakeSmartspaceView = new View(mContext); @@ -141,8 +140,7 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { mSecureSettings, mExecutor, mDumpManager, - mClockEventController, - mFeatureFlags + mClockEventController ); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); @@ -262,9 +260,22 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { verify(mView).switchToClock(KeyguardClockSwitch.SMALL, /* animate */ true); } + @Test + public void testGetClockAnimationsForwardsToClock() { + ClockController mockClockController = mock(ClockController.class); + ClockAnimations mockClockAnimations = mock(ClockAnimations.class); + when(mClockEventController.getClock()).thenReturn(mockClockController); + when(mockClockController.getAnimations()).thenReturn(mockClockAnimations); + + Rect r1 = new Rect(1, 2, 3, 4); + Rect r2 = new Rect(5, 6, 7, 8); + mController.getClockAnimations().onPositionUpdated(r1, r2, 0.2f); + verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.2f); + } + private void verifyAttachment(VerificationMode times) { verify(mClockRegistry, times).registerClockChangeListener( any(ClockRegistry.ClockChangeListener.class)); - verify(mClockEventController, times).registerListeners(); + verify(mClockEventController, times).registerListeners(mView); } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java index 69524e5a4537..5d2b0ca4e7ea 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java @@ -17,13 +17,11 @@ package com.android.keyguard; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; -import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; @@ -92,19 +90,4 @@ public class KeyguardMessageAreaControllerTest extends SysuiTestCase { mMessageAreaController.setIsVisible(true); verify(mKeyguardMessageArea).setIsVisible(true); } - - @Test - public void testSetMessageIfEmpty_empty() { - mMessageAreaController.setMessage(""); - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin); - verify(mKeyguardMessageArea).setMessage(R.string.keyguard_enter_your_pin); - } - - @Test - public void testSetMessageIfEmpty_notEmpty() { - mMessageAreaController.setMessage("abc"); - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin); - verify(mKeyguardMessageArea, never()).setMessage(getContext() - .getResources().getText(R.string.keyguard_enter_your_pin)); - } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt index b89dbd98968a..b369098cafc0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt @@ -114,9 +114,8 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() { } @Test - fun onResume_testSetInitialText() { - keyguardPasswordViewController.onResume(KeyguardSecurityView.SCREEN_ON) - verify(mKeyguardMessageAreaController) - .setMessageIfEmpty(R.string.keyguard_enter_your_password) + fun startAppearAnimation() { + keyguardPasswordViewController.startAppearAnimation() + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password) } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index 3262a77b7711..9eff70487c74 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt @@ -100,16 +100,16 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { } @Test - fun onPause_clearsTextField() { + fun onPause_resetsText() { mKeyguardPatternViewController.init() mKeyguardPatternViewController.onPause() - verify(mKeyguardMessageAreaController).setMessage("") + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) } + @Test - fun onResume_setInitialText() { - mKeyguardPatternViewController.onResume(KeyguardSecurityView.SCREEN_ON) - verify(mKeyguardMessageAreaController) - .setMessageIfEmpty(R.string.keyguard_enter_your_pattern) + fun startAppearAnimation() { + mKeyguardPatternViewController.startAppearAnimation() + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index 97d556b04aa4..ce1101f389c0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -113,11 +113,4 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON); verify(mPasswordEntry).requestFocus(); } - - @Test - public void onResume_setInitialText() { - mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON); - verify(mKeyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin); - } } - diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 9e5bfe53ea05..d9efdeaea04c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -98,6 +98,6 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Test fun startAppearAnimation() { pinViewController.startAppearAnimation() - verify(keyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin) + verify(keyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin) } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index 48e82397e826..b885d546c517 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -146,6 +146,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback; + @Captor + private ArgumentCaptor<KeyguardSecurityContainer.SwipeListener> mSwipeListenerArgumentCaptor; private Configuration mConfiguration; @@ -475,6 +477,64 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { verify(mKeyguardUpdateMonitor, never()).getUserHasTrust(anyInt()); } + @Test + public void onSwipeUp_whenFaceDetectionIsNotRunning_initiatesFaceAuth() { + KeyguardSecurityContainer.SwipeListener registeredSwipeListener = + getRegisteredSwipeListener(); + when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(false); + setupGetSecurityView(); + + registeredSwipeListener.onSwipeUp(); + + verify(mKeyguardUpdateMonitor).requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); + } + + @Test + public void onSwipeUp_whenFaceDetectionIsRunning_doesNotInitiateFaceAuth() { + KeyguardSecurityContainer.SwipeListener registeredSwipeListener = + getRegisteredSwipeListener(); + when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(true); + + registeredSwipeListener.onSwipeUp(); + + verify(mKeyguardUpdateMonitor, never()) + .requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); + } + + @Test + public void onSwipeUp_whenFaceDetectionIsTriggered_hidesBouncerMessage() { + KeyguardSecurityContainer.SwipeListener registeredSwipeListener = + getRegisteredSwipeListener(); + when(mKeyguardUpdateMonitor.requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(true); + setupGetSecurityView(); + + registeredSwipeListener.onSwipeUp(); + + verify(mKeyguardPasswordViewControllerMock).showMessage(null, null); + } + + @Test + public void onSwipeUp_whenFaceDetectionIsNotTriggered_retainsBouncerMessage() { + KeyguardSecurityContainer.SwipeListener registeredSwipeListener = + getRegisteredSwipeListener(); + when(mKeyguardUpdateMonitor.requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(false); + setupGetSecurityView(); + + registeredSwipeListener.onSwipeUp(); + + verify(mKeyguardPasswordViewControllerMock, never()).showMessage(null, null); + } + + private KeyguardSecurityContainer.SwipeListener getRegisteredSwipeListener() { + mKeyguardSecurityContainerController.onViewAttached(); + verify(mView).setSwipeListener(mSwipeListenerArgumentCaptor.capture()); + return mSwipeListenerArgumentCaptor.getValue(); + } + private void setupConditionsToEnableSideFpsHint() { attachView(); setSideFpsHintEnabledFromResources(true); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java index 4dcaa7cf8c09..c94c97c9b638 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java @@ -16,12 +16,16 @@ package com.android.keyguard; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.graphics.Rect; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -108,4 +112,16 @@ public class KeyguardStatusViewControllerTest extends SysuiTestCase { configurationListenerArgumentCaptor.getValue().onLocaleListChanged(); verify(mKeyguardClockSwitchController).onLocaleListChanged(); } + + @Test + public void getClockAnimations_forwardsToClockSwitch() { + ClockAnimations mockClockAnimations = mock(ClockAnimations.class); + when(mKeyguardClockSwitchController.getClockAnimations()).thenReturn(mockClockAnimations); + + Rect r1 = new Rect(1, 2, 3, 4); + Rect r2 = new Rect(5, 6, 7, 8); + mController.getClockAnimations().onPositionUpdated(r1, r2, 0.3f); + + verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.3f); + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 784e7ddb6d06..ebfb4d4d1778 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -25,6 +25,7 @@ import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT; +import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED; import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT; import static com.google.common.truth.Truth.assertThat; @@ -647,6 +648,36 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT); } + @Test + public void requestFaceAuth_whenFaceAuthWasStarted_returnsTrue() throws RemoteException { + // This satisfies all the preconditions to run face auth. + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + bouncerFullyVisibleAndNotGoingToSleep(); + mTestableLooper.processAllMessages(); + + boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true, + NOTIFICATION_PANEL_CLICKED); + + assertThat(didFaceAuthRun).isTrue(); + } + + @Test + public void requestFaceAuth_whenFaceAuthWasNotStarted_returnsFalse() throws RemoteException { + // This ensures face auth won't run. + biometricsDisabledForCurrentUser(); + mTestableLooper.processAllMessages(); + + boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true, + NOTIFICATION_PANEL_CLICKED); + + assertThat(didFaceAuthRun).isFalse(); + } + private void testStrongAuthExceptOnBouncer(int strongAuth) { when(mKeyguardBypassController.canBypass()).thenReturn(true); mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java new file mode 100644 index 000000000000..ae8f419d4e64 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java @@ -0,0 +1,225 @@ +/* + * 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.keyguard; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.AnimatedStateListDrawable; +import android.util.Pair; +import android.view.View; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.AuthRippleController; +import com.android.systemui.doze.util.BurnInHelperKt; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.VibratorHelper; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.After; +import org.junit.Before; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +public class LockIconViewControllerBaseTest extends SysuiTestCase { + protected static final String UNLOCKED_LABEL = "unlocked"; + protected static final int PADDING = 10; + + protected MockitoSession mStaticMockSession; + + protected @Mock LockIconView mLockIconView; + protected @Mock AnimatedStateListDrawable mIconDrawable; + protected @Mock Context mContext; + protected @Mock Resources mResources; + protected @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager; + protected @Mock StatusBarStateController mStatusBarStateController; + protected @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor; + protected @Mock KeyguardViewController mKeyguardViewController; + protected @Mock KeyguardStateController mKeyguardStateController; + protected @Mock FalsingManager mFalsingManager; + protected @Mock AuthController mAuthController; + protected @Mock DumpManager mDumpManager; + protected @Mock AccessibilityManager mAccessibilityManager; + protected @Mock ConfigurationController mConfigurationController; + protected @Mock VibratorHelper mVibrator; + protected @Mock AuthRippleController mAuthRippleController; + protected @Mock FeatureFlags mFeatureFlags; + protected @Mock KeyguardTransitionRepository mTransitionRepository; + protected FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock()); + + protected LockIconViewController mUnderTest; + + // Capture listeners so that they can be used to send events + @Captor protected ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor = + ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); + + @Captor protected ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor = + ArgumentCaptor.forClass(KeyguardStateController.Callback.class); + protected KeyguardStateController.Callback mKeyguardStateCallback; + + @Captor protected ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor = + ArgumentCaptor.forClass(StatusBarStateController.StateListener.class); + protected StatusBarStateController.StateListener mStatusBarStateListener; + + @Captor protected ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor; + protected AuthController.Callback mAuthControllerCallback; + + @Captor protected ArgumentCaptor<KeyguardUpdateMonitorCallback> + mKeyguardUpdateMonitorCallbackCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + protected KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback; + + @Captor protected ArgumentCaptor<Point> mPointCaptor; + + @Before + public void setUp() throws Exception { + mStaticMockSession = mockitoSession() + .mockStatic(BurnInHelperKt.class) + .strictness(Strictness.LENIENT) + .startMocking(); + MockitoAnnotations.initMocks(this); + + setupLockIconViewMocks(); + when(mContext.getResources()).thenReturn(mResources); + when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager); + Rect windowBounds = new Rect(0, 0, 800, 1200); + when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds); + when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL); + when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable); + when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING); + when(mAuthController.getScaleFactor()).thenReturn(1f); + + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); + when(mStatusBarStateController.isDozing()).thenReturn(false); + when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); + + mUnderTest = new LockIconViewController( + mLockIconView, + mStatusBarStateController, + mKeyguardUpdateMonitor, + mKeyguardViewController, + mKeyguardStateController, + mFalsingManager, + mAuthController, + mDumpManager, + mAccessibilityManager, + mConfigurationController, + mDelayableExecutor, + mVibrator, + mAuthRippleController, + mResources, + new KeyguardTransitionInteractor(mTransitionRepository), + new KeyguardInteractor(new FakeKeyguardRepository()), + mFeatureFlags + ); + } + + @After + public void tearDown() { + mStaticMockSession.finishMocking(); + } + + protected Pair<Float, Point> setupUdfps() { + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true); + final Point udfpsLocation = new Point(50, 75); + final float radius = 33f; + when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation); + when(mAuthController.getUdfpsRadius()).thenReturn(radius); + + return new Pair(radius, udfpsLocation); + } + + protected void setupShowLockIcon() { + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); + when(mStatusBarStateController.isDozing()).thenReturn(false); + when(mStatusBarStateController.getDozeAmount()).thenReturn(0f); + when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); + when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false); + } + + protected void captureAuthControllerCallback() { + verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture()); + mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue(); + } + + protected void captureKeyguardStateCallback() { + verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture()); + mKeyguardStateCallback = mKeyguardStateCaptor.getValue(); + } + + protected void captureStatusBarStateListener() { + verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture()); + mStatusBarStateListener = mStatusBarStateCaptor.getValue(); + } + + protected void captureKeyguardUpdateMonitorCallback() { + verify(mKeyguardUpdateMonitor).registerCallback( + mKeyguardUpdateMonitorCallbackCaptor.capture()); + mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue(); + } + + protected void setupLockIconViewMocks() { + when(mLockIconView.getResources()).thenReturn(mResources); + when(mLockIconView.getContext()).thenReturn(mContext); + } + + protected void resetLockIconView() { + reset(mLockIconView); + setupLockIconViewMocks(); + } + + protected void init(boolean useMigrationFlag) { + when(mFeatureFlags.isEnabled(DOZING_MIGRATION_1)).thenReturn(useMigrationFlag); + mUnderTest.init(); + + verify(mLockIconView, atLeast(1)).addOnAttachStateChangeListener(mAttachCaptor.capture()); + mAttachCaptor.getValue().onViewAttachedToWindow(mLockIconView); + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java new file mode 100644 index 000000000000..da40595a4f12 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard; + +import static com.android.keyguard.LockIconView.ICON_LOCK; +import static com.android.keyguard.LockIconView.ICON_UNLOCK; + +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.Point; +import android.hardware.biometrics.BiometricSourceType; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Pair; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.doze.util.BurnInHelperKt; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class LockIconViewControllerTest extends LockIconViewControllerBaseTest { + + @Test + public void testUpdateFingerprintLocationOnInit() { + // GIVEN fp sensor location is available pre-attached + Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location + + // WHEN lock icon view controller is initialized and attached + init(/* useMigrationFlag= */false); + + // THEN lock icon view location is updated to the udfps location with UDFPS radius + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test + public void testUpdatePaddingBasedOnResolutionScale() { + // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5 + Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location + when(mAuthController.getScaleFactor()).thenReturn(5f); + + // WHEN lock icon view controller is initialized and attached + init(/* useMigrationFlag= */false); + + // THEN lock icon view location is updated with the scaled radius + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING * 5)); + } + + @Test + public void testUpdateLockIconLocationOnAuthenticatorsRegistered() { + // GIVEN fp sensor location is not available pre-init + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); + init(/* useMigrationFlag= */false); + resetLockIconView(); // reset any method call counts for when we verify method calls later + + // GIVEN fp sensor location is available post-attached + captureAuthControllerCallback(); + Pair<Float, Point> udfps = setupUdfps(); + + // WHEN all authenticators are registered + mAuthControllerCallback.onAllAuthenticatorsRegistered(); + mDelayableExecutor.runAllReady(); + + // THEN lock icon view location is updated with the same coordinates as auth controller vals + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test + public void testUpdateLockIconLocationOnUdfpsLocationChanged() { + // GIVEN fp sensor location is not available pre-init + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); + init(/* useMigrationFlag= */false); + resetLockIconView(); // reset any method call counts for when we verify method calls later + + // GIVEN fp sensor location is available post-attached + captureAuthControllerCallback(); + Pair<Float, Point> udfps = setupUdfps(); + + // WHEN udfps location changes + mAuthControllerCallback.onUdfpsLocationChanged(); + mDelayableExecutor.runAllReady(); + + // THEN lock icon view location is updated with the same coordinates as auth controller vals + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test + public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() { + // GIVEN Udpfs sensor location is available + setupUdfps(); + + // WHEN the view is attached + init(/* useMigrationFlag= */false); + + // THEN the lock icon view background should be enabled + verify(mLockIconView).setUseBackground(true); + } + + @Test + public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() { + // GIVEN Udfps sensor location is not supported + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + + // WHEN the view is attached + init(/* useMigrationFlag= */false); + + // THEN the lock icon view background should be disabled + verify(mLockIconView).setUseBackground(false); + } + + @Test + public void testUnlockIconShows_biometricUnlockedTrue() { + // GIVEN UDFPS sensor location is available + setupUdfps(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureKeyguardUpdateMonitorCallback(); + + // GIVEN user has unlocked with a biometric auth (ie: face auth) + when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true); + reset(mLockIconView); + + // WHEN face auth's biometric running state changes + mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false, + BiometricSourceType.FACE); + + // THEN the unlock icon is shown + verify(mLockIconView).setContentDescription(UNLOCKED_LABEL); + } + + @Test + public void testLockIconStartState() { + // GIVEN lock icon state + setupShowLockIcon(); + + // WHEN lock icon controller is initialized + init(/* useMigrationFlag= */false); + + // THEN the lock icon should show + verify(mLockIconView).updateIcon(ICON_LOCK, false); + } + + @Test + public void testLockIcon_updateToUnlock() { + // GIVEN starting state for the lock icon + setupShowLockIcon(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureKeyguardStateCallback(); + reset(mLockIconView); + + // WHEN the unlocked state changes to canDismissLockScreen=true + when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); + mKeyguardStateCallback.onUnlockedChanged(); + + // THEN the unlock should show + verify(mLockIconView).updateIcon(ICON_UNLOCK, false); + } + + @Test + public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() { + // GIVEN udfps not enrolled + setupUdfps(); + when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false); + + // GIVEN starting state for the lock icon + setupShowLockIcon(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureStatusBarStateListener(); + reset(mLockIconView); + + // WHEN the dozing state changes + mStatusBarStateListener.onDozingChanged(true /* isDozing */); + + // THEN the icon is cleared + verify(mLockIconView).clearIcon(); + } + + @Test + public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() { + // GIVEN udfps enrolled + setupUdfps(); + when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); + + // GIVEN starting state for the lock icon + setupShowLockIcon(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureStatusBarStateListener(); + reset(mLockIconView); + + // WHEN the dozing state changes + mStatusBarStateListener.onDozingChanged(true /* isDozing */); + + // THEN the AOD lock icon should show + verify(mLockIconView).updateIcon(ICON_LOCK, true); + } + + @Test + public void testBurnInOffsetsUpdated_onDozeAmountChanged() { + // GIVEN udfps enrolled + setupUdfps(); + when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); + + // GIVEN burn-in offset = 5 + int burnInOffset = 5; + when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset); + + // GIVEN starting state for the lock icon (keyguard) + setupShowLockIcon(); + init(/* useMigrationFlag= */false); + captureStatusBarStateListener(); + reset(mLockIconView); + + // WHEN dozing updates + mStatusBarStateListener.onDozingChanged(true /* isDozing */); + mStatusBarStateListener.onDozeAmountChanged(1f, 1f); + + // THEN the view's translation is updated to use the AoD burn-in offsets + verify(mLockIconView).setTranslationY(burnInOffset); + verify(mLockIconView).setTranslationX(burnInOffset); + reset(mLockIconView); + + // WHEN the device is no longer dozing + mStatusBarStateListener.onDozingChanged(false /* isDozing */); + mStatusBarStateListener.onDozeAmountChanged(0f, 0f); + + // THEN the view is updated to NO translation (no burn-in offsets anymore) + verify(mLockIconView).setTranslationY(0); + verify(mLockIconView).setTranslationX(0); + + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt new file mode 100644 index 000000000000..d2c54b4cc0e7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt @@ -0,0 +1,123 @@ +/* + * 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.keyguard + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.keyguard.LockIconView.ICON_LOCK +import com.android.systemui.doze.util.getBurnInOffset +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class LockIconViewControllerWithCoroutinesTest : LockIconViewControllerBaseTest() { + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() = + runBlocking(IMMEDIATE) { + // GIVEN udfps not enrolled + setupUdfps() + whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false) + + // GIVEN starting state for the lock icon + setupShowLockIcon() + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */ true) + reset(mLockIconView) + + // WHEN the dozing state changes + mUnderTest.mIsDozingCallback.accept(true) + + // THEN the icon is cleared + verify(mLockIconView).clearIcon() + } + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testLockIcon_updateToAodLock_whenUdfpsEnrolled() = + runBlocking(IMMEDIATE) { + // GIVEN udfps enrolled + setupUdfps() + whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true) + + // GIVEN starting state for the lock icon + setupShowLockIcon() + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */ true) + reset(mLockIconView) + + // WHEN the dozing state changes + mUnderTest.mIsDozingCallback.accept(true) + + // THEN the AOD lock icon should show + verify(mLockIconView).updateIcon(ICON_LOCK, true) + } + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testBurnInOffsetsUpdated_onDozeAmountChanged() = + runBlocking(IMMEDIATE) { + // GIVEN udfps enrolled + setupUdfps() + whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true) + + // GIVEN burn-in offset = 5 + val burnInOffset = 5 + whenever(getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset) + + // GIVEN starting state for the lock icon (keyguard) + setupShowLockIcon() + init(/* useMigrationFlag= */ true) + reset(mLockIconView) + + // WHEN dozing updates + mUnderTest.mIsDozingCallback.accept(true) + mUnderTest.mDozeTransitionCallback.accept(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED)) + + // THEN the view's translation is updated to use the AoD burn-in offsets + verify(mLockIconView).setTranslationY(burnInOffset.toFloat()) + verify(mLockIconView).setTranslationX(burnInOffset.toFloat()) + reset(mLockIconView) + + // WHEN the device is no longer dozing + mUnderTest.mIsDozingCallback.accept(false) + mUnderTest.mDozeTransitionCallback.accept(TransitionStep(AOD, LOCKSCREEN, 0f, FINISHED)) + + // THEN the view is updated to NO translation (no burn-in offsets anymore) + verify(mLockIconView).setTranslationY(0f) + verify(mLockIconView).setTranslationX(0f) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 2319f4386798..181839ab512f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -36,6 +36,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -255,6 +256,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { }); mScreenDecorations.mDisplayInfo = mDisplayInfo; doReturn(1f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio(); + doNothing().when(mScreenDecorations).updateOverlayProviderViews(any()); reset(mTunerService); try { @@ -1005,18 +1007,13 @@ public class ScreenDecorationsTest extends SysuiTestCase { assertEquals(new Size(3, 3), resDelegate.getTopRoundedSize()); assertEquals(new Size(4, 4), resDelegate.getBottomRoundedSize()); - setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, - getTestsDrawable(com.android.systemui.tests.R.drawable.rounded4px) - /* roundedTopDrawable */, - getTestsDrawable(com.android.systemui.tests.R.drawable.rounded5px) - /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* privacyDot */, false /* faceScanning*/); + doReturn(2f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio(); mDisplayInfo.rotation = Surface.ROTATION_270; mScreenDecorations.onConfigurationChanged(null); - assertEquals(new Size(4, 4), resDelegate.getTopRoundedSize()); - assertEquals(new Size(5, 5), resDelegate.getBottomRoundedSize()); + assertEquals(new Size(6, 6), resDelegate.getTopRoundedSize()); + assertEquals(new Size(8, 8), resDelegate.getBottomRoundedSize()); } @Test @@ -1293,51 +1290,6 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Test - public void testOnDisplayChanged_hwcLayer() { - setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, - null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */); - final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport(); - decorationSupport.format = PixelFormat.R_8; - doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport(); - - // top cutout - mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP)); - - mScreenDecorations.start(); - - final ScreenDecorHwcLayer hwcLayer = mScreenDecorations.mScreenDecorHwcLayer; - spyOn(hwcLayer); - doReturn(mDisplay).when(hwcLayer).getDisplay(); - - mScreenDecorations.mDisplayListener.onDisplayChanged(1); - - verify(hwcLayer, times(1)).onDisplayChanged(any()); - } - - @Test - public void testOnDisplayChanged_nonHwcLayer() { - setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, - null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */); - - // top cutout - mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP)); - - mScreenDecorations.start(); - - final ScreenDecorations.DisplayCutoutView cutoutView = (ScreenDecorations.DisplayCutoutView) - mScreenDecorations.getOverlayView(R.id.display_cutout); - assertNotNull(cutoutView); - spyOn(cutoutView); - doReturn(mDisplay).when(cutoutView).getDisplay(); - - mScreenDecorations.mDisplayListener.onDisplayChanged(1); - - verify(cutoutView, times(1)).onDisplayChanged(any()); - } - - @Test public void testHasSameProvidersWithNullOverlays() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index d52612b000bc..e8c760c3e140 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -52,6 +52,7 @@ import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyLong import org.mockito.Mockito.eq import org.mockito.Mockito.never +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit @@ -123,6 +124,21 @@ class AuthContainerViewTest : SysuiTestCase() { } @Test + fun testDismissBeforeIntroEnd() { + val container = initializeFingerprintContainer() + waitForIdleSync() + + // STATE_ANIMATING_IN = 1 + container?.mContainerState = 1 + + container.dismissWithoutCallback(false) + + // the first time is triggered by initializeFingerprintContainer() + // the second time was triggered by dismissWithoutCallback() + verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L) + } + + @Test fun testDismissesOnFocusLoss() { val container = initializeFingerprintContainer() waitForIdleSync() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index baeabc577fb7..c85334db9499 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -26,6 +26,7 @@ import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_ import android.hardware.biometrics.BiometricOverlayConstants.ShowReason import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.IUdfpsOverlayControllerCallback +import android.provider.Settings import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.LayoutInflater @@ -124,14 +125,18 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { whenever(udfpsEnrollView.context).thenReturn(context) } - private fun withReason(@ShowReason reason: Int, block: () -> Unit) { + private fun withReason( + @ShowReason reason: Int, + isDebuggable: Boolean = false, + block: () -> Unit + ) { controllerOverlay = UdfpsControllerOverlay( context, fingerprintManager, inflater, windowManager, accessibilityManager, statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager, keyguardUpdateMonitor, dialogManager, dumpManager, transitionController, configurationController, systemClock, keyguardStateController, unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason, - controllerCallback, onTouch, activityLaunchAnimator + controllerCallback, onTouch, activityLaunchAnimator, isDebuggable ) block() } @@ -151,11 +156,29 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { } @Test + fun showUdfpsOverlay_locate_withEnrollmentUiRemoved() { + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1) + withReason(REASON_ENROLL_FIND_SENSOR, isDebuggable = true) { + showUdfpsOverlay(isEnrollUseCase = false) + } + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0) + } + + @Test fun showUdfpsOverlay_enroll() = withReason(REASON_ENROLL_ENROLLING) { showUdfpsOverlay(isEnrollUseCase = true) } @Test + fun showUdfpsOverlay_enroll_withEnrollmentUiRemoved() { + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1) + withReason(REASON_ENROLL_ENROLLING, isDebuggable = true) { + showUdfpsOverlay(isEnrollUseCase = false) + } + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0) + } + + @Test fun showUdfpsOverlay_other() = withReason(REASON_AUTH_OTHER) { showUdfpsOverlay() } private fun withRotation(@Rotation rotation: Int, block: () -> Unit) { @@ -372,21 +395,33 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) val rotation = Surface.ROTATION_0 // touch at 0 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[0]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[0]) // touch at 90 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[1]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, -1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[1]) // touch at 180 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[2]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + -1.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[2]) // touch at 270 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[3]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[3]) } fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) { @@ -394,21 +429,33 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) val rotation = Surface.ROTATION_90 // touch at 0 degrees -> 90 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[1]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[1]) // touch at 90 degrees -> 180 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[2]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, -1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[2]) // touch at 180 degrees -> 270 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[3]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + -1.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[3]) // touch at 270 degrees -> 0 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[0]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[0]) } fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) { @@ -416,21 +463,33 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) val rotation = Surface.ROTATION_270 // touch at 0 degrees -> 270 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[3]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[3]) // touch at 90 degrees -> 0 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[0]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, -1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[0]) // touch at 180 degrees -> 90 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[1]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + -1.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[1]) // touch at 270 degrees -> 180 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[2]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[2]) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 8923ba817568..28e13b8e81ab 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -169,6 +169,8 @@ public class UdfpsControllerTest extends SysuiTestCase { @Mock private LatencyTracker mLatencyTracker; private FakeExecutor mFgExecutor; + @Mock + private UdfpsDisplayMode mUdfpsDisplayMode; // Stuff for configuring mocks @Mock @@ -258,7 +260,6 @@ public class UdfpsControllerTest extends SysuiTestCase { mVibrator, mUdfpsHapticsSimulator, mUdfpsShell, - Optional.of(mDisplayModeProvider), mKeyguardStateController, mDisplayManager, mHandler, @@ -275,6 +276,7 @@ public class UdfpsControllerTest extends SysuiTestCase { verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); mScreenObserver = mScreenObserverCaptor.getValue(); mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, new UdfpsOverlayParams()); + mUdfpsController.setUdfpsDisplayMode(mUdfpsDisplayMode); } @Test @@ -659,7 +661,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); when(mUdfpsView.isDisplayConfigured()).thenReturn(true); // WHEN it is cancelled - mUdfpsController.onCancelUdfps(); + mUdfpsController.cancelAodInterrupt(); // THEN the display is unconfigured verify(mUdfpsView).unconfigureDisplay(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java new file mode 100644 index 000000000000..7e35b261b352 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java @@ -0,0 +1,132 @@ +/* + * 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.biometrics; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.hardware.fingerprint.IUdfpsHbmListener; +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper.RunWithLooper; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.util.concurrency.FakeExecution; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@RunWithLooper(setAsMainLooper = true) +public class UdfpsDisplayModeTest extends SysuiTestCase { + private static final int DISPLAY_ID = 0; + + @Mock + private AuthController mAuthController; + @Mock + private IUdfpsHbmListener mDisplayCallback; + @Mock + private Runnable mOnEnabled; + @Mock + private Runnable mOnDisabled; + + private final FakeExecution mExecution = new FakeExecution(); + private UdfpsDisplayMode mHbmController; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + // Force mContext to always return DISPLAY_ID + Context contextSpy = spy(mContext); + when(contextSpy.getDisplayId()).thenReturn(DISPLAY_ID); + + // Set up mocks. + when(mAuthController.getUdfpsHbmListener()).thenReturn(mDisplayCallback); + + // Create a real controller with mock dependencies. + mHbmController = new UdfpsDisplayMode(contextSpy, mExecution, mAuthController); + } + + @Test + public void roundTrip() throws RemoteException { + // Enable the UDFPS mode. + mHbmController.enable(mOnEnabled); + + // Should set the appropriate refresh rate for UDFPS and notify the caller. + verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID)); + verify(mOnEnabled).run(); + + // Disable the UDFPS mode. + mHbmController.disable(mOnDisabled); + + // Should unset the refresh rate and notify the caller. + verify(mOnDisabled).run(); + verify(mDisplayCallback).onHbmDisabled(eq(DISPLAY_ID)); + } + + @Test + public void mustNotEnableMoreThanOnce() throws RemoteException { + // First request to enable the UDFPS mode. + mHbmController.enable(mOnEnabled); + + // Should set the appropriate refresh rate for UDFPS and notify the caller. + verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID)); + verify(mOnEnabled).run(); + + // Second request to enable the UDFPS mode, while it's still enabled. + mHbmController.enable(mOnEnabled); + + // Should ignore the second request. + verifyNoMoreInteractions(mDisplayCallback); + verifyNoMoreInteractions(mOnEnabled); + } + + @Test + public void mustNotDisableMoreThanOnce() throws RemoteException { + // Disable the UDFPS mode. + mHbmController.enable(mOnEnabled); + + // Should set the appropriate refresh rate for UDFPS and notify the caller. + verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID)); + verify(mOnEnabled).run(); + + // First request to disable the UDFPS mode. + mHbmController.disable(mOnDisabled); + + // Should unset the refresh rate and notify the caller. + verify(mOnDisabled).run(); + verify(mDisplayCallback).onHbmDisabled(eq(DISPLAY_ID)); + + // Second request to disable the UDFPS mode, when it's already disabled. + mHbmController.disable(mOnDisabled); + + // Should ignore the second request. + verifyNoMoreInteractions(mOnDisabled); + verifyNoMoreInteractions(mDisplayCallback); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java index 3e9cf1e51b63..fa9c41a3cbb6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java @@ -35,6 +35,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerFake; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.policy.BatteryController; @@ -71,6 +72,8 @@ public class FalsingCollectorImplTest extends SysuiTestCase { @Mock private KeyguardStateController mKeyguardStateController; @Mock + private ShadeExpansionStateManager mShadeExpansionStateManager; + @Mock private BatteryController mBatteryController; private final DockManagerFake mDockManager = new DockManagerFake(); private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); @@ -85,7 +88,8 @@ public class FalsingCollectorImplTest extends SysuiTestCase { mFalsingCollector = new FalsingCollectorImpl(mFalsingDataProvider, mFalsingManager, mKeyguardUpdateMonitor, mHistoryTracker, mProximitySensor, - mStatusBarStateController, mKeyguardStateController, mBatteryController, + mStatusBarStateController, mKeyguardStateController, mShadeExpansionStateManager, + mBatteryController, mDockManager, mFakeExecutor, mFakeSystemClock); } @@ -137,9 +141,9 @@ public class FalsingCollectorImplTest extends SysuiTestCase { public void testUnregisterSensor_QS() { mFalsingCollector.onScreenTurningOn(); reset(mProximitySensor); - mFalsingCollector.setQsExpanded(true); + mFalsingCollector.onQsExpansionChanged(true); verify(mProximitySensor).unregister(any(ThresholdSensor.Listener.class)); - mFalsingCollector.setQsExpanded(false); + mFalsingCollector.onQsExpansionChanged(false); verify(mProximitySensor).register(any(ThresholdSensor.Listener.class)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java index 91214a85ddd5..e7e6918325a7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java @@ -38,6 +38,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.util.DeviceConfigProxyFake; import org.junit.Before; @@ -47,6 +49,9 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +import javax.inject.Provider; @SmallTest @RunWith(AndroidJUnit4.class) @@ -55,11 +60,15 @@ public class ClipboardListenerTest extends SysuiTestCase { @Mock private ClipboardManager mClipboardManager; @Mock - private ClipboardOverlayControllerFactory mClipboardOverlayControllerFactory; + private ClipboardOverlayControllerLegacyFactory mClipboardOverlayControllerLegacyFactory; + @Mock + private ClipboardOverlayControllerLegacy mOverlayControllerLegacy; @Mock private ClipboardOverlayController mOverlayController; @Mock private UiEventLogger mUiEventLogger; + @Mock + private FeatureFlags mFeatureFlags; private DeviceConfigProxyFake mDeviceConfigProxy; private ClipData mSampleClipData; @@ -72,12 +81,17 @@ public class ClipboardListenerTest extends SysuiTestCase { @Captor private ArgumentCaptor<String> mStringCaptor; + @Spy + private Provider<ClipboardOverlayController> mOverlayControllerProvider; + @Before public void setup() { + mOverlayControllerProvider = () -> mOverlayController; + MockitoAnnotations.initMocks(this); - when(mClipboardOverlayControllerFactory.create(any())).thenReturn( - mOverlayController); + when(mClipboardOverlayControllerLegacyFactory.create(any())) + .thenReturn(mOverlayControllerLegacy); when(mClipboardManager.hasPrimaryClip()).thenReturn(true); @@ -94,7 +108,8 @@ public class ClipboardListenerTest extends SysuiTestCase { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "false", false); ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); listener.start(); verifyZeroInteractions(mClipboardManager); verifyZeroInteractions(mUiEventLogger); @@ -105,7 +120,8 @@ public class ClipboardListenerTest extends SysuiTestCase { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); listener.start(); verify(mClipboardManager).addPrimaryClipChangedListener(any()); verifyZeroInteractions(mUiEventLogger); @@ -113,16 +129,58 @@ public class ClipboardListenerTest extends SysuiTestCase { @Test public void test_consecutiveCopies() { + when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false); + mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); listener.start(); listener.onPrimaryClipChanged(); - verify(mClipboardOverlayControllerFactory).create(any()); + verify(mClipboardOverlayControllerLegacyFactory).create(any()); - verify(mOverlayController).setClipData(mClipDataCaptor.capture(), mStringCaptor.capture()); + verify(mOverlayControllerLegacy).setClipData( + mClipDataCaptor.capture(), mStringCaptor.capture()); + + assertEquals(mSampleClipData, mClipDataCaptor.getValue()); + assertEquals(mSampleSource, mStringCaptor.getValue()); + + verify(mOverlayControllerLegacy).setOnSessionCompleteListener(mRunnableCaptor.capture()); + + // Should clear the overlay controller + mRunnableCaptor.getValue().run(); + + listener.onPrimaryClipChanged(); + + verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any()); + + // Not calling the runnable here, just change the clip again and verify that the overlay is + // NOT recreated. + + listener.onPrimaryClipChanged(); + + verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any()); + verifyZeroInteractions(mOverlayControllerProvider); + } + + @Test + public void test_consecutiveCopies_new() { + when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true); + + mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, + "true", false); + ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); + listener.start(); + listener.onPrimaryClipChanged(); + + verify(mOverlayControllerProvider).get(); + + verify(mOverlayController).setClipData( + mClipDataCaptor.capture(), mStringCaptor.capture()); assertEquals(mSampleClipData, mClipDataCaptor.getValue()); assertEquals(mSampleSource, mStringCaptor.getValue()); @@ -134,14 +192,15 @@ public class ClipboardListenerTest extends SysuiTestCase { listener.onPrimaryClipChanged(); - verify(mClipboardOverlayControllerFactory, times(2)).create(any()); + verify(mOverlayControllerProvider, times(2)).get(); // Not calling the runnable here, just change the clip again and verify that the overlay is // NOT recreated. listener.onPrimaryClipChanged(); - verify(mClipboardOverlayControllerFactory, times(2)).create(any()); + verify(mOverlayControllerProvider, times(2)).get(); + verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory); } @Test @@ -169,4 +228,40 @@ public class ClipboardListenerTest extends SysuiTestCase { assertTrue(ClipboardListener.shouldSuppressOverlay(suppressableClipData, ClipboardListener.SHELL_PACKAGE, false)); } + + @Test + public void test_logging_enterAndReenter() { + when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false); + + ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); + listener.start(); + + listener.onPrimaryClipChanged(); + listener.onPrimaryClipChanged(); + + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource); + } + + @Test + public void test_logging_enterAndReenter_new() { + when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true); + + ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); + listener.start(); + + listener.onPrimaryClipChanged(); + listener.onPrimaryClipChanged(); + + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java new file mode 100644 index 000000000000..b7f1c1a9f001 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java @@ -0,0 +1,189 @@ +/* + * 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.clipboardoverlay; + +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.animation.Animator; +import android.content.ClipData; +import android.content.ClipDescription; +import android.net.Uri; +import android.os.PersistableBundle; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.screenshot.TimeoutHandler; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ClipboardOverlayControllerTest extends SysuiTestCase { + + private ClipboardOverlayController mOverlayController; + @Mock + private ClipboardOverlayView mClipboardOverlayView; + @Mock + private ClipboardOverlayWindow mClipboardOverlayWindow; + @Mock + private BroadcastSender mBroadcastSender; + @Mock + private TimeoutHandler mTimeoutHandler; + @Mock + private UiEventLogger mUiEventLogger; + + @Mock + private Animator mAnimator; + + private ClipData mSampleClipData; + + @Captor + private ArgumentCaptor<ClipboardOverlayView.ClipboardOverlayCallbacks> mOverlayCallbacksCaptor; + private ClipboardOverlayView.ClipboardOverlayCallbacks mCallbacks; + + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator); + when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator); + + mSampleClipData = new ClipData("Test", new String[]{"text/plain"}, + new ClipData.Item("Test Item")); + + mOverlayController = new ClipboardOverlayController( + mContext, + mClipboardOverlayView, + mClipboardOverlayWindow, + getFakeBroadcastDispatcher(), + mBroadcastSender, + mTimeoutHandler, + mUiEventLogger); + verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture()); + mCallbacks = mOverlayCallbacksCaptor.getValue(); + } + + @After + public void tearDown() { + mOverlayController.hideImmediate(); + } + + @Test + public void test_setClipData_nullData() { + ClipData clipData = null; + mOverlayController.setClipData(clipData, ""); + + verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); + verify(mClipboardOverlayView, times(0)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_invalidImageData() { + ClipData clipData = new ClipData("", new String[]{"image/png"}, + new ClipData.Item(Uri.parse(""))); + + mOverlayController.setClipData(clipData, ""); + + verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); + verify(mClipboardOverlayView, times(0)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_textData() { + mOverlayController.setClipData(mSampleClipData, ""); + + verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false); + verify(mClipboardOverlayView, times(1)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_sensitiveTextData() { + ClipDescription description = mSampleClipData.getDescription(); + PersistableBundle b = new PersistableBundle(); + b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true); + description.setExtras(b); + ClipData data = new ClipData(description, mSampleClipData.getItemAt(0)); + mOverlayController.setClipData(data, ""); + + verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true); + verify(mClipboardOverlayView, times(1)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_repeatedCalls() { + when(mAnimator.isRunning()).thenReturn(true); + + mOverlayController.setClipData(mSampleClipData, ""); + mOverlayController.setClipData(mSampleClipData, ""); + + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_viewCallbacks_onShareTapped() { + mOverlayController.setClipData(mSampleClipData, ""); + + mCallbacks.onShareButtonTapped(); + + verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED); + verify(mClipboardOverlayView, times(1)).getExitAnimation(); + } + + @Test + public void test_viewCallbacks_onDismissTapped() { + mOverlayController.setClipData(mSampleClipData, ""); + + mCallbacks.onDismissButtonTapped(); + + verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED); + verify(mClipboardOverlayView, times(1)).getExitAnimation(); + } + + @Test + public void test_multipleDismissals_dismissesOnce() { + mCallbacks.onSwipeDismissInitiated(mAnimator); + mCallbacks.onDismissButtonTapped(); + mCallbacks.onSwipeDismissInitiated(mAnimator); + mCallbacks.onDismissButtonTapped(); + + verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SWIPE_DISMISSED); + verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java deleted file mode 100644 index c7c2cd8d7b4b..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.clipboardoverlay; - -import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.provider.DeviceConfig; - -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.internal.logging.UiEventLogger; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.util.DeviceConfigProxyFake; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class ClipboardOverlayEventTest extends SysuiTestCase { - - @Mock - private ClipboardManager mClipboardManager; - @Mock - private ClipboardOverlayControllerFactory mClipboardOverlayControllerFactory; - @Mock - private ClipboardOverlayController mOverlayController; - @Mock - private UiEventLogger mUiEventLogger; - - private final String mSampleSource = "Example source"; - - private ClipboardListener mClipboardListener; - - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - when(mClipboardOverlayControllerFactory.create(any())).thenReturn( - mOverlayController); - when(mClipboardManager.hasPrimaryClip()).thenReturn(true); - - ClipData sampleClipData = new ClipData("Test", new String[]{"text/plain"}, - new ClipData.Item("Test Item")); - when(mClipboardManager.getPrimaryClip()).thenReturn(sampleClipData); - when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource); - - DeviceConfigProxyFake deviceConfigProxy = new DeviceConfigProxyFake(); - deviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, - "true", false); - - mClipboardListener = new ClipboardListener(getContext(), deviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); - } - - @Test - public void test_enterAndReenter() { - mClipboardListener.start(); - - mClipboardListener.onPrimaryClipChanged(); - mClipboardListener.onPrimaryClipChanged(); - - verify(mUiEventLogger, times(1)).log( - ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); - verify(mUiEventLogger, times(1)).log( - ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt index f93336134900..93a1868b72f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt @@ -24,12 +24,11 @@ import androidx.annotation.DrawableRes import androidx.test.filters.SmallTest import com.android.internal.R as InternalR import com.android.systemui.R as SystemUIR -import com.android.systemui.tests.R import com.android.systemui.SysuiTestCase +import com.android.systemui.tests.R import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test - import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations @@ -102,14 +101,11 @@ class RoundedCornerResDelegateTest : SysuiTestCase() { assertEquals(Size(3, 3), roundedCornerResDelegate.topRoundedSize) assertEquals(Size(4, 4), roundedCornerResDelegate.bottomRoundedSize) - setupResources(radius = 100, - roundedTopDrawable = getTestsDrawable(R.drawable.rounded4px), - roundedBottomDrawable = getTestsDrawable(R.drawable.rounded5px)) - + roundedCornerResDelegate.physicalPixelDisplaySizeRatio = 2f roundedCornerResDelegate.updateDisplayUniqueId(null, 1) - assertEquals(Size(4, 4), roundedCornerResDelegate.topRoundedSize) - assertEquals(Size(5, 5), roundedCornerResDelegate.bottomRoundedSize) + assertEquals(Size(6, 6), roundedCornerResDelegate.topRoundedSize) + assertEquals(Size(8, 8), roundedCornerResDelegate.bottomRoundedSize) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java index 6a55a60c2fda..5bbd8109d8f9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java @@ -16,6 +16,9 @@ package com.android.systemui.doze; +import static android.content.res.Configuration.UI_MODE_NIGHT_YES; +import static android.content.res.Configuration.UI_MODE_TYPE_CAR; + import static com.android.systemui.doze.DozeMachine.State.DOZE; import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD; import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_DOCKED; @@ -38,16 +41,17 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.UiModeManager; import android.content.res.Configuration; import android.hardware.display.AmbientDisplayConfiguration; import android.testing.AndroidTestingRunner; import android.testing.UiThreadTest; import android.view.Display; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; @@ -78,25 +82,30 @@ public class DozeMachineTest extends SysuiTestCase { @Mock private DozeHost mHost; @Mock - private UiModeManager mUiModeManager; + private DozeMachine.Part mPartMock; + @Mock + private DozeMachine.Part mAnotherPartMock; private DozeServiceFake mServiceFake; private WakeLockFake mWakeLockFake; - private AmbientDisplayConfiguration mConfigMock; - private DozeMachine.Part mPartMock; + private AmbientDisplayConfiguration mAmbientDisplayConfigMock; @Before public void setUp() { MockitoAnnotations.initMocks(this); mServiceFake = new DozeServiceFake(); mWakeLockFake = new WakeLockFake(); - mConfigMock = mock(AmbientDisplayConfiguration.class); - mPartMock = mock(DozeMachine.Part.class); + mAmbientDisplayConfigMock = mock(AmbientDisplayConfiguration.class); when(mDockManager.isDocked()).thenReturn(false); when(mDockManager.isHidden()).thenReturn(false); - mMachine = new DozeMachine(mServiceFake, mConfigMock, mWakeLockFake, - mWakefulnessLifecycle, mUiModeManager, mDozeLog, mDockManager, - mHost, new DozeMachine.Part[]{mPartMock}); + mMachine = new DozeMachine(mServiceFake, + mAmbientDisplayConfigMock, + mWakeLockFake, + mWakefulnessLifecycle, + mDozeLog, + mDockManager, + mHost, + new DozeMachine.Part[]{mPartMock, mAnotherPartMock}); } @Test @@ -108,7 +117,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_goesToDoze() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); mMachine.requestState(INITIALIZED); @@ -118,7 +127,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_goesToAod() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); mMachine.requestState(INITIALIZED); @@ -138,7 +147,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_afterDockPaused_goesToDoze() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); @@ -151,7 +160,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); mMachine.requestState(INITIALIZED); @@ -162,7 +171,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); mMachine.requestState(INITIALIZED); @@ -184,7 +193,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_afterDockPaused_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); @@ -197,7 +206,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_afterDockPaused_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); @@ -209,7 +218,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_goesToDoze() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); mMachine.requestState(INITIALIZED); mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION); mMachine.requestState(DOZE_PULSING); @@ -222,7 +231,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_goesToAoD() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); mMachine.requestState(INITIALIZED); mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION); mMachine.requestState(DOZE_PULSING); @@ -236,7 +245,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_alwaysOnSuppressed_goesToSuppressed() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); mMachine.requestState(INITIALIZED); mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION); mMachine.requestState(DOZE_PULSING); @@ -287,7 +296,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_afterDockPaused_goesToDoze() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); mMachine.requestState(INITIALIZED); @@ -303,7 +312,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_alwaysOnSuppressed_afterDockPaused_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); mMachine.requestState(INITIALIZED); @@ -471,7 +480,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testTransitionToInitialized_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); verify(mPartMock).transitionTo(UNINITIALIZED, INITIALIZED); @@ -481,7 +492,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testTransitionToFinish_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(FINISH); @@ -490,7 +503,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testDozeToDozeSuspendTriggers_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(DOZE); @@ -499,7 +514,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testDozeAoDToDozeSuspendTriggers_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(DOZE_AOD); @@ -508,7 +525,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testDozePulsingBrightDozeSuspendTriggers_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(DOZE_PULSING_BRIGHT); @@ -517,7 +536,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testDozeAodDockedDozeSuspendTriggers_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(DOZE_AOD_DOCKED); @@ -525,7 +546,35 @@ public class DozeMachineTest extends SysuiTestCase { } @Test + public void testOnConfigurationChanged_propagatesUiModeTypeToParts() { + Configuration newConfig = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(newConfig); + + verify(mPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR); + verify(mAnotherPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR); + } + + @Test + public void testOnConfigurationChanged_propagatesOnlyUiModeChangesToParts() { + Configuration newConfig = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(newConfig); + mMachine.onConfigurationChanged(newConfig); + + verify(mPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR); + verify(mAnotherPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR); + } + + @Test public void testDozeSuppressTriggers_screenState() { assertEquals(Display.STATE_OFF, DOZE_SUSPEND_TRIGGERS.screenState(null)); } + + @NonNull + private Configuration configWithCarNightUiMode() { + Configuration configuration = Configuration.EMPTY; + configuration.uiMode = UI_MODE_TYPE_CAR | UI_MODE_NIGHT_YES; + return configuration; + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java index 9ffc5a57cef6..c40c187d572a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java @@ -423,7 +423,7 @@ public class DozeSensorsTest extends SysuiTestCase { @Test public void testGesturesAllInitiallyRespectSettings() { - DozeSensors dozeSensors = new DozeSensors(getContext(), mSensorManager, mDozeParameters, + DozeSensors dozeSensors = new DozeSensors(mSensorManager, mDozeParameters, mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog, mProximitySensor, mFakeSettings, mAuthController, mDevicePostureController); @@ -435,7 +435,7 @@ public class DozeSensorsTest extends SysuiTestCase { private class TestableDozeSensors extends DozeSensors { TestableDozeSensors() { - super(getContext(), mSensorManager, mDozeParameters, + super(mSensorManager, mDozeParameters, mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog, mProximitySensor, mFakeSettings, mAuthController, mDevicePostureController); diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java index 0f29dcd5a939..32b994538e12 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java @@ -10,14 +10,14 @@ * 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 andatest + * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.doze; -import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE; -import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE; +import static android.content.res.Configuration.UI_MODE_TYPE_CAR; +import static android.content.res.Configuration.UI_MODE_TYPE_NORMAL; import static com.android.systemui.doze.DozeMachine.State.DOZE; import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD; @@ -26,17 +26,16 @@ import static com.android.systemui.doze.DozeMachine.State.FINISH; import static com.android.systemui.doze.DozeMachine.State.INITIALIZED; import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.UiModeManager; -import android.content.BroadcastReceiver; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; import android.hardware.display.AmbientDisplayConfiguration; import android.testing.AndroidTestingRunner; import android.testing.UiThreadTest; @@ -44,13 +43,13 @@ import android.testing.UiThreadTest; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.statusbar.phone.BiometricUnlockController; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.AdditionalMatchers; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -71,10 +70,6 @@ public class DozeSuppressorTest extends SysuiTestCase { @Mock private AmbientDisplayConfiguration mConfig; @Mock - private BroadcastDispatcher mBroadcastDispatcher; - @Mock - private UiModeManager mUiModeManager; - @Mock private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy; @Mock private BiometricUnlockController mBiometricUnlockController; @@ -83,13 +78,6 @@ public class DozeSuppressorTest extends SysuiTestCase { private DozeMachine mDozeMachine; @Captor - private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor; - @Captor - private ArgumentCaptor<IntentFilter> mIntentFilterCaptor; - private BroadcastReceiver mBroadcastReceiver; - private IntentFilter mIntentFilter; - - @Captor private ArgumentCaptor<DozeHost.Callback> mDozeHostCaptor; private DozeHost.Callback mDozeHostCallback; @@ -106,8 +94,6 @@ public class DozeSuppressorTest extends SysuiTestCase { mDozeHost, mConfig, mDozeLog, - mBroadcastDispatcher, - mUiModeManager, mBiometricUnlockControllerLazy); mDozeSuppressor.setDozeMachine(mDozeMachine); @@ -122,36 +108,35 @@ public class DozeSuppressorTest extends SysuiTestCase { public void testRegistersListenersOnInitialized_unregisteredOnFinish() { // check that receivers and callbacks registered mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); - captureBroadcastReceiver(); captureDozeHostCallback(); // check that receivers and callbacks are unregistered mDozeSuppressor.transitionTo(INITIALIZED, FINISH); - verify(mBroadcastDispatcher).unregisterReceiver(mBroadcastReceiver); verify(mDozeHost).removeCallback(mDozeHostCallback); } @Test public void testSuspendTriggersDoze_carMode() { // GIVEN car mode - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); // WHEN dozing begins mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); // THEN doze continues with all doze triggers disabled. - verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS); + verify(mDozeMachine, atLeastOnce()).requestState(DOZE_SUSPEND_TRIGGERS); + verify(mDozeMachine, never()) + .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS))); } @Test public void testSuspendTriggersDoze_enterCarMode() { // GIVEN currently dozing mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); - captureBroadcastReceiver(); mDozeSuppressor.transitionTo(INITIALIZED, DOZE); // WHEN car mode entered - mBroadcastReceiver.onReceive(null, new Intent(ACTION_ENTER_CAR_MODE)); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); // THEN doze continues with all doze triggers disabled. verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS); @@ -160,13 +145,13 @@ public class DozeSuppressorTest extends SysuiTestCase { @Test public void testDozeResume_exitCarMode() { // GIVEN currently suspended, with AOD not enabled + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(false); mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); - captureBroadcastReceiver(); mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS); // WHEN exiting car mode - mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE)); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL); // THEN doze is resumed verify(mDozeMachine).requestState(DOZE); @@ -175,19 +160,53 @@ public class DozeSuppressorTest extends SysuiTestCase { @Test public void testDozeAoDResume_exitCarMode() { // GIVEN currently suspended, with AOD not enabled + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(true); mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); - captureBroadcastReceiver(); mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS); // WHEN exiting car mode - mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE)); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL); // THEN doze AOD is resumed verify(mDozeMachine).requestState(DOZE_AOD); } @Test + public void testUiModeDoesNotChange_noStateTransition() { + mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); + clearInvocations(mDozeMachine); + + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + + verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS); + verify(mDozeMachine, never()) + .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS))); + } + + @Test + public void testUiModeTypeChange_whenDozeMachineIsNotReady_doesNotDoAnything() { + when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true); + + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + + verify(mDozeMachine, never()).requestState(any()); + } + + @Test + public void testUiModeTypeChange_CarModeEnabledAndDozeMachineNotReady_suspendsTriggersAfter() { + when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + verify(mDozeMachine, never()).requestState(any()); + + mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); + + verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS); + } + + @Test public void testEndDoze_unprovisioned() { // GIVEN device unprovisioned when(mDozeHost.isProvisioned()).thenReturn(false); @@ -276,14 +295,4 @@ public class DozeSuppressorTest extends SysuiTestCase { verify(mDozeHost).addCallback(mDozeHostCaptor.capture()); mDozeHostCallback = mDozeHostCaptor.getValue(); } - - private void captureBroadcastReceiver() { - verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiverCaptor.capture(), - mIntentFilterCaptor.capture()); - mBroadcastReceiver = mBroadcastReceiverCaptor.getValue(); - mIntentFilter = mIntentFilterCaptor.getValue(); - assertEquals(2, mIntentFilter.countActions()); - org.hamcrest.MatcherAssert.assertThat(() -> mIntentFilter.actionsIterator(), - containsInAnyOrder(ACTION_ENTER_CAR_MODE, ACTION_EXIT_CAR_MODE)); - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java index 781dc1550048..6091d3a93f14 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java @@ -23,10 +23,10 @@ import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -88,6 +88,8 @@ public class DozeTriggersTest extends SysuiTestCase { @Mock private ProximityCheck mProximityCheck; @Mock + private DozeLog mDozeLog; + @Mock private AuthController mAuthController; @Mock private UiEventLogger mUiEventLogger; @@ -127,7 +129,7 @@ public class DozeTriggersTest extends SysuiTestCase { mTriggers = new DozeTriggers(mContext, mHost, config, dozeParameters, asyncSensorManager, wakeLock, mDockManager, mProximitySensor, - mProximityCheck, mock(DozeLog.class), mBroadcastDispatcher, new FakeSettings(), + mProximityCheck, mDozeLog, mBroadcastDispatcher, new FakeSettings(), mAuthController, mUiEventLogger, mSessionTracker, mKeyguardStateController, mDevicePostureController); mTriggers.setDozeMachine(mMachine); @@ -342,6 +344,16 @@ public class DozeTriggersTest extends SysuiTestCase { verify(mProximityCheck).destroy(); } + @Test + public void testIsExecutingTransition_dropPulse() { + when(mHost.isPulsePending()).thenReturn(false); + when(mMachine.isExecutingTransition()).thenReturn(true); + + mTriggers.onSensor(DozeLog.PULSE_REASON_SENSOR_LONG_PRESS, 100, 100, null); + + verify(mDozeLog).tracePulseDropped(anyString(), eq(null)); + } + private void waitForSensorManager() { mExecutor.runAllReady(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java index 522b5b5a8530..50f27ea27ae9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java @@ -33,7 +33,7 @@ import com.android.systemui.ActivityIntentHelper; import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.media.MediaCarouselController; +import com.android.systemui.media.controls.ui.MediaCarouselController; import com.android.systemui.media.dream.MediaDreamComplication; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.NotificationLockscreenUserManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt index fc672016a886..65ae90b8f7e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt @@ -17,11 +17,19 @@ package com.android.systemui.dump import androidx.test.filters.SmallTest +import com.android.systemui.CoreStartable import com.android.systemui.Dumpable +import com.android.systemui.ProtoDumpable import com.android.systemui.SysuiTestCase -import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.google.common.truth.Truth.assertThat +import java.io.FileDescriptor +import java.io.PrintWriter +import java.io.StringWriter +import javax.inject.Provider import org.junit.Before import org.junit.Test import org.mockito.Mock @@ -29,7 +37,6 @@ import org.mockito.Mockito.anyInt import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import java.io.PrintWriter @SmallTest class DumpHandlerTest : SysuiTestCase() { @@ -43,6 +50,8 @@ class DumpHandlerTest : SysuiTestCase() { @Mock private lateinit var pw: PrintWriter + @Mock + private lateinit var fd: FileDescriptor @Mock private lateinit var dumpable1: Dumpable @@ -52,6 +61,11 @@ class DumpHandlerTest : SysuiTestCase() { private lateinit var dumpable3: Dumpable @Mock + private lateinit var protoDumpable1: ProtoDumpable + @Mock + private lateinit var protoDumpable2: ProtoDumpable + + @Mock private lateinit var buffer1: LogBuffer @Mock private lateinit var buffer2: LogBuffer @@ -66,7 +80,9 @@ class DumpHandlerTest : SysuiTestCase() { mContext, dumpManager, logBufferEulogizer, - mutableMapOf(), + mutableMapOf( + EmptyCoreStartable::class.java to Provider { EmptyCoreStartable() } + ), exceptionHandlerManager ) } @@ -82,7 +98,7 @@ class DumpHandlerTest : SysuiTestCase() { // WHEN some of them are dumped explicitly val args = arrayOf("dumpable1", "dumpable3", "buffer2") - dumpHandler.dump(pw, args) + dumpHandler.dump(fd, pw, args) // THEN only the requested ones have their dump() method called verify(dumpable1).dump(pw, args) @@ -101,7 +117,7 @@ class DumpHandlerTest : SysuiTestCase() { // WHEN that module is dumped val args = arrayOf("dumpable1") - dumpHandler.dump(pw, args) + dumpHandler.dump(fd, pw, args) // THEN its dump() method is called verify(dumpable1).dump(pw, args) @@ -118,7 +134,7 @@ class DumpHandlerTest : SysuiTestCase() { // WHEN a critical dump is requested val args = arrayOf("--dump-priority", "CRITICAL") - dumpHandler.dump(pw, args) + dumpHandler.dump(fd, pw, args) // THEN all modules are dumped (but no buffers) verify(dumpable1).dump(pw, args) @@ -139,7 +155,7 @@ class DumpHandlerTest : SysuiTestCase() { // WHEN a normal dump is requested val args = arrayOf("--dump-priority", "NORMAL") - dumpHandler.dump(pw, args) + dumpHandler.dump(fd, pw, args) // THEN all buffers are dumped (but no modules) verify(dumpable1, never()).dump( @@ -154,4 +170,44 @@ class DumpHandlerTest : SysuiTestCase() { verify(buffer1).dump(pw, 0) verify(buffer2).dump(pw, 0) } -}
\ No newline at end of file + + @Test + fun testConfigDump() { + // GIVEN a StringPrintWriter + val stringWriter = StringWriter() + val spw = PrintWriter(stringWriter) + + // When a config dump is requested + dumpHandler.dump(fd, spw, arrayOf("config")) + + assertThat(stringWriter.toString()).contains(EmptyCoreStartable::class.java.simpleName) + } + + @Test + fun testDumpAllProtoDumpables() { + dumpManager.registerDumpable("protoDumpable1", protoDumpable1) + dumpManager.registerDumpable("protoDumpable2", protoDumpable2) + + val args = arrayOf(DumpHandler.PROTO) + dumpHandler.dump(fd, pw, args) + + verify(protoDumpable1).dumpProto(any(), eq(args)) + verify(protoDumpable2).dumpProto(any(), eq(args)) + } + + @Test + fun testDumpSingleProtoDumpable() { + dumpManager.registerDumpable("protoDumpable1", protoDumpable1) + dumpManager.registerDumpable("protoDumpable2", protoDumpable2) + + val args = arrayOf(DumpHandler.PROTO, "protoDumpable1") + dumpHandler.dump(fd, pw, args) + + verify(protoDumpable1).dumpProto(any(), eq(args)) + verify(protoDumpable2, never()).dumpProto(any(), any()) + } + + private class EmptyCoreStartable : CoreStartable { + override fun start() {} + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt index bd029a727ee3..64547f4463d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt @@ -16,9 +16,9 @@ package com.android.systemui.dump -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogcatEchoTracker /** * Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests. diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt index 4c6113870737..9628ee93ceff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt @@ -51,14 +51,6 @@ class FlagCommandTest : SysuiTestCase() { } @Test - fun noOpCommand() { - cmd.execute(pw, ArrayList()) - Mockito.verify(pw, Mockito.atLeastOnce()).println() - Mockito.verify(featureFlags).isEnabled(flagA) - Mockito.verify(featureFlags).isEnabled(flagB) - } - - @Test fun readFlagCommand() { cmd.execute(pw, listOf(flagA.id.toString())) Mockito.verify(featureFlags).isEnabled(flagA) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java deleted file mode 100644 index b5e9e8decb4c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; - -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyObject; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.res.Resources; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.view.View; - -import androidx.test.filters.SmallTest; - -import com.android.keyguard.AnimatableClockController; -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.settingslib.Utils; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shared.clocks.AnimatableClockView; -import com.android.systemui.statusbar.policy.BatteryController; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.MockitoSession; -import org.mockito.quality.Strictness; - -import java.util.concurrent.Executor; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper -public class AnimatableClockControllerTest extends SysuiTestCase { - @Mock - private AnimatableClockView mClockView; - @Mock - private StatusBarStateController mStatusBarStateController; - @Mock - private BroadcastDispatcher mBroadcastDispatcher; - @Mock - private BatteryController mBatteryController; - @Mock - private KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @Mock - private Resources mResources; - @Mock - private Executor mMainExecutor; - @Mock - private Executor mBgExecutor; - @Mock - private FeatureFlags mFeatureFlags; - - private MockitoSession mStaticMockSession; - private AnimatableClockController mAnimatableClockController; - - // Capture listeners so that they can be used to send events - @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor = - ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); - private View.OnAttachStateChangeListener mAttachListener; - - @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor; - private StatusBarStateController.StateListener mStatusBarStateCallback; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - mStaticMockSession = mockitoSession() - .mockStatic(Utils.class) - .strictness(Strictness.LENIENT) // it's ok if mocked classes aren't used - .startMocking(); - when(Utils.getColorAttrDefaultColor(anyObject(), anyInt())).thenReturn(0); - - mAnimatableClockController = new AnimatableClockController( - mClockView, - mStatusBarStateController, - mBroadcastDispatcher, - mBatteryController, - mKeyguardUpdateMonitor, - mResources, - mMainExecutor, - mBgExecutor, - mFeatureFlags - ); - mAnimatableClockController.init(); - captureAttachListener(); - } - - @After - public void tearDown() { - mStaticMockSession.finishMocking(); - } - - @Test - public void testOnAttachedUpdatesDozeStateToTrue() { - // GIVEN dozing - when(mStatusBarStateController.isDozing()).thenReturn(true); - when(mStatusBarStateController.getDozeAmount()).thenReturn(1f); - - // WHEN the clock view gets attached - mAttachListener.onViewAttachedToWindow(mClockView); - - // THEN the clock controller updated its dozing state to true - assertTrue(mAnimatableClockController.isDozing()); - } - - @Test - public void testOnAttachedUpdatesDozeStateToFalse() { - // GIVEN not dozing - when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarStateController.getDozeAmount()).thenReturn(0f); - - // WHEN the clock view gets attached - mAttachListener.onViewAttachedToWindow(mClockView); - - // THEN the clock controller updated its dozing state to false - assertFalse(mAnimatableClockController.isDozing()); - } - - private void captureAttachListener() { - verify(mClockView).addOnAttachStateChangeListener(mAttachCaptor.capture()); - mAttachListener = mAttachCaptor.getValue(); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 4c986bffd172..2c3ddd574b0f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -60,6 +60,7 @@ import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; @@ -112,6 +113,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private FalsingCollectorFake mFalsingCollector; + private @Mock CentralSurfaces mCentralSurfaces; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -258,6 +261,26 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { verify(mKeyguardStateController).notifyKeyguardGoingAway(false); } + @Test + public void testUpdateIsKeyguardAfterOccludeAnimationEnds() { + mViewMediator.mOccludeAnimationController.onLaunchAnimationEnd( + false /* isExpandingFullyAbove */); + + // Since the updateIsKeyguard call is delayed during the animation, ensure it's called once + // it ends. + verify(mCentralSurfaces).updateIsKeyguard(); + } + + @Test + public void testUpdateIsKeyguardAfterOccludeAnimationIsCancelled() { + mViewMediator.mOccludeAnimationController.onLaunchAnimationCancelled( + null /* newKeyguardOccludedState */); + + // Since the updateIsKeyguard call is delayed during the animation, ensure it's called if + // it's cancelled. + verify(mCentralSurfaces).updateIsKeyguard(); + } + private void createAndStartViewMediator() { mViewMediator = new KeyguardViewMediator( mContext, @@ -287,5 +310,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mNotificationShadeWindowControllerLazy, () -> mActivityLaunchAnimator); mViewMediator.start(); + + mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null, null); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java deleted file mode 100644 index cefd68dde0a0..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java +++ /dev/null @@ -1,476 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; -import static com.android.keyguard.LockIconView.ICON_LOCK; -import static com.android.keyguard.LockIconView.ICON_UNLOCK; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.anyInt; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.drawable.AnimatedStateListDrawable; -import android.hardware.biometrics.BiometricSourceType; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.util.Pair; -import android.view.View; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityManager; - -import androidx.test.filters.SmallTest; - -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.keyguard.KeyguardUpdateMonitorCallback; -import com.android.keyguard.KeyguardViewController; -import com.android.keyguard.LockIconView; -import com.android.keyguard.LockIconViewController; -import com.android.systemui.R; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.biometrics.AuthController; -import com.android.systemui.biometrics.AuthRippleController; -import com.android.systemui.doze.util.BurnInHelperKt; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.VibratorHelper; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.MockitoSession; -import org.mockito.quality.Strictness; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper -public class LockIconViewControllerTest extends SysuiTestCase { - private static final String UNLOCKED_LABEL = "unlocked"; - private static final int PADDING = 10; - - private MockitoSession mStaticMockSession; - - private @Mock LockIconView mLockIconView; - private @Mock AnimatedStateListDrawable mIconDrawable; - private @Mock Context mContext; - private @Mock Resources mResources; - private @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager; - private @Mock StatusBarStateController mStatusBarStateController; - private @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private @Mock KeyguardViewController mKeyguardViewController; - private @Mock KeyguardStateController mKeyguardStateController; - private @Mock FalsingManager mFalsingManager; - private @Mock AuthController mAuthController; - private @Mock DumpManager mDumpManager; - private @Mock AccessibilityManager mAccessibilityManager; - private @Mock ConfigurationController mConfigurationController; - private @Mock VibratorHelper mVibrator; - private @Mock AuthRippleController mAuthRippleController; - private FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock()); - - private LockIconViewController mLockIconViewController; - - // Capture listeners so that they can be used to send events - @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor = - ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); - private View.OnAttachStateChangeListener mAttachListener; - - @Captor private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor = - ArgumentCaptor.forClass(KeyguardStateController.Callback.class); - private KeyguardStateController.Callback mKeyguardStateCallback; - - @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor = - ArgumentCaptor.forClass(StatusBarStateController.StateListener.class); - private StatusBarStateController.StateListener mStatusBarStateListener; - - @Captor private ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor; - private AuthController.Callback mAuthControllerCallback; - - @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> - mKeyguardUpdateMonitorCallbackCaptor = - ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); - private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback; - - @Captor private ArgumentCaptor<Point> mPointCaptor; - - @Before - public void setUp() throws Exception { - mStaticMockSession = mockitoSession() - .mockStatic(BurnInHelperKt.class) - .strictness(Strictness.LENIENT) - .startMocking(); - MockitoAnnotations.initMocks(this); - - setupLockIconViewMocks(); - when(mContext.getResources()).thenReturn(mResources); - when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager); - Rect windowBounds = new Rect(0, 0, 800, 1200); - when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds); - when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL); - when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable); - when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING); - when(mAuthController.getScaleFactor()).thenReturn(1f); - - when(mKeyguardStateController.isShowing()).thenReturn(true); - when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); - when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); - - mLockIconViewController = new LockIconViewController( - mLockIconView, - mStatusBarStateController, - mKeyguardUpdateMonitor, - mKeyguardViewController, - mKeyguardStateController, - mFalsingManager, - mAuthController, - mDumpManager, - mAccessibilityManager, - mConfigurationController, - mDelayableExecutor, - mVibrator, - mAuthRippleController, - mResources - ); - } - - @After - public void tearDown() { - mStaticMockSession.finishMocking(); - } - - @Test - public void testUpdateFingerprintLocationOnInit() { - // GIVEN fp sensor location is available pre-attached - Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location - - // WHEN lock icon view controller is initialized and attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN lock icon view location is updated to the udfps location with UDFPS radius - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING)); - } - - @Test - public void testUpdatePaddingBasedOnResolutionScale() { - // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5 - Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location - when(mAuthController.getScaleFactor()).thenReturn(5f); - - // WHEN lock icon view controller is initialized and attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN lock icon view location is updated with the scaled radius - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING * 5)); - } - - @Test - public void testUpdateLockIconLocationOnAuthenticatorsRegistered() { - // GIVEN fp sensor location is not available pre-init - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); - when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - resetLockIconView(); // reset any method call counts for when we verify method calls later - - // GIVEN fp sensor location is available post-attached - captureAuthControllerCallback(); - Pair<Float, Point> udfps = setupUdfps(); - - // WHEN all authenticators are registered - mAuthControllerCallback.onAllAuthenticatorsRegistered(); - mDelayableExecutor.runAllReady(); - - // THEN lock icon view location is updated with the same coordinates as auth controller vals - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING)); - } - - @Test - public void testUpdateLockIconLocationOnUdfpsLocationChanged() { - // GIVEN fp sensor location is not available pre-init - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); - when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - resetLockIconView(); // reset any method call counts for when we verify method calls later - - // GIVEN fp sensor location is available post-attached - captureAuthControllerCallback(); - Pair<Float, Point> udfps = setupUdfps(); - - // WHEN udfps location changes - mAuthControllerCallback.onUdfpsLocationChanged(); - mDelayableExecutor.runAllReady(); - - // THEN lock icon view location is updated with the same coordinates as auth controller vals - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING)); - } - - @Test - public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() { - // GIVEN Udpfs sensor location is available - setupUdfps(); - - mLockIconViewController.init(); - captureAttachListener(); - - // WHEN the view is attached - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN the lock icon view background should be enabled - verify(mLockIconView).setUseBackground(true); - } - - @Test - public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() { - // GIVEN Udfps sensor location is not supported - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); - - mLockIconViewController.init(); - captureAttachListener(); - - // WHEN the view is attached - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN the lock icon view background should be disabled - verify(mLockIconView).setUseBackground(false); - } - - @Test - public void testUnlockIconShows_biometricUnlockedTrue() { - // GIVEN UDFPS sensor location is available - setupUdfps(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureKeyguardUpdateMonitorCallback(); - - // GIVEN user has unlocked with a biometric auth (ie: face auth) - when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true); - reset(mLockIconView); - - // WHEN face auth's biometric running state changes - mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false, - BiometricSourceType.FACE); - - // THEN the unlock icon is shown - verify(mLockIconView).setContentDescription(UNLOCKED_LABEL); - } - - @Test - public void testLockIconStartState() { - // GIVEN lock icon state - setupShowLockIcon(); - - // WHEN lock icon controller is initialized - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN the lock icon should show - verify(mLockIconView).updateIcon(ICON_LOCK, false); - } - - @Test - public void testLockIcon_updateToUnlock() { - // GIVEN starting state for the lock icon - setupShowLockIcon(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureKeyguardStateCallback(); - reset(mLockIconView); - - // WHEN the unlocked state changes to canDismissLockScreen=true - when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); - mKeyguardStateCallback.onUnlockedChanged(); - - // THEN the unlock should show - verify(mLockIconView).updateIcon(ICON_UNLOCK, false); - } - - @Test - public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() { - // GIVEN udfps not enrolled - setupUdfps(); - when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false); - - // GIVEN starting state for the lock icon - setupShowLockIcon(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureStatusBarStateListener(); - reset(mLockIconView); - - // WHEN the dozing state changes - mStatusBarStateListener.onDozingChanged(true /* isDozing */); - - // THEN the icon is cleared - verify(mLockIconView).clearIcon(); - } - - @Test - public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() { - // GIVEN udfps enrolled - setupUdfps(); - when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); - - // GIVEN starting state for the lock icon - setupShowLockIcon(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureStatusBarStateListener(); - reset(mLockIconView); - - // WHEN the dozing state changes - mStatusBarStateListener.onDozingChanged(true /* isDozing */); - - // THEN the AOD lock icon should show - verify(mLockIconView).updateIcon(ICON_LOCK, true); - } - - @Test - public void testBurnInOffsetsUpdated_onDozeAmountChanged() { - // GIVEN udfps enrolled - setupUdfps(); - when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); - - // GIVEN burn-in offset = 5 - int burnInOffset = 5; - when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset); - - // GIVEN starting state for the lock icon (keyguard) - setupShowLockIcon(); - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureStatusBarStateListener(); - reset(mLockIconView); - - // WHEN dozing updates - mStatusBarStateListener.onDozingChanged(true /* isDozing */); - mStatusBarStateListener.onDozeAmountChanged(1f, 1f); - - // THEN the view's translation is updated to use the AoD burn-in offsets - verify(mLockIconView).setTranslationY(burnInOffset); - verify(mLockIconView).setTranslationX(burnInOffset); - reset(mLockIconView); - - // WHEN the device is no longer dozing - mStatusBarStateListener.onDozingChanged(false /* isDozing */); - mStatusBarStateListener.onDozeAmountChanged(0f, 0f); - - // THEN the view is updated to NO translation (no burn-in offsets anymore) - verify(mLockIconView).setTranslationY(0); - verify(mLockIconView).setTranslationX(0); - - } - private Pair<Float, Point> setupUdfps() { - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true); - final Point udfpsLocation = new Point(50, 75); - final float radius = 33f; - when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation); - when(mAuthController.getUdfpsRadius()).thenReturn(radius); - - return new Pair(radius, udfpsLocation); - } - - private void setupShowLockIcon() { - when(mKeyguardStateController.isShowing()).thenReturn(true); - when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); - when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarStateController.getDozeAmount()).thenReturn(0f); - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); - when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false); - } - - private void captureAuthControllerCallback() { - verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture()); - mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue(); - } - - private void captureAttachListener() { - verify(mLockIconView).addOnAttachStateChangeListener(mAttachCaptor.capture()); - mAttachListener = mAttachCaptor.getValue(); - } - - private void captureKeyguardStateCallback() { - verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture()); - mKeyguardStateCallback = mKeyguardStateCaptor.getValue(); - } - - private void captureStatusBarStateListener() { - verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture()); - mStatusBarStateListener = mStatusBarStateCaptor.getValue(); - } - - private void captureKeyguardUpdateMonitorCallback() { - verify(mKeyguardUpdateMonitor).registerCallback( - mKeyguardUpdateMonitorCallbackCaptor.capture()); - mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue(); - } - - private void setupLockIconViewMocks() { - when(mLockIconView.getResources()).thenReturn(mResources); - when(mLockIconView.getContext()).thenReturn(mContext); - } - - private void resetLockIconView() { - reset(mLockIconView); - setupLockIconViewMocks(); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt new file mode 100644 index 000000000000..1b34100b1cef --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -0,0 +1,259 @@ +/* + * 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.keyguard.data.repository + +import android.animation.AnimationHandler.AnimationFrameCallbackProvider +import android.animation.ValueAnimator +import android.util.Log +import android.util.Log.TerribleFailure +import android.util.Log.TerribleFailureHandler +import android.view.Choreographer.FrameCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.google.common.truth.Truth.assertThat +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.After +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardTransitionRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardTransitionRepository + private lateinit var oldWtfHandler: TerribleFailureHandler + private lateinit var wtfHandler: WtfHandler + + @Before + fun setUp() { + underTest = KeyguardTransitionRepository() + wtfHandler = WtfHandler() + oldWtfHandler = Log.setWtfHandler(wtfHandler) + } + + @After + fun tearDown() { + oldWtfHandler?.let { Log.setWtfHandler(it) } + } + + @Test + fun `startTransition runs animator to completion`() = + runBlocking(IMMEDIATE) { + val (animator, provider) = setupAnimator(this) + + val steps = mutableListOf<TransitionStep>() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator)) + + val startTime = System.currentTimeMillis() + while (animator.isRunning()) { + yield() + if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) { + fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION") + } + } + + assertSteps(steps, listWithStep(BigDecimal(.1))) + + job.cancel() + provider.stop() + } + + @Test + fun `startTransition called during another transition fails`() { + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, null)) + underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, BOUNCER, null)) + + assertThat(wtfHandler.failed).isTrue() + } + + @Test + fun `Null animator enables manual control with updateTransition`() = + runBlocking(IMMEDIATE) { + val steps = mutableListOf<TransitionStep>() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + underTest.updateTransition(it, 1f, TransitionState.FINISHED) + } + + assertThat(steps.size).isEqualTo(3) + assertThat(steps[0]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + assertThat(steps[1]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING)) + assertThat(steps[2]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + job.cancel() + } + + @Test + fun `Attempt to manually update transition with invalid UUID throws exception`() { + underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING) + assertThat(wtfHandler.failed).isTrue() + } + + @Test + fun `Attempt to manually update transition after FINISHED state throws exception`() { + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 1f, TransitionState.FINISHED) + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + } + assertThat(wtfHandler.failed).isTrue() + } + + private fun listWithStep(step: BigDecimal): List<BigDecimal> { + val steps = mutableListOf<BigDecimal>() + + var i = BigDecimal.ZERO + while (i.compareTo(BigDecimal.ONE) <= 0) { + steps.add(i) + i = (i + step).setScale(2, RoundingMode.HALF_UP) + } + + return steps + } + + private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) { + // + 2 accounts for start and finish of automated transition + assertThat(steps.size).isEqualTo(fractions.size + 2) + + assertThat(steps[0]).isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + fractions.forEachIndexed { index, fraction -> + assertThat(steps[index + 1]) + .isEqualTo( + TransitionStep(AOD, LOCKSCREEN, fraction.toFloat(), TransitionState.RUNNING) + ) + } + assertThat(steps[steps.size - 1]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + + assertThat(wtfHandler.failed).isFalse() + } + + private fun setupAnimator( + scope: CoroutineScope + ): Pair<ValueAnimator, TestFrameCallbackProvider> { + val animator = + ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(ANIMATION_DURATION) + } + + val provider = TestFrameCallbackProvider(animator, scope) + provider.start() + + return Pair(animator, provider) + } + + /** Gives direct control over ValueAnimator. See [AnimationHandler] */ + private class TestFrameCallbackProvider( + private val animator: ValueAnimator, + private val scope: CoroutineScope, + ) : AnimationFrameCallbackProvider { + + private var frameCount = 1L + private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null)) + private var job: Job? = null + + fun start() { + animator.getAnimationHandler().setProvider(this) + + job = + scope.launch { + frames.collect { + // Delay is required for AnimationHandler to properly register a callback + delay(1) + val (frameNumber, callback) = it + callback?.doFrame(frameNumber) + } + } + } + + fun stop() { + job?.cancel() + animator.getAnimationHandler().setProvider(null) + } + + override fun postFrameCallback(cb: FrameCallback) { + frames.value = Pair(++frameCount, cb) + } + override fun postCommitCallback(runnable: Runnable) {} + override fun getFrameTime() = frameCount + override fun getFrameDelay() = 1L + override fun setFrameDelay(delay: Long) {} + } + + private class WtfHandler : TerribleFailureHandler { + var failed = false + override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) { + failed = true + } + } + + companion object { + private const val MAX_TEST_DURATION = 100L + private const val ANIMATION_DURATION = 10L + private const val OWNER_NAME = "Test" + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt deleted file mode 100644 index cc6874ba3ba7..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt +++ /dev/null @@ -1,427 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media - -import android.app.PendingIntent -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import androidx.test.filters.SmallTest -import com.android.internal.logging.InstanceId -import com.android.systemui.SysuiTestCase -import com.android.systemui.classifier.FalsingCollector -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.dump.DumpManager -import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION -import com.android.systemui.media.MediaCarouselController.Companion.DURATION -import com.android.systemui.media.MediaCarouselController.Companion.PAGINATION_DELAY -import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER -import com.android.systemui.media.MediaHierarchyManager.Companion.LOCATION_QS -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener -import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider -import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.systemui.util.mockito.capture -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.time.FakeSystemClock -import javax.inject.Provider -import junit.framework.Assert.assertEquals -import junit.framework.Assert.assertTrue -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.mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever -import org.mockito.MockitoAnnotations - -private val DATA = MediaTestUtils.emptyMediaData - -private val SMARTSPACE_KEY = "smartspace" - -@SmallTest -@TestableLooper.RunWithLooper(setAsMainLooper = true) -@RunWith(AndroidTestingRunner::class) -class MediaCarouselControllerTest : SysuiTestCase() { - - @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel> - @Mock lateinit var panel: MediaControlPanel - @Mock lateinit var visualStabilityProvider: VisualStabilityProvider - @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager - @Mock lateinit var mediaHostState: MediaHostState - @Mock lateinit var activityStarter: ActivityStarter - @Mock @Main private lateinit var executor: DelayableExecutor - @Mock lateinit var mediaDataManager: MediaDataManager - @Mock lateinit var configurationController: ConfigurationController - @Mock lateinit var falsingCollector: FalsingCollector - @Mock lateinit var falsingManager: FalsingManager - @Mock lateinit var dumpManager: DumpManager - @Mock lateinit var logger: MediaUiEventLogger - @Mock lateinit var debugLogger: MediaCarouselControllerLogger - @Mock lateinit var mediaPlayer: MediaControlPanel - @Mock lateinit var mediaViewController: MediaViewController - @Mock lateinit var smartspaceMediaData: SmartspaceMediaData - @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> - @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener> - - private val clock = FakeSystemClock() - private lateinit var mediaCarouselController: MediaCarouselController - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - mediaCarouselController = MediaCarouselController( - context, - mediaControlPanelFactory, - visualStabilityProvider, - mediaHostStatesManager, - activityStarter, - clock, - executor, - mediaDataManager, - configurationController, - falsingCollector, - falsingManager, - dumpManager, - logger, - debugLogger - ) - verify(mediaDataManager).addListener(capture(listener)) - verify(visualStabilityProvider) - .addPersistentReorderingAllowedListener(capture(visualStabilityCallback)) - whenever(mediaControlPanelFactory.get()).thenReturn(mediaPlayer) - whenever(mediaPlayer.mediaViewController).thenReturn(mediaViewController) - whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData) - MediaPlayerData.clear() - } - - @Test - fun testPlayerOrdering() { - // Test values: key, data, last active time - val playingLocal = Triple("playing local", - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false), - 4500L) - - val playingCast = Triple("playing cast", - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false), - 5000L) - - val pausedLocal = Triple("paused local", - DATA.copy(active = true, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false), - 1000L) - - val pausedCast = Triple("paused cast", - DATA.copy(active = true, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false), - 2000L) - - val playingRcn = Triple("playing RCN", - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false), - 5000L) - - val pausedRcn = Triple("paused RCN", - DATA.copy(active = true, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false), - 5000L) - - val active = Triple("active", - DATA.copy(active = true, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true), - 250L) - - val resume1 = Triple("resume 1", - DATA.copy(active = false, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true), - 500L) - - val resume2 = Triple("resume 2", - DATA.copy(active = false, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true), - 1000L) - - val activeMoreRecent = Triple("active more recent", - DATA.copy(active = false, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 2L), - 1000L) - - val activeLessRecent = Triple("active less recent", - DATA.copy(active = false, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 1L), - 1000L) - // Expected ordering for media players: - // Actively playing local sessions - // Actively playing cast sessions - // Paused local and cast sessions, by last active - // RCNs - // Resume controls, by last active - - val expected = listOf(playingLocal, playingCast, pausedCast, pausedLocal, playingRcn, - pausedRcn, active, resume2, resume1) - - expected.forEach { - clock.setCurrentTimeMillis(it.third) - MediaPlayerData.addMediaPlayer(it.first, it.second.copy(notificationKey = it.first), - panel, clock, isSsReactivated = false) - } - - for ((index, key) in MediaPlayerData.playerKeys().withIndex()) { - assertEquals(expected.get(index).first, key.data.notificationKey) - } - } - - @Test - fun testOrderWithSmartspace_prioritized() { - testPlayerOrdering() - - // If smartspace is prioritized - MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel, - true, clock) - - // Then it should be shown immediately after any actively playing controls - assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec) - } - - @Test - fun testOrderWithSmartspace_notPrioritized() { - testPlayerOrdering() - - // If smartspace is not prioritized - MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel, - false, clock) - - // Then it should be shown at the end of the carousel's active entries - val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1 - assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec) - } - - @Test - fun testSwipeDismiss_logged() { - mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke() - - verify(logger).logSwipeDismiss() - } - - @Test - fun testSettingsButton_logged() { - mediaCarouselController.settingsButton.callOnClick() - - verify(logger).logCarouselSettings() - } - - @Test - fun testLocationChangeQs_logged() { - mediaCarouselController.onDesiredLocationChanged( - MediaHierarchyManager.LOCATION_QS, - mediaHostState, - animate = false) - verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS) - } - - @Test - fun testLocationChangeQqs_logged() { - mediaCarouselController.onDesiredLocationChanged( - MediaHierarchyManager.LOCATION_QQS, - mediaHostState, - animate = false) - verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS) - } - - @Test - fun testLocationChangeLockscreen_logged() { - mediaCarouselController.onDesiredLocationChanged( - MediaHierarchyManager.LOCATION_LOCKSCREEN, - mediaHostState, - animate = false) - verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN) - } - - @Test - fun testLocationChangeDream_logged() { - mediaCarouselController.onDesiredLocationChanged( - MediaHierarchyManager.LOCATION_DREAM_OVERLAY, - mediaHostState, - animate = false) - verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY) - } - - @Test - fun testRecommendationRemoved_logged() { - val packageName = "smartspace package" - val instanceId = InstanceId.fakeInstanceId(123) - - val smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( - packageName = packageName, - instanceId = instanceId - ) - MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock) - mediaCarouselController.removePlayer(SMARTSPACE_KEY) - - verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!)) - } - - @Test - fun testMediaLoaded_ScrollToActivePlayer() { - listener.value.onMediaDataLoaded("playing local", - null, - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false) - ) - listener.value.onMediaDataLoaded("paused local", - null, - DATA.copy(active = true, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)) - // adding a media recommendation card. - listener.value.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, - false) - mediaCarouselController.shouldScrollToActivePlayer = true - // switching between media players. - listener.value.onMediaDataLoaded("playing local", - "playing local", - DATA.copy(active = true, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true) - ) - listener.value.onMediaDataLoaded("paused local", - "paused local", - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)) - - assertEquals( - MediaPlayerData.getMediaPlayerIndex("paused local"), - mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex - ) - } - - @Test - fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() { - MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel, - false, clock) - listener.value.onMediaDataLoaded("playing local", - null, - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false) - ) - - var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local") - assertEquals( - playerIndex, - mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex - ) - assertEquals(playerIndex, 0) - - // Replaying the same media player one more time. - // And check that the card stays in its position. - listener.value.onMediaDataLoaded("playing local", - null, - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false) - ) - playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local") - assertEquals(playerIndex, 0) - } - - @Test - fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() { - var result = false - mediaCarouselController.updateHostVisibility = { result = true } - - whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true) - listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) - - assertEquals(true, result) - } - - @Test - fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() { - var result = false - mediaCarouselController.updateHostVisibility = { result = true } - - whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false) - listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) - assertEquals(false, result) - - visualStabilityCallback.value.onReorderingAllowed() - assertEquals(true, result) - } - - @Test - fun testGetCurrentVisibleMediaContentIntent() { - val clickIntent1 = mock(PendingIntent::class.java) - val player1 = Triple("player1", - DATA.copy(clickIntent = clickIntent1), - 1000L) - clock.setCurrentTimeMillis(player1.third) - MediaPlayerData.addMediaPlayer(player1.first, - player1.second.copy(notificationKey = player1.first), - panel, clock, isSsReactivated = false) - - assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1) - - val clickIntent2 = mock(PendingIntent::class.java) - val player2 = Triple("player2", - DATA.copy(clickIntent = clickIntent2), - 2000L) - clock.setCurrentTimeMillis(player2.third) - MediaPlayerData.addMediaPlayer(player2.first, - player2.second.copy(notificationKey = player2.first), - panel, clock, isSsReactivated = false) - - // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is - // added to the front because it was active more recently. - assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2) - - val clickIntent3 = mock(PendingIntent::class.java) - val player3 = Triple("player3", - DATA.copy(clickIntent = clickIntent3), - 500L) - clock.setCurrentTimeMillis(player3.third) - MediaPlayerData.addMediaPlayer(player3.first, - player3.second.copy(notificationKey = player3.first), - panel, clock, isSsReactivated = false) - - // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is - // added to the end because it was active less recently. - assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2) - } - - @Test - fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() { - val delta = 0.0001F - val paginationSquishMiddle = TRANSFORM_BEZIER.getInterpolation( - (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION) - val paginationSquishEnd = TRANSFORM_BEZIER.getInterpolation( - (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION) - whenever(mediaHostStatesManager.mediaHostStates) - .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState)) - whenever(mediaHostState.visible).thenReturn(true) - mediaCarouselController.currentEndLocation = LOCATION_QS - whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle) - mediaCarouselController.updatePageIndicatorAlpha() - assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta) - - whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd) - mediaCarouselController.updatePageIndicatorAlpha() - assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt deleted file mode 100644 index 3d9ed5fe7f7b..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.android.systemui.media - -import com.android.internal.logging.InstanceId - -class MediaTestUtils { - companion object { - val emptyMediaData = MediaData( - userId = 0, - initialized = true, - app = null, - appIcon = null, - artist = null, - song = null, - artwork = null, - actions = emptyList(), - actionsToShowInCompact = emptyList(), - packageName = "", - token = null, - clickIntent = null, - device = null, - active = true, - resumeAction = null, - isPlaying = false, - instanceId = InstanceId.fakeInstanceId(-1), - appUid = -1) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewHolderTest.kt deleted file mode 100644 index ee327937e091..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewHolderTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.android.systemui.media - -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.view.LayoutInflater -import android.widget.FrameLayout -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper -class MediaViewHolderTest : SysuiTestCase() { - - @Test - fun create_succeeds() { - val inflater = LayoutInflater.from(context) - val parent = FrameLayout(context) - - MediaViewHolder.create(inflater, parent) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/MediaTestUtils.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/MediaTestUtils.kt new file mode 100644 index 000000000000..3437365d9902 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/MediaTestUtils.kt @@ -0,0 +1,46 @@ +/* + * 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.media.controls + +import com.android.internal.logging.InstanceId +import com.android.systemui.media.controls.models.player.MediaData + +class MediaTestUtils { + companion object { + val emptyMediaData = + MediaData( + userId = 0, + initialized = true, + app = null, + appIcon = null, + artist = null, + song = null, + artwork = null, + actions = emptyList(), + actionsToShowInCompact = emptyList(), + packageName = "", + token = null, + clickIntent = null, + device = null, + active = true, + resumeAction = null, + isPlaying = false, + instanceId = InstanceId.fakeInstanceId(-1), + appUid = -1 + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/MediaViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/MediaViewHolderTest.kt new file mode 100644 index 000000000000..c829d4cbfb71 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/MediaViewHolderTest.kt @@ -0,0 +1,40 @@ +/* + * 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.media.controls.models.player + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.LayoutInflater +import android.widget.FrameLayout +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class MediaViewHolderTest : SysuiTestCase() { + + @Test + fun create_succeeds() { + val inflater = LayoutInflater.from(context) + val parent = FrameLayout(context) + + MediaViewHolder.create(inflater, parent) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt index 9e9cda843c8f..97b18e214550 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.player import android.animation.Animator import android.animation.ObjectAnimator @@ -26,6 +26,7 @@ import android.widget.TextView import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.media.controls.ui.SquigglyProgress import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule @@ -33,8 +34,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit @SmallTest @RunWith(AndroidTestingRunner::class) @@ -56,10 +57,14 @@ class SeekBarObserverTest : SysuiTestCase() { @Before fun setUp() { - context.orCreateTestableResources - .addOverride(R.dimen.qs_media_enabled_seekbar_height, enabledHeight) - context.orCreateTestableResources - .addOverride(R.dimen.qs_media_disabled_seekbar_height, disabledHeight) + context.orCreateTestableResources.addOverride( + R.dimen.qs_media_enabled_seekbar_height, + enabledHeight + ) + context.orCreateTestableResources.addOverride( + R.dimen.qs_media_disabled_seekbar_height, + disabledHeight + ) seekBarView = SeekBar(context) seekBarView.progressDrawable = mockSquigglyProgress @@ -69,11 +74,12 @@ class SeekBarObserverTest : SysuiTestCase() { whenever(mockHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView) whenever(mockHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView) - observer = object : SeekBarObserver(mockHolder) { - override fun buildResetAnimator(targetTime: Int): Animator { - return mockSeekbarAnimator + observer = + object : SeekBarObserver(mockHolder) { + override fun buildResetAnimator(targetTime: Int): Animator { + return mockSeekbarAnimator + } } - } } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt index 597334033895..7cd8e749a6e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.player import android.media.MediaMetadata import android.media.session.MediaController @@ -57,17 +57,18 @@ public class SeekBarViewModelTest : SysuiTestCase() { private lateinit var viewModel: SeekBarViewModel private lateinit var fakeExecutor: FakeExecutor - private val taskExecutor: TaskExecutor = object : TaskExecutor() { - override fun executeOnDiskIO(runnable: Runnable) { - runnable.run() - } - override fun postToMainThread(runnable: Runnable) { - runnable.run() - } - override fun isMainThread(): Boolean { - return true + private val taskExecutor: TaskExecutor = + object : TaskExecutor() { + override fun executeOnDiskIO(runnable: Runnable) { + runnable.run() + } + override fun postToMainThread(runnable: Runnable) { + runnable.run() + } + override fun isMainThread(): Boolean { + return true + } } - } @Mock private lateinit var mockController: MediaController @Mock private lateinit var mockTransport: MediaController.TransportControls @Mock private lateinit var falsingManager: FalsingManager @@ -81,7 +82,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun setUp() { fakeExecutor = FakeExecutor(FakeSystemClock()) viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor), falsingManager) - viewModel.logSeek = { } + viewModel.logSeek = {} whenever(mockController.sessionToken).thenReturn(token1) whenever(mockBar.context).thenReturn(context) @@ -135,16 +136,18 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun updateDurationWithPlayback() { // GIVEN that the duration is contained within the metadata val duration = 12000L - val metadata = MediaMetadata.Builder().run { - putLong(MediaMetadata.METADATA_KEY_DURATION, duration) - build() - } + val metadata = + MediaMetadata.Builder().run { + putLong(MediaMetadata.METADATA_KEY_DURATION, duration) + build() + } whenever(mockController.getMetadata()).thenReturn(metadata) // AND a valid playback state (ie. media session is not destroyed) - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -158,10 +161,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun updateDurationWithoutPlayback() { // GIVEN that the duration is contained within the metadata val duration = 12000L - val metadata = MediaMetadata.Builder().run { - putLong(MediaMetadata.METADATA_KEY_DURATION, duration) - build() - } + val metadata = + MediaMetadata.Builder().run { + putLong(MediaMetadata.METADATA_KEY_DURATION, duration) + build() + } whenever(mockController.getMetadata()).thenReturn(metadata) // WHEN the controller is updated viewModel.updateController(mockController) @@ -174,16 +178,18 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun updateDurationNegative() { // GIVEN that the duration is negative val duration = -1L - val metadata = MediaMetadata.Builder().run { - putLong(MediaMetadata.METADATA_KEY_DURATION, duration) - build() - } + val metadata = + MediaMetadata.Builder().run { + putLong(MediaMetadata.METADATA_KEY_DURATION, duration) + build() + } whenever(mockController.getMetadata()).thenReturn(metadata) // AND a valid playback state (ie. media session is not destroyed) - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -195,16 +201,18 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun updateDurationZero() { // GIVEN that the duration is zero val duration = 0L - val metadata = MediaMetadata.Builder().run { - putLong(MediaMetadata.METADATA_KEY_DURATION, duration) - build() - } + val metadata = + MediaMetadata.Builder().run { + putLong(MediaMetadata.METADATA_KEY_DURATION, duration) + build() + } whenever(mockController.getMetadata()).thenReturn(metadata) // AND a valid playback state (ie. media session is not destroyed) - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -218,10 +226,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { // GIVEN that the metadata is null whenever(mockController.getMetadata()).thenReturn(null) // AND a valid playback state (ie. media session is not destroyed) - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -233,10 +242,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun updateElapsedTime() { // GIVEN that the PlaybackState contains the current position val position = 200L - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, position, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, position, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -248,10 +258,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Ignore fun updateSeekAvailable() { // GIVEN that seek is included in actions - val state = PlaybackState.Builder().run { - setActions(PlaybackState.ACTION_SEEK_TO) - build() - } + val state = + PlaybackState.Builder().run { + setActions(PlaybackState.ACTION_SEEK_TO) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -263,10 +274,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Ignore fun updateSeekNotAvailable() { // GIVEN that seek is not included in actions - val state = PlaybackState.Builder().run { - setActions(PlaybackState.ACTION_PLAY) - build() - } + val state = + PlaybackState.Builder().run { + setActions(PlaybackState.ACTION_PLAY) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -318,9 +330,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Ignore fun onSeekProgressWithSeekStarting() { val pos = 42L - with(viewModel) { - onSeekProgress(pos) - } + with(viewModel) { onSeekProgress(pos) } fakeExecutor.runAllReady() // THEN then elapsed time should not be updated assertThat(viewModel.progress.value!!.elapsedTime).isNull() @@ -329,11 +339,12 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Test fun seekStarted_listenerNotified() { var isScrubbing: Boolean? = null - val listener = object : SeekBarViewModel.ScrubbingChangeListener { - override fun onScrubbingChanged(scrubbing: Boolean) { - isScrubbing = scrubbing + val listener = + object : SeekBarViewModel.ScrubbingChangeListener { + override fun onScrubbingChanged(scrubbing: Boolean) { + isScrubbing = scrubbing + } } - } viewModel.setScrubbingChangeListener(listener) viewModel.onSeekStarting() @@ -345,11 +356,12 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Test fun seekEnded_listenerNotified() { var isScrubbing: Boolean? = null - val listener = object : SeekBarViewModel.ScrubbingChangeListener { - override fun onScrubbingChanged(scrubbing: Boolean) { - isScrubbing = scrubbing + val listener = + object : SeekBarViewModel.ScrubbingChangeListener { + override fun onScrubbingChanged(scrubbing: Boolean) { + isScrubbing = scrubbing + } } - } viewModel.setScrubbingChangeListener(listener) // Start seeking @@ -385,9 +397,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { val bar = SeekBar(context) // WHEN we get an onProgressChanged event without an onStartTrackingTouch event - with(viewModel.seekBarListener) { - onProgressChanged(bar, pos, true) - } + with(viewModel.seekBarListener) { onProgressChanged(bar, pos, true) } fakeExecutor.runAllReady() // THEN we immediately update the transport @@ -412,9 +422,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { viewModel.updateController(mockController) // WHEN user starts dragging the seek bar val pos = 42 - val bar = SeekBar(context).apply { - progress = pos - } + val bar = SeekBar(context).apply { progress = pos } viewModel.seekBarListener.onStartTrackingTouch(bar) fakeExecutor.runAllReady() // THEN transport controls should be used @@ -427,9 +435,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { viewModel.updateController(mockController) // WHEN user ends drag val pos = 42 - val bar = SeekBar(context).apply { - progress = pos - } + val bar = SeekBar(context).apply { progress = pos } viewModel.seekBarListener.onStopTrackingTouch(bar) fakeExecutor.runAllReady() // THEN transport controls should be used @@ -443,9 +449,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { // WHEN user starts dragging the seek bar val pos = 42 val progPos = 84 - val bar = SeekBar(context).apply { - progress = pos - } + val bar = SeekBar(context).apply { progress = pos } with(viewModel.seekBarListener) { onStartTrackingTouch(bar) onProgressChanged(bar, progPos, true) @@ -478,10 +482,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Test fun queuePollTaskWhenPlaying() { // GIVEN that the track is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 100L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 100L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -492,10 +497,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Test fun noQueuePollTaskWhenStopped() { // GIVEN that the playback state is stopped - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_STOPPED, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_STOPPED, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN updated viewModel.updateController(mockController) @@ -512,10 +518,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { runAllReady() } // AND the playback state is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN updated viewModel.updateController(mockController) @@ -532,10 +539,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { runAllReady() } // AND the playback state is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN updated viewModel.updateController(mockController) @@ -546,10 +554,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Test fun pollTaskQueuesAnotherPollTaskWhenPlaying() { // GIVEN that the track is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 100L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 100L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) viewModel.updateController(mockController) // WHEN the next task runs @@ -566,10 +575,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { // GIVEN listening viewModel.listening = true // AND the playback state is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) viewModel.updateController(mockController) with(fakeExecutor) { @@ -592,10 +602,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { // GIVEN listening viewModel.listening = true // AND the playback state is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) viewModel.updateController(mockController) with(fakeExecutor) { @@ -621,10 +632,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { // GIVEN listening viewModel.listening = true // AND the playback state is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) viewModel.updateController(mockController) with(fakeExecutor) { @@ -654,10 +666,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { runAllReady() } // AND the playback state is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_STOPPED, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_STOPPED, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) viewModel.updateController(mockController) // WHEN start listening @@ -673,10 +686,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { verify(mockController).registerCallback(captor.capture()) val callback = captor.value // WHEN the callback receives an new state - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 100L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 100L, 1f) + build() + } callback.onPlaybackStateChanged(state) with(fakeExecutor) { advanceClockToNext() @@ -690,16 +704,18 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Ignore fun clearSeekBar() { // GIVEN that the duration is contained within the metadata - val metadata = MediaMetadata.Builder().run { - putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L) - build() - } + val metadata = + MediaMetadata.Builder().run { + putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L) + build() + } whenever(mockController.getMetadata()).thenReturn(metadata) // AND a valid playback state (ie. media session is not destroyed) - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // AND the controller has been updated viewModel.updateController(mockController) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt index b5078bc37b84..1d6e980bdb86 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt @@ -1,4 +1,20 @@ -package com.android.systemui.media +/* + * 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.media.controls.models.recommendation import android.app.smartspace.SmartspaceAction import android.graphics.drawable.Icon @@ -36,11 +52,11 @@ class SmartspaceMediaDataTest : SysuiTestCase() { @Test fun isValid_tooFewRecs_returnsFalse() { - val data = DEFAULT_DATA.copy( - recommendations = listOf( - SmartspaceAction.Builder("id", "title").setIcon(icon).build() + val data = + DEFAULT_DATA.copy( + recommendations = + listOf(SmartspaceAction.Builder("id", "title").setIcon(icon).build()) ) - ) assertThat(data.isValid()).isFalse() } @@ -50,14 +66,10 @@ class SmartspaceMediaDataTest : SysuiTestCase() { val recommendations = mutableListOf<SmartspaceAction>() // Add one fewer recommendation w/ icon than the number required for (i in 1 until NUM_REQUIRED_RECOMMENDATIONS) { - recommendations.add( - SmartspaceAction.Builder("id", "title").setIcon(icon).build() - ) + recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build()) } for (i in 1 until 3) { - recommendations.add( - SmartspaceAction.Builder("id", "title").setIcon(null).build() - ) + recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(null).build()) } val data = DEFAULT_DATA.copy(recommendations = recommendations) @@ -70,9 +82,7 @@ class SmartspaceMediaDataTest : SysuiTestCase() { val recommendations = mutableListOf<SmartspaceAction>() // Add the number of required recommendations for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS) { - recommendations.add( - SmartspaceAction.Builder("id", "title").setIcon(icon).build() - ) + recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build()) } val data = DEFAULT_DATA.copy(recommendations = recommendations) @@ -85,9 +95,7 @@ class SmartspaceMediaDataTest : SysuiTestCase() { val recommendations = mutableListOf<SmartspaceAction>() // Add more than enough recommendations for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS + 3) { - recommendations.add( - SmartspaceAction.Builder("id", "title").setIcon(icon).build() - ) + recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build()) } val data = DEFAULT_DATA.copy(recommendations = recommendations) @@ -96,13 +104,14 @@ class SmartspaceMediaDataTest : SysuiTestCase() { } } -private val DEFAULT_DATA = SmartspaceMediaData( - targetId = "INVALID", - isActive = false, - packageName = "INVALID", - cardAction = null, - recommendations = emptyList(), - dismissIntent = null, - headphoneConnectionTimeMillis = 0, - instanceId = InstanceId.fakeInstanceId(-1) -) +private val DEFAULT_DATA = + SmartspaceMediaData( + targetId = "INVALID", + isActive = false, + packageName = "INVALID", + cardAction = null, + recommendations = emptyList(), + dismissIntent = null, + headphoneConnectionTimeMillis = 0, + instanceId = InstanceId.fakeInstanceId(-1) + ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java index 04b93d79f83b..4d2d0f05b76a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.pipeline; import static com.google.common.truth.Truth.assertThat; @@ -26,7 +26,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; -import android.graphics.Color; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -34,6 +33,8 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.InstanceId; import com.android.systemui.SysuiTestCase; +import com.android.systemui.media.controls.models.player.MediaData; +import com.android.systemui.media.controls.models.player.MediaDeviceData; import org.junit.Before; import org.junit.Rule; diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt index 6468fe1a81d7..575b1c6b126e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.app.smartspace.SmartspaceAction import android.testing.AndroidTestingRunner @@ -24,6 +24,11 @@ import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.ui.MediaPlayerData +import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -58,24 +63,15 @@ private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!! @TestableLooper.RunWithLooper class MediaDataFilterTest : SysuiTestCase() { - @Mock - private lateinit var listener: MediaDataManager.Listener - @Mock - private lateinit var broadcastDispatcher: BroadcastDispatcher - @Mock - private lateinit var broadcastSender: BroadcastSender - @Mock - private lateinit var mediaDataManager: MediaDataManager - @Mock - private lateinit var lockscreenUserManager: NotificationLockscreenUserManager - @Mock - private lateinit var executor: Executor - @Mock - private lateinit var smartspaceData: SmartspaceMediaData - @Mock - private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction - @Mock - private lateinit var logger: MediaUiEventLogger + @Mock private lateinit var listener: MediaDataManager.Listener + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock private lateinit var broadcastSender: BroadcastSender + @Mock private lateinit var mediaDataManager: MediaDataManager + @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager + @Mock private lateinit var executor: Executor + @Mock private lateinit var smartspaceData: SmartspaceMediaData + @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction + @Mock private lateinit var logger: MediaUiEventLogger private lateinit var mediaDataFilter: MediaDataFilter private lateinit var dataMain: MediaData @@ -86,14 +82,16 @@ class MediaDataFilterTest : SysuiTestCase() { fun setup() { MockitoAnnotations.initMocks(this) MediaPlayerData.clear() - mediaDataFilter = MediaDataFilter( - context, - broadcastDispatcher, - broadcastSender, - lockscreenUserManager, - executor, - clock, - logger) + mediaDataFilter = + MediaDataFilter( + context, + broadcastDispatcher, + broadcastSender, + lockscreenUserManager, + executor, + clock, + logger + ) mediaDataFilter.mediaDataManager = mediaDataManager mediaDataFilter.addListener(listener) @@ -101,11 +99,13 @@ class MediaDataFilterTest : SysuiTestCase() { setUser(USER_MAIN) // Set up test media data - dataMain = MediaTestUtils.emptyMediaData.copy( + dataMain = + MediaTestUtils.emptyMediaData.copy( userId = USER_MAIN, packageName = PACKAGE, instanceId = INSTANCE_ID, - appUid = APP_UID) + appUid = APP_UID + ) dataGuest = dataMain.copy(userId = USER_GUEST) `when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY) @@ -113,8 +113,8 @@ class MediaDataFilterTest : SysuiTestCase() { `when`(smartspaceData.isValid()).thenReturn(true) `when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE) `when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem)) - `when`(smartspaceData.headphoneConnectionTimeMillis).thenReturn( - clock.currentTimeMillis() - 100) + `when`(smartspaceData.headphoneConnectionTimeMillis) + .thenReturn(clock.currentTimeMillis() - 100) `when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID) } @@ -130,8 +130,8 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) // THEN we should tell the listener - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false)) } @Test @@ -140,8 +140,8 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) // THEN we should NOT tell the listener - verify(listener, never()).onMediaDataLoaded(any(), any(), any(), anyBoolean(), - anyInt(), anyBoolean()) + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } @Test @@ -187,12 +187,12 @@ class MediaDataFilterTest : SysuiTestCase() { setUser(USER_GUEST) // THEN we should add back the guest user media - verify(listener).onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false)) // but not the main user's - verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(), - anyInt(), anyBoolean()) + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(), anyInt(), anyBoolean()) } @Test @@ -340,7 +340,7 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) verify(listener) - .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() assertThat(mediaDataFilter.hasActiveMedia()).isFalse() verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) @@ -353,8 +353,8 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - verify(listener, never()).onMediaDataLoaded(any(), any(), any(), anyBoolean(), - anyInt(), anyBoolean()) + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse() @@ -370,7 +370,7 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) verify(listener) - .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() assertThat(mediaDataFilter.hasActiveMedia()).isFalse() verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) @@ -400,15 +400,15 @@ class MediaDataFilterTest : SysuiTestCase() { // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) // AND we get a smartspace signal mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should tell listeners to treat the media as not active instead - verify(listener, never()).onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), - anyInt(), anyBoolean()) + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean()) verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse() @@ -423,16 +423,23 @@ class MediaDataFilterTest : SysuiTestCase() { // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) // AND we get a smartspace signal mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should tell listeners to treat the media as active instead val dataCurrentAndActive = dataCurrent.copy(active = true) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true), - eq(100), eq(true)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() // Smartspace update shouldn't be propagated for the empty rec list. verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) @@ -445,20 +452,27 @@ class MediaDataFilterTest : SysuiTestCase() { // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) // AND we get a smartspace signal mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should tell listeners to treat the media as active instead val dataCurrentAndActive = dataCurrent.copy(active = true) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true), - eq(100), eq(true)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() // Smartspace update should also be propagated but not prioritized. verify(listener) - .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID)) } @@ -477,14 +491,21 @@ class MediaDataFilterTest : SysuiTestCase() { fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() { val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) val dataCurrentAndActive = dataCurrent.copy(active = true) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true), - eq(100), eq(true)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt index f9c7d2d5cb41..11eb26b1da02 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt @@ -1,4 +1,20 @@ -package com.android.systemui.media +/* + * 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.media.controls.pipeline import android.app.Notification import android.app.Notification.MediaStyle @@ -26,6 +42,13 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider +import com.android.systemui.media.controls.resume.MediaResumeListener +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.SbnBuilder import com.android.systemui.tuner.TunerService @@ -111,58 +134,68 @@ class MediaDataManagerTest : SysuiTestCase() { private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20) - private val originalSmartspaceSetting = Settings.Secure.getInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1) + private val originalSmartspaceSetting = + Settings.Secure.getInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 1 + ) @Before fun setup() { foregroundExecutor = FakeExecutor(clock) backgroundExecutor = FakeExecutor(clock) smartspaceMediaDataProvider = SmartspaceMediaDataProvider() - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1) - mediaDataManager = MediaDataManager( - context = context, - backgroundExecutor = backgroundExecutor, - foregroundExecutor = foregroundExecutor, - mediaControllerFactory = mediaControllerFactory, - broadcastDispatcher = broadcastDispatcher, - dumpManager = dumpManager, - mediaTimeoutListener = mediaTimeoutListener, - mediaResumeListener = mediaResumeListener, - mediaSessionBasedFilter = mediaSessionBasedFilter, - mediaDeviceManager = mediaDeviceManager, - mediaDataCombineLatest = mediaDataCombineLatest, - mediaDataFilter = mediaDataFilter, - activityStarter = activityStarter, - smartspaceMediaDataProvider = smartspaceMediaDataProvider, - useMediaResumption = true, - useQsMediaPlayer = true, - systemClock = clock, - tunerService = tunerService, - mediaFlags = mediaFlags, - logger = logger + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 1 ) - verify(tunerService).addTunable(capture(tunableCaptor), - eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)) + mediaDataManager = + MediaDataManager( + context = context, + backgroundExecutor = backgroundExecutor, + foregroundExecutor = foregroundExecutor, + mediaControllerFactory = mediaControllerFactory, + broadcastDispatcher = broadcastDispatcher, + dumpManager = dumpManager, + mediaTimeoutListener = mediaTimeoutListener, + mediaResumeListener = mediaResumeListener, + mediaSessionBasedFilter = mediaSessionBasedFilter, + mediaDeviceManager = mediaDeviceManager, + mediaDataCombineLatest = mediaDataCombineLatest, + mediaDataFilter = mediaDataFilter, + activityStarter = activityStarter, + smartspaceMediaDataProvider = smartspaceMediaDataProvider, + useMediaResumption = true, + useQsMediaPlayer = true, + systemClock = clock, + tunerService = tunerService, + mediaFlags = mediaFlags, + logger = logger + ) + verify(tunerService) + .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)) session = MediaSession(context, "MediaDataManagerTestSession") - mediaNotification = SbnBuilder().run { - setPkg(PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + } + build() + } + metadataBuilder = + MediaMetadata.Builder().apply { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) } - build() - } - metadataBuilder = MediaMetadata.Builder().apply { - putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) - putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) - } whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller) whenever(controller.transportControls).thenReturn(transportControls) whenever(controller.playbackInfo).thenReturn(playbackInfo) - whenever(playbackInfo.playbackType).thenReturn( - MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) // This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal // listeners in the internal processing pipeline. It receives events, but ince it is a @@ -170,18 +203,18 @@ class MediaDataManagerTest : SysuiTestCase() { // treat mediaSessionBasedFilter as a listener for testing. listener = mediaSessionBasedFilter - val recommendationExtras = Bundle().apply { - putString("package_name", PACKAGE_NAME) - putParcelable("dismiss_intent", DISMISS_INTENT) - } + val recommendationExtras = + Bundle().apply { + putString("package_name", PACKAGE_NAME) + putParcelable("dismiss_intent", DISMISS_INTENT) + } val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play) whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras) whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction) whenever(mediaRecommendationItem.extras).thenReturn(recommendationExtras) whenever(mediaRecommendationItem.icon).thenReturn(icon) - validRecommendationList = listOf( - mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem - ) + validRecommendationList = + listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem) whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE) whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA) whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList) @@ -194,8 +227,11 @@ class MediaDataManagerTest : SysuiTestCase() { fun tearDown() { session.release() mediaDataManager.destroy() - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, originalSmartspaceSetting) + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + originalSmartspaceSetting + ) } @Test @@ -212,21 +248,36 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testSetTimedOut_resume_dismissesMedia() { // WHEN resume controls are present, and time out - val desc = MediaDescription.Builder().run { - setTitle(SESSION_TITLE) - build() - } - mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken, - APP_NAME, pendingIntent, PACKAGE_NAME) + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) backgroundExecutor.runAllReady() foregroundExecutor.runAllReady() - verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor), - eq(true), eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true) - verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), - eq(mediaDataCaptor.value.instanceId)) + verify(logger) + .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId)) // THEN it is removed and listeners are informed foregroundExecutor.advanceClockToLast() @@ -243,8 +294,13 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testOnMetaDataLoaded_callsListener() { addNotificationAndLoad() - verify(logger).logActiveMediaAdded(anyInt(), eq(PACKAGE_NAME), - eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_LOCAL)) + verify(logger) + .logActiveMediaAdded( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId), + eq(MediaData.PLAYBACK_LOCAL) + ) } @Test @@ -255,56 +311,85 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value!!.active).isTrue() } @Test fun testOnNotificationAdded_isRcn_markedRemote() { - val rcn = SbnBuilder().run { - setPkg(SYSTEM_PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { - setMediaSession(session.sessionToken) - setRemotePlaybackInfo("Remote device", 0, null) - }) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setRemotePlaybackInfo("Remote device", 0, null) + } + ) + } + build() } - build() - } mediaDataManager.onNotificationAdded(KEY, rcn) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) - assertThat(mediaDataCaptor.value!!.playbackLocation).isEqualTo( - MediaData.PLAYBACK_CAST_REMOTE) - verify(logger).logActiveMediaAdded(anyInt(), eq(SYSTEM_PACKAGE_NAME), - eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_CAST_REMOTE)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.playbackLocation) + .isEqualTo(MediaData.PLAYBACK_CAST_REMOTE) + verify(logger) + .logActiveMediaAdded( + anyInt(), + eq(SYSTEM_PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId), + eq(MediaData.PLAYBACK_CAST_REMOTE) + ) } @Test fun testOnNotificationAdded_hasSubstituteName_isUsed() { val subName = "Substitute Name" - val notif = SbnBuilder().run { - modifyNotification(context).also { - it.extras = Bundle().apply { - putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName) + val notif = + SbnBuilder().run { + modifyNotification(context).also { + it.extras = + Bundle().apply { + putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName) + } + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) } - it.setStyle(MediaStyle().apply { - setMediaSession(session.sessionToken) - }) + build() } - build() - } mediaDataManager.onNotificationAdded(KEY, notif) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value!!.app).isEqualTo(subName) } @@ -314,17 +399,18 @@ class MediaDataManagerTest : SysuiTestCase() { val bundle = Bundle() // wrong data type bundle.putParcelable(Notification.EXTRA_MEDIA_SESSION, Bundle()) - val rcn = SbnBuilder().run { - setPkg(SYSTEM_PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.addExtras(bundle) - it.setStyle(MediaStyle().apply { - setRemotePlaybackInfo("Remote device", 0, null) - }) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.addExtras(bundle) + it.setStyle( + MediaStyle().apply { setRemotePlaybackInfo("Remote device", 0, null) } + ) + } + build() } - build() - } mediaDataManager.loadMediaDataInBg(KEY, rcn, null) // no crash even though the data structure is incorrect @@ -335,18 +421,21 @@ class MediaDataManagerTest : SysuiTestCase() { val bundle = Bundle() // wrong data type bundle.putParcelable(Notification.EXTRA_MEDIA_REMOTE_INTENT, Bundle()) - val rcn = SbnBuilder().run { - setPkg(SYSTEM_PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.addExtras(bundle) - it.setStyle(MediaStyle().apply { - setMediaSession(session.sessionToken) - setRemotePlaybackInfo("Remote device", 0, null) - }) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.addExtras(bundle) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setRemotePlaybackInfo("Remote device", 0, null) + } + ) + } + build() } - build() - } mediaDataManager.loadMediaDataInBg(KEY, rcn, null) // no crash even though the data structure is incorrect @@ -373,8 +462,14 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationRemoved(KEY) // THEN the media data indicates that it is for resumption verify(listener) - .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.resumption).isTrue() assertThat(mediaDataCaptor.value.isPlaying).isFalse() verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) @@ -389,8 +484,14 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(backgroundExecutor.runAllReady()).isEqualTo(2) assertThat(foregroundExecutor.runAllReady()).isEqualTo(2) verify(listener) - .onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) val data = mediaDataCaptor.value assertThat(data.resumption).isFalse() val resumableData = data.copy(resumeAction = Runnable {}) @@ -401,8 +502,14 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationRemoved(KEY) // THEN the data is for resumption and the key is migrated to the package name verify(listener) - .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.resumption).isTrue() verify(listener, never()).onMediaDataRemoved(eq(KEY)) // WHEN the second is removed @@ -410,8 +517,13 @@ class MediaDataManagerTest : SysuiTestCase() { // THEN the data is for resumption and the second key is removed verify(listener) .onMediaDataLoaded( - eq(PACKAGE_NAME), eq(PACKAGE_NAME), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + eq(PACKAGE_NAME), + eq(PACKAGE_NAME), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.resumption).isTrue() verify(listener).onMediaDataRemoved(eq(KEY_2)) } @@ -420,15 +532,20 @@ class MediaDataManagerTest : SysuiTestCase() { fun testOnNotificationRemoved_withResumption_butNotLocal() { // GIVEN that the manager has a notification with a resume action, but is not local whenever(controller.metadata).thenReturn(metadataBuilder.build()) - whenever(playbackInfo.playbackType).thenReturn( - MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) addNotificationAndLoad() val data = mediaDataCaptor.value - val dataRemoteWithResume = data.copy(resumeAction = Runnable {}, - playbackLocation = MediaData.PLAYBACK_CAST_LOCAL) + val dataRemoteWithResume = + data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL) mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume) - verify(logger).logActiveMediaAdded(anyInt(), eq(PACKAGE_NAME), - eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_CAST_LOCAL)) + verify(logger) + .logActiveMediaAdded( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId), + eq(MediaData.PLAYBACK_CAST_LOCAL) + ) // WHEN the notification is removed mediaDataManager.onNotificationRemoved(KEY) @@ -440,19 +557,33 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testAddResumptionControls() { // WHEN resumption controls are added - val desc = MediaDescription.Builder().run { - setTitle(SESSION_TITLE) - build() - } + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } val currentTime = clock.elapsedRealtime() - mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken, - APP_NAME, pendingIntent, PACKAGE_NAME) + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) // THEN the media data indicates that it is for resumption verify(listener) - .onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) val data = mediaDataCaptor.value assertThat(data.resumption).isTrue() assertThat(data.song).isEqualTo(SESSION_TITLE) @@ -466,16 +597,31 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testResumptionDisabled_dismissesResumeControls() { // WHEN there are resume controls and resumption is switched off - val desc = MediaDescription.Builder().run { - setTitle(SESSION_TITLE) - build() - } - mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken, - APP_NAME, pendingIntent, PACKAGE_NAME) + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor), - eq(true), eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) val data = mediaDataCaptor.value mediaDataManager.setMediaResumptionEnabled(false) @@ -508,23 +654,30 @@ class MediaDataManagerTest : SysuiTestCase() { fun testBadArtwork_doesNotUse() { // WHEN notification has a too-small artwork val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) - val notif = SbnBuilder().run { - setPkg(PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) - it.setLargeIcon(artwork) + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setLargeIcon(artwork) + } + build() } - build() - } mediaDataManager.onNotificationAdded(KEY, notif) // THEN it still loads assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) verify(listener) - .onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) } @Test @@ -533,18 +686,23 @@ class MediaDataManagerTest : SysuiTestCase() { verify(logger).getNewInstanceId() val instanceId = instanceIdSequence.lastInstanceId - verify(listener).onSmartspaceMediaDataLoaded( - eq(KEY_MEDIA_SMARTSPACE), - eq(SmartspaceMediaData( - targetId = KEY_MEDIA_SMARTSPACE, - isActive = true, - packageName = PACKAGE_NAME, - cardAction = mediaSmartspaceBaseAction, - recommendations = validRecommendationList, - dismissIntent = DISMISS_INTENT, - headphoneConnectionTimeMillis = 1234L, - instanceId = InstanceId.fakeInstanceId(instanceId))), - eq(false)) + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + packageName = PACKAGE_NAME, + cardAction = mediaSmartspaceBaseAction, + recommendations = validRecommendationList, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = 1234L, + instanceId = InstanceId.fakeInstanceId(instanceId) + ) + ), + eq(false) + ) } @Test @@ -554,23 +712,29 @@ class MediaDataManagerTest : SysuiTestCase() { verify(logger).getNewInstanceId() val instanceId = instanceIdSequence.lastInstanceId - verify(listener).onSmartspaceMediaDataLoaded( - eq(KEY_MEDIA_SMARTSPACE), - eq(EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = KEY_MEDIA_SMARTSPACE, - isActive = true, - dismissIntent = DISMISS_INTENT, - headphoneConnectionTimeMillis = 1234L, - instanceId = InstanceId.fakeInstanceId(instanceId))), - eq(false)) + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = 1234L, + instanceId = InstanceId.fakeInstanceId(instanceId) + ) + ), + eq(false) + ) } @Test fun testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListener() { - val recommendationExtras = Bundle().apply { - putString("package_name", PACKAGE_NAME) - putParcelable("dismiss_intent", null) - } + val recommendationExtras = + Bundle().apply { + putString("package_name", PACKAGE_NAME) + putParcelable("dismiss_intent", null) + } whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras) whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction) whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf()) @@ -579,15 +743,20 @@ class MediaDataManagerTest : SysuiTestCase() { verify(logger).getNewInstanceId() val instanceId = instanceIdSequence.lastInstanceId - verify(listener).onSmartspaceMediaDataLoaded( - eq(KEY_MEDIA_SMARTSPACE), - eq(EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = KEY_MEDIA_SMARTSPACE, - isActive = true, - dismissIntent = null, - headphoneConnectionTimeMillis = 1234L, - instanceId = InstanceId.fakeInstanceId(instanceId))), - eq(false)) + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + dismissIntent = null, + headphoneConnectionTimeMillis = 1234L, + instanceId = InstanceId.fakeInstanceId(instanceId) + ) + ), + eq(false) + ) } @Test @@ -595,7 +764,7 @@ class MediaDataManagerTest : SysuiTestCase() { smartspaceMediaDataProvider.onTargetsAvailable(listOf()) verify(logger, never()).getNewInstanceId() verify(listener, never()) - .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) + .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) } @Ignore("b/233283726") @@ -615,15 +784,18 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() { // WHEN media recommendation setting is off - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0) + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 0 + ) tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0") smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) // THEN smartspace signal is ignored verify(listener, never()) - .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) + .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) } @Ignore("b/229838140") @@ -631,12 +803,15 @@ class MediaDataManagerTest : SysuiTestCase() { fun testMediaRecommendationDisabled_removesSmartspaceData() { // GIVEN a media recommendation card is present smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) - verify(listener).onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), - anyBoolean()) + verify(listener) + .onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), anyBoolean()) // WHEN the media recommendation setting is turned off - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0) + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 0 + ) tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0") // THEN listeners are notified @@ -665,8 +840,15 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.setTimedOut(KEY, true, true) // THEN the last active time is not changed - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime) } @@ -687,8 +869,14 @@ class MediaDataManagerTest : SysuiTestCase() { // THEN the last active time is not changed verify(listener) - .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.resumption).isTrue() assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime) @@ -700,17 +888,20 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testTooManyCompactActions_isTruncated() { // GIVEN a notification where too many compact actions were specified - val notif = SbnBuilder().run { - setPkg(PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { - setMediaSession(session.sessionToken) - setShowActionsInCompactView(0, 1, 2, 3, 4) - }) + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setShowActionsInCompactView(0, 1, 2, 3, 4) + } + ) + } + build() } - build() - } // WHEN the notification is loaded mediaDataManager.onNotificationAdded(KEY, notif) @@ -718,29 +909,35 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) // THEN only the first MAX_COMPACT_ACTIONS are actually set - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) - assertThat(mediaDataCaptor.value.actionsToShowInCompact.size).isEqualTo( - MediaDataManager.MAX_COMPACT_ACTIONS) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.actionsToShowInCompact.size) + .isEqualTo(MediaDataManager.MAX_COMPACT_ACTIONS) } @Test fun testTooManyNotificationActions_isTruncated() { // GIVEN a notification where too many notification actions are added val action = Notification.Action(R.drawable.ic_android, "action", null) - val notif = SbnBuilder().run { - setPkg(PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { - setMediaSession(session.sessionToken) - }) - for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) { - it.addAction(action) + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) { + it.addAction(action) + } } + build() } - build() - } // WHEN the notification is loaded mediaDataManager.onNotificationAdded(KEY, notif) @@ -748,10 +945,17 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) // THEN only the first MAX_NOTIFICATION_ACTIONS are actually included - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) - assertThat(mediaDataCaptor.value.actions.size).isEqualTo( - MediaDataManager.MAX_NOTIFICATION_ACTIONS) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.actions.size) + .isEqualTo(MediaDataManager.MAX_NOTIFICATION_ACTIONS) } @Test @@ -760,21 +964,29 @@ class MediaDataManagerTest : SysuiTestCase() { whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) whenever(controller.playbackState).thenReturn(null) - val notifWithAction = SbnBuilder().run { - setPkg(PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) - it.addAction(android.R.drawable.ic_media_play, desc, null) + val notifWithAction = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.addAction(android.R.drawable.ic_media_play, desc, null) + } + build() } - build() - } mediaDataManager.onNotificationAdded(KEY, notifWithAction) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value!!.semanticActions).isNull() assertThat(mediaDataCaptor.value!!.actions).hasSize(1) @@ -785,11 +997,11 @@ class MediaDataManagerTest : SysuiTestCase() { fun testPlaybackActions_hasPrevNext() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) - val stateActions = PlaybackState.ACTION_PLAY or + val stateActions = + PlaybackState.ACTION_PLAY or PlaybackState.ACTION_SKIP_TO_PREVIOUS or PlaybackState.ACTION_SKIP_TO_NEXT - val stateBuilder = PlaybackState.Builder() - .setActions(stateActions) + val stateBuilder = PlaybackState.Builder().setActions(stateActions) customDesc.forEach { stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause) } @@ -801,20 +1013,20 @@ class MediaDataManagerTest : SysuiTestCase() { val actions = mediaDataCaptor.value!!.semanticActions!! assertThat(actions.playOrPause).isNotNull() - assertThat(actions.playOrPause!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_play)) + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) actions.playOrPause!!.action!!.run() verify(transportControls).play() assertThat(actions.prevOrCustom).isNotNull() - assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_prev)) + assertThat(actions.prevOrCustom!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_prev)) actions.prevOrCustom!!.action!!.run() verify(transportControls).skipToPrevious() assertThat(actions.nextOrCustom).isNotNull() - assertThat(actions.nextOrCustom!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_next)) + assertThat(actions.nextOrCustom!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_next)) actions.nextOrCustom!!.action!!.run() verify(transportControls).skipToNext() @@ -830,8 +1042,7 @@ class MediaDataManagerTest : SysuiTestCase() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5") whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) val stateActions = PlaybackState.ACTION_PLAY - val stateBuilder = PlaybackState.Builder() - .setActions(stateActions) + val stateBuilder = PlaybackState.Builder().setActions(stateActions) customDesc.forEach { stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause) } @@ -843,8 +1054,8 @@ class MediaDataManagerTest : SysuiTestCase() { val actions = mediaDataCaptor.value!!.semanticActions!! assertThat(actions.playOrPause).isNotNull() - assertThat(actions.playOrPause!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_play)) + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) assertThat(actions.prevOrCustom).isNotNull() assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo(customDesc[0]) @@ -863,7 +1074,8 @@ class MediaDataManagerTest : SysuiTestCase() { fun testPlaybackActions_connecting() { whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) val stateActions = PlaybackState.ACTION_PLAY - val stateBuilder = PlaybackState.Builder() + val stateBuilder = + PlaybackState.Builder() .setState(PlaybackState.STATE_BUFFERING, 0, 10f) .setActions(stateActions) whenever(controller.playbackState).thenReturn(stateBuilder.build()) @@ -874,8 +1086,8 @@ class MediaDataManagerTest : SysuiTestCase() { val actions = mediaDataCaptor.value!!.semanticActions!! assertThat(actions.playOrPause).isNotNull() - assertThat(actions.playOrPause!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_connecting)) + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_connecting)) } @Test @@ -883,15 +1095,15 @@ class MediaDataManagerTest : SysuiTestCase() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) val stateActions = PlaybackState.ACTION_PLAY - val stateBuilder = PlaybackState.Builder() - .setActions(stateActions) + val stateBuilder = PlaybackState.Builder().setActions(stateActions) customDesc.forEach { stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause) } - val extras = Bundle().apply { - putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) - putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) - } + val extras = + Bundle().apply { + putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) + putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) + } whenever(controller.playbackState).thenReturn(stateBuilder.build()) whenever(controller.extras).thenReturn(extras) @@ -901,8 +1113,8 @@ class MediaDataManagerTest : SysuiTestCase() { val actions = mediaDataCaptor.value!!.semanticActions!! assertThat(actions.playOrPause).isNotNull() - assertThat(actions.playOrPause!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_play)) + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) assertThat(actions.prevOrCustom).isNull() assertThat(actions.nextOrCustom).isNull() @@ -930,8 +1142,8 @@ class MediaDataManagerTest : SysuiTestCase() { val actions = mediaDataCaptor.value!!.semanticActions!! assertThat(actions.playOrPause).isNotNull() - assertThat(actions.playOrPause!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_play)) + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) actions.playOrPause!!.action!!.run() verify(transportControls).play() } @@ -944,30 +1156,43 @@ class MediaDataManagerTest : SysuiTestCase() { // Location is updated to local cast whenever(controller.metadata).thenReturn(metadataBuilder.build()) - whenever(playbackInfo.playbackType).thenReturn( - MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) addNotificationAndLoad() - verify(logger).logPlaybackLocationChange(anyInt(), eq(PACKAGE_NAME), - eq(instanceId), eq(MediaData.PLAYBACK_CAST_LOCAL)) + verify(logger) + .logPlaybackLocationChange( + anyInt(), + eq(PACKAGE_NAME), + eq(instanceId), + eq(MediaData.PLAYBACK_CAST_LOCAL) + ) // update to remote cast - val rcn = SbnBuilder().run { - setPkg(SYSTEM_PACKAGE_NAME) // System package - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { - setMediaSession(session.sessionToken) - setRemotePlaybackInfo("Remote device", 0, null) - }) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) // System package + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setRemotePlaybackInfo("Remote device", 0, null) + } + ) + } + build() } - build() - } mediaDataManager.onNotificationAdded(KEY, rcn) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(logger).logPlaybackLocationChange(anyInt(), eq(SYSTEM_PACKAGE_NAME), - eq(instanceId), eq(MediaData.PLAYBACK_CAST_REMOTE)) + verify(logger) + .logPlaybackLocationChange( + anyInt(), + eq(SYSTEM_PACKAGE_NAME), + eq(instanceId), + eq(MediaData.PLAYBACK_CAST_REMOTE) + ) } @Test @@ -977,14 +1202,19 @@ class MediaDataManagerTest : SysuiTestCase() { verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) // Callback gets an updated state - val state = PlaybackState.Builder() - .setState(PlaybackState.STATE_PLAYING, 0L, 1f) - .build() + val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() callbackCaptor.value.invoke(KEY, state) // Listener is notified of updated state - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), - capture(mediaDataCaptor), eq(true), eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.isPlaying).isTrue() } @@ -996,8 +1226,8 @@ class MediaDataManagerTest : SysuiTestCase() { // No media added with this key callbackCaptor.value.invoke(KEY, state) - verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), - anyBoolean()) + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } @Test @@ -1015,35 +1245,42 @@ class MediaDataManagerTest : SysuiTestCase() { // Then no changes are made callbackCaptor.value.invoke(KEY, state) - verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), - anyBoolean()) + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } @Test fun testPlaybackState_PauseWhenFlagTrue_keyExists_callsListener() { whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) - val state = PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 1f) - .build() + val state = PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 1f).build() whenever(controller.playbackState).thenReturn(state) addNotificationAndLoad() verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) callbackCaptor.value.invoke(KEY, state) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), - capture(mediaDataCaptor), eq(true), eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.isPlaying).isFalse() assertThat(mediaDataCaptor.value.semanticActions).isNotNull() } @Test fun testPlaybackState_PauseStateAfterAddingResumption_keyExists_callsListener() { - val desc = MediaDescription.Builder().run { - setTitle(SESSION_TITLE) - build() - } - val state = PlaybackState.Builder() + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + val state = + PlaybackState.Builder() .setState(PlaybackState.STATE_PAUSED, 0L, 1f) .setActions(PlaybackState.ACTION_PLAY_PAUSE) .build() @@ -1051,13 +1288,13 @@ class MediaDataManagerTest : SysuiTestCase() { // Add resumption controls in order to have semantic actions. // To make sure that they are not null after changing state. mediaDataManager.addResumptionControls( - USER_ID, - desc, - Runnable {}, - session.sessionToken, - APP_NAME, - pendingIntent, - PACKAGE_NAME + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME ) backgroundExecutor.runAllReady() foregroundExecutor.runAllReady() @@ -1066,14 +1303,14 @@ class MediaDataManagerTest : SysuiTestCase() { callbackCaptor.value.invoke(PACKAGE_NAME, state) verify(listener) - .onMediaDataLoaded( - eq(PACKAGE_NAME), - eq(PACKAGE_NAME), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false) - ) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(PACKAGE_NAME), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.isPlaying).isFalse() assertThat(mediaDataCaptor.value.semanticActions).isNotNull() } @@ -1081,7 +1318,8 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testPlaybackStateNull_Pause_keyExists_callsListener() { whenever(controller.playbackState).thenReturn(null) - val state = PlaybackState.Builder() + val state = + PlaybackState.Builder() .setState(PlaybackState.STATE_PAUSED, 0L, 1f) .setActions(PlaybackState.ACTION_PLAY_PAUSE) .build() @@ -1090,20 +1328,32 @@ class MediaDataManagerTest : SysuiTestCase() { verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) callbackCaptor.value.invoke(KEY, state) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), - capture(mediaDataCaptor), eq(true), eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.isPlaying).isFalse() assertThat(mediaDataCaptor.value.semanticActions).isNull() } - /** - * Helper function to add a media notification and capture the resulting MediaData - */ + /** Helper function to add a media notification and capture the resulting MediaData */ private fun addNotificationAndLoad() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt index 121c8946d164..a45e9d9fcacf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.bluetooth.BluetoothLeBroadcast import android.bluetooth.BluetoothLeBroadcastMetadata @@ -37,6 +37,10 @@ import com.android.settingslib.media.MediaDevice import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaDeviceData +import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory import com.android.systemui.statusbar.policy.ConfigurationController @@ -109,7 +113,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fakeFgExecutor = FakeExecutor(FakeSystemClock()) fakeBgExecutor = FakeExecutor(FakeSystemClock()) localBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager::class.java) - manager = MediaDeviceManager( + manager = + MediaDeviceManager( context, controllerFactory, lmmFactory, @@ -120,7 +125,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fakeFgExecutor, fakeBgExecutor, dumpster - ) + ) manager.addListener(listener) // Configure mocks. @@ -134,11 +139,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() { // Create a media sesssion and notification for testing. session = MediaSession(context, SESSION_KEY) - mediaData = MediaTestUtils.emptyMediaData.copy( - packageName = PACKAGE, - token = session.sessionToken) - whenever(controllerFactory.create(session.sessionToken)) - .thenReturn(controller) + mediaData = + MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, token = session.sessionToken) + whenever(controllerFactory.create(session.sessionToken)).thenReturn(controller) setupLeAudioConfiguration(false) } @@ -354,7 +357,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() { val deviceCallback = captureCallback() // First set a non-null about-to-connect device deviceCallback.onAboutToConnectDeviceAdded( - "fakeAddress", "AboutToConnectDeviceName", mock(Drawable::class.java) + "fakeAddress", + "AboutToConnectDeviceName", + mock(Drawable::class.java) ) // Run and reset the executors and listeners so we only focus on new events. fakeBgExecutor.runAllReady() @@ -583,8 +588,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun testRemotePlaybackDeviceOverride() { whenever(route.name).thenReturn(DEVICE_NAME) - val deviceData = MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null, - showBroadcastButton = false) + val deviceData = + MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null, showBroadcastButton = false) val mediaDataWithDevice = mediaData.copy(device = deviceData) // GIVEN media data that already has a device set @@ -613,8 +618,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { val data = captureDeviceData(KEY) assertThat(data.showBroadcastButton).isTrue() assertThat(data.enabled).isTrue() - assertThat(data.name).isEqualTo(context.getString( - R.string.broadcasting_description_is_broadcasting)) + assertThat(data.name) + .isEqualTo(context.getString(R.string.broadcasting_description_is_broadcasting)) } @Test @@ -655,20 +660,21 @@ public class MediaDeviceManagerTest : SysuiTestCase() { } fun setupBroadcastCallback(): BluetoothLeBroadcast.Callback { - val callback: BluetoothLeBroadcast.Callback = object : BluetoothLeBroadcast.Callback { - override fun onBroadcastStarted(reason: Int, broadcastId: Int) {} - override fun onBroadcastStartFailed(reason: Int) {} - override fun onBroadcastStopped(reason: Int, broadcastId: Int) {} - override fun onBroadcastStopFailed(reason: Int) {} - override fun onPlaybackStarted(reason: Int, broadcastId: Int) {} - override fun onPlaybackStopped(reason: Int, broadcastId: Int) {} - override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {} - override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {} - override fun onBroadcastMetadataChanged( - broadcastId: Int, - metadata: BluetoothLeBroadcastMetadata - ) {} - } + val callback: BluetoothLeBroadcast.Callback = + object : BluetoothLeBroadcast.Callback { + override fun onBroadcastStarted(reason: Int, broadcastId: Int) {} + override fun onBroadcastStartFailed(reason: Int) {} + override fun onBroadcastStopped(reason: Int, broadcastId: Int) {} + override fun onBroadcastStopFailed(reason: Int) {} + override fun onPlaybackStarted(reason: Int, broadcastId: Int) {} + override fun onPlaybackStopped(reason: Int, broadcastId: Int) {} + override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {} + override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {} + override fun onBroadcastMetadataChanged( + broadcastId: Int, + metadata: BluetoothLeBroadcastMetadata + ) {} + } bluetoothLeBroadcast.registerCallback(fakeFgExecutor, callback) return callback @@ -677,7 +683,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fun setupLeAudioConfiguration(isLeAudio: Boolean) { whenever(localBluetoothManager.profileManager).thenReturn(localBluetoothProfileManager) whenever(localBluetoothProfileManager.leAudioBroadcastProfile) - .thenReturn(localBluetoothLeBroadcast) + .thenReturn(localBluetoothLeBroadcast) whenever(localBluetoothLeBroadcast.isEnabled(any())).thenReturn(isLeAudio) whenever(localBluetoothLeBroadcast.appSourceName).thenReturn(BROADCAST_APP_NAME) } @@ -685,7 +691,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fun setupBroadcastPackage(currentName: String) { whenever(lmm.packageName).thenReturn(PACKAGE) whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt())) - .thenReturn(applicationInfo) + .thenReturn(applicationInfo) whenever(packageManager.getApplicationLabel(applicationInfo)).thenReturn(currentName) context.setMockPackageManager(packageManager) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilterTest.kt index 558645377936..3099609d42f0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilterTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.media.session.MediaController import android.media.session.MediaController.PlaybackInfo @@ -23,12 +23,12 @@ import android.media.session.MediaSessionManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest - import com.android.systemui.SysuiTestCase +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock - import org.junit.After import org.junit.Before import org.junit.Rule @@ -42,17 +42,15 @@ import org.mockito.Mockito.any import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit private const val PACKAGE = "PKG" private const val KEY = "TEST_KEY" private const val NOTIF_KEY = "TEST_KEY" -private val info = MediaTestUtils.emptyMediaData.copy( - packageName = PACKAGE, - notificationKey = NOTIF_KEY -) +private val info = + MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, notificationKey = NOTIF_KEY) @SmallTest @RunWith(AndroidTestingRunner::class) @@ -139,10 +137,10 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { // Capture listener bgExecutor.runAllReady() - val listenerCaptor = ArgumentCaptor.forClass( - MediaSessionManager.OnActiveSessionsChangedListener::class.java) - verify(mediaSessionManager).addOnActiveSessionsChangedListener( - listenerCaptor.capture(), any()) + val listenerCaptor = + ArgumentCaptor.forClass(MediaSessionManager.OnActiveSessionsChangedListener::class.java) + verify(mediaSessionManager) + .addOnActiveSessionsChangedListener(listenerCaptor.capture(), any()) sessionListener = listenerCaptor.value filter.addListener(mediaListener) @@ -161,8 +159,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { filter.onMediaDataLoaded(KEY, null, mediaData1) bgExecutor.runAllReady() fgExecutor.runAllReady() - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) } @Test @@ -184,8 +182,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) } @Test @@ -214,8 +212,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) } @Test @@ -230,15 +228,22 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) // WHEN a loaded event is received that matches the local session filter.onMediaDataLoaded(KEY, null, mediaData2) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is filtered - verify(mediaListener, never()).onMediaDataLoaded( - eq(KEY), eq(null), eq(mediaData2), anyBoolean(), anyInt(), anyBoolean()) + verify(mediaListener, never()) + .onMediaDataLoaded( + eq(KEY), + eq(null), + eq(mediaData2), + anyBoolean(), + anyInt(), + anyBoolean() + ) } @Test @@ -254,8 +259,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { fgExecutor.runAllReady() // THEN the event is not filtered because there isn't a notification for the remote // session. - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) } @Test @@ -272,16 +277,22 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) // WHEN a loaded event is received that matches the local session filter.onMediaDataLoaded(key2, null, mediaData2) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is filtered verify(mediaListener, never()) - .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean(), - anyInt(), anyBoolean()) + .onMediaDataLoaded( + eq(key2), + eq(null), + eq(mediaData2), + anyBoolean(), + anyInt(), + anyBoolean() + ) // AND there should be a removed event for key2 verify(mediaListener).onMediaDataRemoved(eq(key2)) } @@ -300,15 +311,15 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) // WHEN a loaded event is received that matches the remote session filter.onMediaDataLoaded(key2, null, mediaData2) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true), eq(0), eq(false)) } @Test @@ -324,15 +335,15 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) // WHEN a loaded event is received that matches the local session filter.onMediaDataLoaded(KEY, null, mediaData2) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true), eq(0), eq(false)) } @Test @@ -350,8 +361,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) } @Test @@ -373,8 +384,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the key migration event is fired - verify(mediaListener).onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true), eq(0), eq(false)) } @Test @@ -404,14 +415,20 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { fgExecutor.runAllReady() // THEN the key migration event is filtered verify(mediaListener, never()) - .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean(), - anyInt(), anyBoolean()) + .onMediaDataLoaded( + eq(key2), + eq(null), + eq(mediaData2), + anyBoolean(), + anyInt(), + anyBoolean() + ) // WHEN a loaded event is received that matches the remote session filter.onMediaDataLoaded(key2, null, mediaData1) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the key migration event is fired - verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt index 823d4ae8c447..344dffafb448 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.media.MediaMetadata import android.media.session.MediaController @@ -23,6 +23,9 @@ import android.media.session.PlaybackState import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.util.concurrency.FakeExecutor @@ -41,11 +44,11 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito -import org.mockito.Mockito.`when` import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit private const val KEY = "KEY" @@ -70,7 +73,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback> - @Captor private lateinit var dozingCallbackCaptor: + @Captor + private lateinit var dozingCallbackCaptor: ArgumentCaptor<StatusBarStateController.StateListener> @JvmField @Rule val mockito = MockitoJUnit.rule() private lateinit var metadataBuilder: MediaMetadata.Builder @@ -85,36 +89,41 @@ class MediaTimeoutListenerTest : SysuiTestCase() { fun setup() { `when`(mediaControllerFactory.create(any())).thenReturn(mediaController) executor = FakeExecutor(clock) - mediaTimeoutListener = MediaTimeoutListener( - mediaControllerFactory, - executor, - logger, - statusBarStateController, - clock - ) + mediaTimeoutListener = + MediaTimeoutListener( + mediaControllerFactory, + executor, + logger, + statusBarStateController, + clock + ) mediaTimeoutListener.timeoutCallback = timeoutCallback mediaTimeoutListener.stateCallback = stateCallback // Create a media session and notification for testing. - metadataBuilder = MediaMetadata.Builder().apply { - putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) - putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) - } - playbackBuilder = PlaybackState.Builder().apply { - setState(PlaybackState.STATE_PAUSED, 6000L, 1f) - setActions(PlaybackState.ACTION_PLAY) - } - session = MediaSession(context, SESSION_KEY).apply { - setMetadata(metadataBuilder.build()) - setPlaybackState(playbackBuilder.build()) - } + metadataBuilder = + MediaMetadata.Builder().apply { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + } + playbackBuilder = + PlaybackState.Builder().apply { + setState(PlaybackState.STATE_PAUSED, 6000L, 1f) + setActions(PlaybackState.ACTION_PLAY) + } + session = + MediaSession(context, SESSION_KEY).apply { + setMetadata(metadataBuilder.build()) + setPlaybackState(playbackBuilder.build()) + } session.setActive(true) - mediaData = MediaTestUtils.emptyMediaData.copy( - app = PACKAGE, - packageName = PACKAGE, - token = session.sessionToken - ) + mediaData = + MediaTestUtils.emptyMediaData.copy( + app = PACKAGE, + packageName = PACKAGE, + token = session.sessionToken + ) resumeData = mediaData.copy(token = null, active = false, resumption = true) } @@ -212,8 +221,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we're registered testOnMediaDataLoaded_registersPlaybackListener() - mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) + mediaCallbackCaptor.value.onPlaybackStateChanged( + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() + ) assertThat(executor.numPending()).isEqualTo(1) assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT) } @@ -223,8 +233,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we have a pending timeout testOnPlaybackStateChanged_schedulesTimeout_whenPaused() - mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() - .setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()) + mediaCallbackCaptor.value.onPlaybackStateChanged( + PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build() + ) assertThat(executor.numPending()).isEqualTo(0) verify(logger).logTimeoutCancelled(eq(KEY), any()) } @@ -234,8 +245,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we have a pending timeout testOnPlaybackStateChanged_schedulesTimeout_whenPaused() - mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() - .setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()) + mediaCallbackCaptor.value.onPlaybackStateChanged( + PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build() + ) assertThat(executor.numPending()).isEqualTo(1) } @@ -329,9 +341,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnMediaDataLoaded_pausedToResume_updatesTimeout() { // WHEN regular media is paused - val pausedState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f) - .build() + val pausedState = + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() `when`(mediaController.playbackState).thenReturn(pausedState) mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) assertThat(executor.numPending()).isEqualTo(1) @@ -362,9 +373,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() { mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData) // AND that media is resumed - val playingState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f) - .build() + val playingState = + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() `when`(mediaController.playbackState).thenReturn(playingState) mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData) @@ -386,15 +396,11 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnMediaDataLoaded_playbackActionsChanged_noCallback() { // Load media data once - val pausedState = PlaybackState.Builder() - .setActions(PlaybackState.ACTION_PAUSE) - .build() + val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build() loadMediaDataWithPlaybackState(pausedState) // When media data is loaded again, with different actions - val playingState = PlaybackState.Builder() - .setActions(PlaybackState.ACTION_PLAY) - .build() + val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build() loadMediaDataWithPlaybackState(playingState) // Then the callback is not invoked @@ -404,15 +410,11 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnPlaybackStateChanged_playbackActionsChanged_sendsCallback() { // Load media data once - val pausedState = PlaybackState.Builder() - .setActions(PlaybackState.ACTION_PAUSE) - .build() + val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build() loadMediaDataWithPlaybackState(pausedState) // When the playback state changes, and has different actions - val playingState = PlaybackState.Builder() - .setActions(PlaybackState.ACTION_PLAY) - .build() + val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build() mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) // Then the callback is invoked @@ -421,24 +423,30 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnPlaybackStateChanged_differentCustomActions_sendsCallback() { - val customOne = PlaybackState.CustomAction.Builder( + val customOne = + PlaybackState.CustomAction.Builder( "ACTION_1", "custom action 1", - android.R.drawable.ic_media_ff) + android.R.drawable.ic_media_ff + ) .build() - val pausedState = PlaybackState.Builder() + val pausedState = + PlaybackState.Builder() .setActions(PlaybackState.ACTION_PAUSE) .addCustomAction(customOne) .build() loadMediaDataWithPlaybackState(pausedState) // When the playback state actions change - val customTwo = PlaybackState.CustomAction.Builder( - "ACTION_2", - "custom action 2", - android.R.drawable.ic_media_rew) + val customTwo = + PlaybackState.CustomAction.Builder( + "ACTION_2", + "custom action 2", + android.R.drawable.ic_media_rew + ) .build() - val pausedStateTwoActions = PlaybackState.Builder() + val pausedStateTwoActions = + PlaybackState.Builder() .setActions(PlaybackState.ACTION_PAUSE) .addCustomAction(customOne) .addCustomAction(customTwo) @@ -451,9 +459,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnPlaybackStateChanged_sameActions_noCallback() { - val stateWithActions = PlaybackState.Builder() - .setActions(PlaybackState.ACTION_PLAY) - .build() + val stateWithActions = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build() loadMediaDataWithPlaybackState(stateWithActions) // When the playback state updates with the same actions @@ -467,18 +473,20 @@ class MediaTimeoutListenerTest : SysuiTestCase() { fun testOnPlaybackStateChanged_sameCustomActions_noCallback() { val actionName = "custom action" val actionIcon = android.R.drawable.ic_media_ff - val customOne = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon) - .build() - val stateOne = PlaybackState.Builder() + val customOne = + PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build() + val stateOne = + PlaybackState.Builder() .setActions(PlaybackState.ACTION_PAUSE) .addCustomAction(customOne) .build() loadMediaDataWithPlaybackState(stateOne) // When the playback state is updated, but has the same actions - val customTwo = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon) - .build() - val stateTwo = PlaybackState.Builder() + val customTwo = + PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build() + val stateTwo = + PlaybackState.Builder() .setActions(PlaybackState.ACTION_PAUSE) .addCustomAction(customTwo) .build() @@ -491,15 +499,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnMediaDataLoaded_isPlayingChanged_noCallback() { // Load media data in paused state - val pausedState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f) - .build() + val pausedState = + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() loadMediaDataWithPlaybackState(pausedState) // When media data is loaded again but playing - val playingState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PLAYING, 0L, 1f) - .build() + val playingState = + PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() loadMediaDataWithPlaybackState(playingState) // Then the callback is not invoked @@ -509,15 +515,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnPlaybackStateChanged_isPlayingChanged_sendsCallback() { // Load media data in paused state - val pausedState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f) - .build() + val pausedState = + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() loadMediaDataWithPlaybackState(pausedState) // When the playback state changes to playing - val playingState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PLAYING, 0L, 1f) - .build() + val playingState = + PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) // Then the callback is invoked @@ -527,15 +531,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnPlaybackStateChanged_isPlayingSame_noCallback() { // Load media data in paused state - val pausedState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f) - .build() + val pausedState = + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() loadMediaDataWithPlaybackState(pausedState) // When the playback state is updated, but still not playing - val playingState = PlaybackState.Builder() - .setState(PlaybackState.STATE_STOPPED, 0L, 0f) - .build() + val playingState = + PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build() mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) // Then the callback is not invoked @@ -546,8 +548,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() { // When paused media is loaded testOnMediaDataLoaded_registersPlaybackListener() - mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) + mediaCallbackCaptor.value.onPlaybackStateChanged( + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() + ) verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) // And we doze past the scheduled timeout @@ -571,8 +574,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { val time = clock.currentTimeMillis() clock.setElapsedRealtime(time) testOnMediaDataLoaded_registersPlaybackListener() - mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) + mediaCallbackCaptor.value.onPlaybackStateChanged( + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() + ) verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) // And we doze, but not past the scheduled timeout diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt index 83168cb87dfe..84fdfd78e9fc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.resume import android.app.PendingIntent import android.content.ComponentName @@ -33,11 +33,16 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaDeviceData +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT import com.android.systemui.tuner.TunerService import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock -import org.junit.After import com.google.common.truth.Truth.assertThat +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -63,7 +68,9 @@ private const val MEDIA_PREFERENCES = "media_control_prefs" private const val RESUME_COMPONENTS = "package1/class1:package2/class2:package3/class3" private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() + private fun <T> eq(value: T): T = Mockito.eq(value) ?: value + private fun <T> any(): T = Mockito.any<T>() @SmallTest @@ -93,26 +100,32 @@ class MediaResumeListenerTest : SysuiTestCase() { private lateinit var resumeListener: MediaResumeListener private val clock = FakeSystemClock() - private var originalQsSetting = Settings.Global.getInt(context.contentResolver, - Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1) - private var originalResumeSetting = Settings.Secure.getInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RESUME, 0) + private var originalQsSetting = + Settings.Global.getInt( + context.contentResolver, + Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, + 1 + ) + private var originalResumeSetting = + Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0) @Before fun setup() { MockitoAnnotations.initMocks(this) - Settings.Global.putInt(context.contentResolver, - Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1) - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RESUME, 1) + Settings.Global.putInt( + context.contentResolver, + Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, + 1 + ) + Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1) whenever(resumeBrowserFactory.create(capture(callbackCaptor), any())) - .thenReturn(resumeBrowser) + .thenReturn(resumeBrowser) // resume components are stored in sharedpreferences whenever(mockContext.getSharedPreferences(eq(MEDIA_PREFERENCES), anyInt())) - .thenReturn(sharedPrefs) + .thenReturn(sharedPrefs) whenever(sharedPrefs.getString(any(), any())).thenReturn(RESUME_COMPONENTS) whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor) whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor) @@ -120,36 +133,59 @@ class MediaResumeListenerTest : SysuiTestCase() { whenever(mockContext.contentResolver).thenReturn(context.contentResolver) executor = FakeExecutor(clock) - resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, - tunerService, resumeBrowserFactory, dumpManager, clock) + resumeListener = + MediaResumeListener( + mockContext, + broadcastDispatcher, + executor, + tunerService, + resumeBrowserFactory, + dumpManager, + clock + ) resumeListener.setManager(mediaDataManager) mediaDataManager.addListener(resumeListener) - data = MediaTestUtils.emptyMediaData.copy( + data = + MediaTestUtils.emptyMediaData.copy( song = TITLE, packageName = PACKAGE_NAME, - token = token) + token = token + ) } @After fun tearDown() { - Settings.Global.putInt(context.contentResolver, - Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, originalQsSetting) - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RESUME, originalResumeSetting) + Settings.Global.putInt( + context.contentResolver, + Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, + originalQsSetting + ) + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RESUME, + originalResumeSetting + ) } @Test fun testWhenNoResumption_doesNothing() { - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RESUME, 0) + Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0) // When listener is created, we do NOT register a user change listener - val listener = MediaResumeListener(context, broadcastDispatcher, executor, tunerService, - resumeBrowserFactory, dumpManager, clock) + val listener = + MediaResumeListener( + context, + broadcastDispatcher, + executor, + tunerService, + resumeBrowserFactory, + dumpManager, + clock + ) listener.setManager(mediaDataManager) - verify(broadcastDispatcher, never()).registerReceiver(eq(listener.userChangeReceiver), - any(), any(), any(), anyInt(), any()) + verify(broadcastDispatcher, never()) + .registerReceiver(eq(listener.userChangeReceiver), any(), any(), any(), anyInt(), any()) // When data is loaded, we do NOT execute or update anything listener.onMediaDataLoaded(KEY, OLD_KEY, data) @@ -170,9 +206,7 @@ class MediaResumeListenerTest : SysuiTestCase() { fun testOnLoad_checksForResume_badService() { setUpMbsWithValidResolveInfo() - whenever(resumeBrowser.testConnection()).thenAnswer { - callbackCaptor.value.onError() - } + whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() } // When media data is loaded that has not been checked yet, and does not have a MBS resumeListener.onMediaDataLoaded(KEY, null, data) @@ -226,7 +260,7 @@ class MediaResumeListenerTest : SysuiTestCase() { // But we do not tell it to add new controls verify(mediaDataManager, never()) - .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any()) + .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any()) } @Test @@ -253,8 +287,15 @@ class MediaResumeListenerTest : SysuiTestCase() { // Make sure broadcast receiver is registered resumeListener.setManager(mediaDataManager) - verify(broadcastDispatcher).registerReceiver(eq(resumeListener.userChangeReceiver), - any(), any(), any(), anyInt(), any()) + verify(broadcastDispatcher) + .registerReceiver( + eq(resumeListener.userChangeReceiver), + any(), + any(), + any(), + anyInt(), + any() + ) // When we get an unlock event val intent = Intent(Intent.ACTION_USER_UNLOCKED) @@ -264,8 +305,8 @@ class MediaResumeListenerTest : SysuiTestCase() { verify(resumeBrowser, times(3)).findRecentMedia() // Then since the mock service found media, the manager should be informed - verify(mediaDataManager, times(3)).addResumptionControls(anyInt(), - any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) + verify(mediaDataManager, times(3)) + .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) } @Test @@ -304,12 +345,14 @@ class MediaResumeListenerTest : SysuiTestCase() { // Then we save an update with the current time verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor))) - componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex()) - .dropLastWhile { it.isEmpty() }.forEach { - val result = it.split("/") - assertThat(result.size).isEqualTo(3) - assertThat(result[2].toLong()).isEqualTo(currentTime) - } + componentCaptor.value + .split(ResumeMediaBrowser.DELIMITER.toRegex()) + .dropLastWhile { it.isEmpty() } + .forEach { + val result = it.split("/") + assertThat(result.size).isEqualTo(3) + assertThat(result[2].toLong()).isEqualTo(currentTime) + } verify(sharedPrefsEditor, times(1)).apply() } @@ -328,8 +371,16 @@ class MediaResumeListenerTest : SysuiTestCase() { val lastPlayed = clock.currentTimeMillis() val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:" whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString) - val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, - tunerService, resumeBrowserFactory, dumpManager, clock) + val resumeListener = + MediaResumeListener( + mockContext, + broadcastDispatcher, + executor, + tunerService, + resumeBrowserFactory, + dumpManager, + clock + ) resumeListener.setManager(mediaDataManager) mediaDataManager.addListener(resumeListener) @@ -339,8 +390,8 @@ class MediaResumeListenerTest : SysuiTestCase() { // We add its resume controls verify(resumeBrowser, times(1)).findRecentMedia() - verify(mediaDataManager, times(1)).addResumptionControls(anyInt(), - any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) + verify(mediaDataManager, times(1)) + .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) } @Test @@ -349,8 +400,16 @@ class MediaResumeListenerTest : SysuiTestCase() { val lastPlayed = clock.currentTimeMillis() - RESUME_MEDIA_TIMEOUT - 100 val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:" whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString) - val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, - tunerService, resumeBrowserFactory, dumpManager, clock) + val resumeListener = + MediaResumeListener( + mockContext, + broadcastDispatcher, + executor, + tunerService, + resumeBrowserFactory, + dumpManager, + clock + ) resumeListener.setManager(mediaDataManager) mediaDataManager.addListener(resumeListener) @@ -360,8 +419,8 @@ class MediaResumeListenerTest : SysuiTestCase() { // We do not try to add resume controls verify(resumeBrowser, times(0)).findRecentMedia() - verify(mediaDataManager, times(0)).addResumptionControls(anyInt(), - any(), any(), any(), any(), any(), any()) + verify(mediaDataManager, times(0)) + .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any()) } @Test @@ -380,8 +439,16 @@ class MediaResumeListenerTest : SysuiTestCase() { val lastPlayed = currentTime - 1000 val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:" whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString) - val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, - tunerService, resumeBrowserFactory, dumpManager, clock) + val resumeListener = + MediaResumeListener( + mockContext, + broadcastDispatcher, + executor, + tunerService, + resumeBrowserFactory, + dumpManager, + clock + ) resumeListener.setManager(mediaDataManager) mediaDataManager.addListener(resumeListener) @@ -391,12 +458,14 @@ class MediaResumeListenerTest : SysuiTestCase() { // Then we store the new lastPlayed time verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor))) - componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex()) - .dropLastWhile { it.isEmpty() }.forEach { - val result = it.split("/") - assertThat(result.size).isEqualTo(3) - assertThat(result[2].toLong()).isEqualTo(currentTime) - } + componentCaptor.value + .split(ResumeMediaBrowser.DELIMITER.toRegex()) + .dropLastWhile { it.isEmpty() } + .forEach { + val result = it.split("/") + assertThat(result.size).isEqualTo(3) + assertThat(result[2].toLong()).isEqualTo(currentTime) + } verify(sharedPrefsEditor, times(1)).apply() } @@ -417,9 +486,7 @@ class MediaResumeListenerTest : SysuiTestCase() { setUpMbsWithValidResolveInfo() // Set up mocks to return with an error - whenever(resumeBrowser.testConnection()).thenAnswer { - callbackCaptor.value.onError() - } + whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() } resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data) executor.runAllReady() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt index dafaa6b93696..a04cfd46588b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.resume import android.content.ComponentName import android.content.Context @@ -37,8 +37,8 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.reset import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations private const val PACKAGE_NAME = "package" private const val CLASS_NAME = "class" @@ -47,7 +47,9 @@ private const val MEDIA_ID = "media ID" private const val ROOT = "media browser root" private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() + private fun <T> eq(value: T): T = Mockito.eq(value) ?: value + private fun <T> any(): T = Mockito.any<T>() @SmallTest @@ -57,10 +59,8 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { private lateinit var resumeBrowser: TestableResumeMediaBrowser private val component = ComponentName(PACKAGE_NAME, CLASS_NAME) - private val description = MediaDescription.Builder() - .setTitle(TITLE) - .setMediaId(MEDIA_ID) - .build() + private val description = + MediaDescription.Builder().setTitle(TITLE).setMediaId(MEDIA_ID).build() @Mock lateinit var callback: ResumeMediaBrowser.Callback @Mock lateinit var listener: MediaResumeListener @@ -81,19 +81,20 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(browserFactory.create(any(), capture(connectionCallback), any())) - .thenReturn(browser) + .thenReturn(browser) whenever(mediaController.transportControls).thenReturn(transportControls) whenever(mediaController.sessionToken).thenReturn(token) - resumeBrowser = TestableResumeMediaBrowser( - context, - callback, - component, - browserFactory, - logger, - mediaController - ) + resumeBrowser = + TestableResumeMediaBrowser( + context, + callback, + component, + browserFactory, + logger, + mediaController + ) } @Test @@ -329,30 +330,20 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { verify(oldBrowser).disconnect() } - /** - * Helper function to mock a failed connection - */ + /** Helper function to mock a failed connection */ private fun setupBrowserFailed() { - whenever(browser.connect()).thenAnswer { - connectionCallback.value.onConnectionFailed() - } + whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnectionFailed() } } - /** - * Helper function to mock a successful connection only - */ + /** Helper function to mock a successful connection only */ private fun setupBrowserConnection() { - whenever(browser.connect()).thenAnswer { - connectionCallback.value.onConnected() - } + whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnected() } whenever(browser.isConnected()).thenReturn(true) whenever(browser.getRoot()).thenReturn(ROOT) whenever(browser.sessionToken).thenReturn(token) } - /** - * Helper function to mock a successful connection, but no media results - */ + /** Helper function to mock a successful connection, but no media results */ private fun setupBrowserConnectionNoResults() { setupBrowserConnection() whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer { @@ -360,9 +351,7 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { } } - /** - * Helper function to mock a successful connection, but no playable results - */ + /** Helper function to mock a successful connection, but no playable results */ private fun setupBrowserConnectionNotPlayable() { setupBrowserConnection() @@ -373,9 +362,7 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { } } - /** - * Helper function to mock a successful connection with playable media - */ + /** Helper function to mock a successful connection with playable media */ private fun setupBrowserConnectionValidMedia() { setupBrowserConnection() @@ -387,9 +374,7 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { } } - /** - * Override so media controller use is testable - */ + /** Override so media controller use is testable */ private class TestableResumeMediaBrowser( context: Context, callback: Callback, @@ -403,4 +388,4 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { return fakeController } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/AnimationBindHandlerTest.kt index e4cab1810822..99f56b16ab8b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/AnimationBindHandlerTest.kt @@ -14,26 +14,26 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui -import org.mockito.Mockito.`when` as whenever import android.graphics.drawable.Animatable2 import android.graphics.drawable.Drawable import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner import android.testing.TestableLooper import com.android.systemui.SysuiTestCase -import junit.framework.Assert.assertTrue import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.times import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit @SmallTest @@ -56,8 +56,7 @@ class AnimationBindHandlerTest : SysuiTestCase() { handler = AnimationBindHandler() } - @After - fun tearDown() {} + @After fun tearDown() {} @Test fun registerNoAnimations_executeCallbackImmediately() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt index f56d42ec3fb4..5bb74e5a31f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.animation.ValueAnimator import android.graphics.Color @@ -22,6 +22,8 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.media.controls.models.GutsViewHolder +import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.monet.ColorScheme import junit.framework.Assert.assertEquals import org.junit.After @@ -67,21 +69,18 @@ class ColorSchemeTransitionTest : SysuiTestCase() { animatingColorTransitionFactory = { _, _, _ -> mockAnimatingTransition } whenever(extractColor.invoke(colorScheme)).thenReturn(TARGET_COLOR) - colorSchemeTransition = ColorSchemeTransition( - context, mediaViewHolder, animatingColorTransitionFactory - ) + colorSchemeTransition = + ColorSchemeTransition(context, mediaViewHolder, animatingColorTransitionFactory) - colorTransition = object : AnimatingColorTransition( - DEFAULT_COLOR, extractColor, applyColor - ) { - override fun buildAnimator(): ValueAnimator { - return valueAnimator + colorTransition = + object : AnimatingColorTransition(DEFAULT_COLOR, extractColor, applyColor) { + override fun buildAnimator(): ValueAnimator { + return valueAnimator + } } - } } - @After - fun tearDown() {} + @After fun tearDown() {} @Test fun testColorTransition_nullColorScheme_keepsDefault() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerTest.kt index c41fac71a179..20260069c943 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.provider.Settings import android.test.suitebuilder.annotation.SmallTest @@ -48,17 +48,12 @@ import org.mockito.junit.MockitoJUnit @TestableLooper.RunWithLooper class KeyguardMediaControllerTest : SysuiTestCase() { - @Mock - private lateinit var mediaHost: MediaHost - @Mock - private lateinit var bypassController: KeyguardBypassController - @Mock - private lateinit var statusBarStateController: SysuiStatusBarStateController - @Mock - private lateinit var configurationController: ConfigurationController + @Mock private lateinit var mediaHost: MediaHost + @Mock private lateinit var bypassController: KeyguardBypassController + @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController + @Mock private lateinit var configurationController: ConfigurationController - @JvmField @Rule - val mockito = MockitoJUnit.rule() + @JvmField @Rule val mockito = MockitoJUnit.rule() private val mediaContainerView: MediaContainerView = MediaContainerView(context, null) private val hostView = UniqueObjectHostView(context) @@ -76,15 +71,16 @@ class KeyguardMediaControllerTest : SysuiTestCase() { hostView.layoutParams = FrameLayout.LayoutParams(100, 100) testableLooper = TestableLooper.get(this) fakeHandler = FakeHandler(testableLooper.looper) - keyguardMediaController = KeyguardMediaController( - mediaHost, - bypassController, - statusBarStateController, - context, - settings, - fakeHandler, - configurationController, - ) + keyguardMediaController = + KeyguardMediaController( + mediaHost, + bypassController, + statusBarStateController, + context, + settings, + fakeHandler, + configurationController, + ) keyguardMediaController.attachSinglePaneContainer(mediaContainerView) keyguardMediaController.useSplitShade = false } @@ -153,8 +149,10 @@ class KeyguardMediaControllerTest : SysuiTestCase() { keyguardMediaController.attachSplitShadeContainer(splitShadeContainer) keyguardMediaController.useSplitShade = true - assertTrue("HostView wasn't attached to the split pane container", - splitShadeContainer.childCount == 1) + assertTrue( + "HostView wasn't attached to the split pane container", + splitShadeContainer.childCount == 1 + ) } @Test @@ -162,8 +160,10 @@ class KeyguardMediaControllerTest : SysuiTestCase() { val splitShadeContainer = FrameLayout(context) keyguardMediaController.attachSplitShadeContainer(splitShadeContainer) - assertTrue("HostView wasn't attached to the single pane container", - mediaContainerView.childCount == 1) + assertTrue( + "HostView wasn't attached to the single pane container", + mediaContainerView.childCount == 1 + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt new file mode 100644 index 000000000000..c8e8943689c9 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt @@ -0,0 +1,645 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.ui + +import android.app.PendingIntent +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.logging.InstanceId +import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.PAGINATION_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER +import com.android.systemui.media.controls.ui.MediaHierarchyManager.Companion.LOCATION_QS +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener +import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.time.FakeSystemClock +import javax.inject.Provider +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue +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.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +private val DATA = MediaTestUtils.emptyMediaData + +private val SMARTSPACE_KEY = "smartspace" + +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidTestingRunner::class) +class MediaCarouselControllerTest : SysuiTestCase() { + + @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel> + @Mock lateinit var panel: MediaControlPanel + @Mock lateinit var visualStabilityProvider: VisualStabilityProvider + @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager + @Mock lateinit var mediaHostState: MediaHostState + @Mock lateinit var activityStarter: ActivityStarter + @Mock @Main private lateinit var executor: DelayableExecutor + @Mock lateinit var mediaDataManager: MediaDataManager + @Mock lateinit var configurationController: ConfigurationController + @Mock lateinit var falsingCollector: FalsingCollector + @Mock lateinit var falsingManager: FalsingManager + @Mock lateinit var dumpManager: DumpManager + @Mock lateinit var logger: MediaUiEventLogger + @Mock lateinit var debugLogger: MediaCarouselControllerLogger + @Mock lateinit var mediaViewController: MediaViewController + @Mock lateinit var smartspaceMediaData: SmartspaceMediaData + @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> + @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener> + + private val clock = FakeSystemClock() + private lateinit var mediaCarouselController: MediaCarouselController + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + mediaCarouselController = + MediaCarouselController( + context, + mediaControlPanelFactory, + visualStabilityProvider, + mediaHostStatesManager, + activityStarter, + clock, + executor, + mediaDataManager, + configurationController, + falsingCollector, + falsingManager, + dumpManager, + logger, + debugLogger + ) + verify(mediaDataManager).addListener(capture(listener)) + verify(visualStabilityProvider) + .addPersistentReorderingAllowedListener(capture(visualStabilityCallback)) + whenever(mediaControlPanelFactory.get()).thenReturn(panel) + whenever(panel.mediaViewController).thenReturn(mediaViewController) + whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData) + MediaPlayerData.clear() + } + + @Test + fun testPlayerOrdering() { + // Test values: key, data, last active time + val playingLocal = + Triple( + "playing local", + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ), + 4500L + ) + + val playingCast = + Triple( + "playing cast", + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, + resumption = false + ), + 5000L + ) + + val pausedLocal = + Triple( + "paused local", + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ), + 1000L + ) + + val pausedCast = + Triple( + "paused cast", + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, + resumption = false + ), + 2000L + ) + + val playingRcn = + Triple( + "playing RCN", + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, + resumption = false + ), + 5000L + ) + + val pausedRcn = + Triple( + "paused RCN", + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, + resumption = false + ), + 5000L + ) + + val active = + Triple( + "active", + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true + ), + 250L + ) + + val resume1 = + Triple( + "resume 1", + DATA.copy( + active = false, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true + ), + 500L + ) + + val resume2 = + Triple( + "resume 2", + DATA.copy( + active = false, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true + ), + 1000L + ) + + val activeMoreRecent = + Triple( + "active more recent", + DATA.copy( + active = false, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true, + lastActive = 2L + ), + 1000L + ) + + val activeLessRecent = + Triple( + "active less recent", + DATA.copy( + active = false, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true, + lastActive = 1L + ), + 1000L + ) + // Expected ordering for media players: + // Actively playing local sessions + // Actively playing cast sessions + // Paused local and cast sessions, by last active + // RCNs + // Resume controls, by last active + + val expected = + listOf( + playingLocal, + playingCast, + pausedCast, + pausedLocal, + playingRcn, + pausedRcn, + active, + resume2, + resume1 + ) + + expected.forEach { + clock.setCurrentTimeMillis(it.third) + MediaPlayerData.addMediaPlayer( + it.first, + it.second.copy(notificationKey = it.first), + panel, + clock, + isSsReactivated = false + ) + } + + for ((index, key) in MediaPlayerData.playerKeys().withIndex()) { + assertEquals(expected.get(index).first, key.data.notificationKey) + } + + for ((index, key) in MediaPlayerData.visiblePlayerKeys().withIndex()) { + assertEquals(expected.get(index).first, key.data.notificationKey) + } + } + + @Test + fun testOrderWithSmartspace_prioritized() { + testPlayerOrdering() + + // If smartspace is prioritized + MediaPlayerData.addMediaRecommendation( + SMARTSPACE_KEY, + EMPTY_SMARTSPACE_MEDIA_DATA, + panel, + true, + clock + ) + + // Then it should be shown immediately after any actively playing controls + assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec) + } + + @Test + fun testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayers() { + testPlayerOrdering() + + // If smartspace is prioritized + listener.value.onSmartspaceMediaDataLoaded( + SMARTSPACE_KEY, + EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true), + true + ) + + // Then it should be shown immediately after any actively playing controls + assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec) + assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(2).isSsMediaRec) + } + + @Test + fun testOrderWithSmartspace_notPrioritized() { + testPlayerOrdering() + + // If smartspace is not prioritized + MediaPlayerData.addMediaRecommendation( + SMARTSPACE_KEY, + EMPTY_SMARTSPACE_MEDIA_DATA, + panel, + false, + clock + ) + + // Then it should be shown at the end of the carousel's active entries + val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1 + assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec) + } + + @Test + fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() { + testPlayerOrdering() + // playing paused player + listener.value.onMediaDataLoaded( + "paused local", + "paused local", + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ) + ) + listener.value.onMediaDataLoaded( + "playing local", + "playing local", + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true + ) + ) + + assertEquals( + MediaPlayerData.getMediaPlayerIndex("paused local"), + mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex + ) + // paused player order should stays the same in visibleMediaPLayer map. + // paused player order should be first in mediaPlayer map. + assertEquals( + MediaPlayerData.visiblePlayerKeys().elementAt(3), + MediaPlayerData.playerKeys().elementAt(0) + ) + } + @Test + fun testSwipeDismiss_logged() { + mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke() + + verify(logger).logSwipeDismiss() + } + + @Test + fun testSettingsButton_logged() { + mediaCarouselController.settingsButton.callOnClick() + + verify(logger).logCarouselSettings() + } + + @Test + fun testLocationChangeQs_logged() { + mediaCarouselController.onDesiredLocationChanged( + MediaHierarchyManager.LOCATION_QS, + mediaHostState, + animate = false + ) + verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS) + } + + @Test + fun testLocationChangeQqs_logged() { + mediaCarouselController.onDesiredLocationChanged( + MediaHierarchyManager.LOCATION_QQS, + mediaHostState, + animate = false + ) + verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS) + } + + @Test + fun testLocationChangeLockscreen_logged() { + mediaCarouselController.onDesiredLocationChanged( + MediaHierarchyManager.LOCATION_LOCKSCREEN, + mediaHostState, + animate = false + ) + verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN) + } + + @Test + fun testLocationChangeDream_logged() { + mediaCarouselController.onDesiredLocationChanged( + MediaHierarchyManager.LOCATION_DREAM_OVERLAY, + mediaHostState, + animate = false + ) + verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY) + } + + @Test + fun testRecommendationRemoved_logged() { + val packageName = "smartspace package" + val instanceId = InstanceId.fakeInstanceId(123) + + val smartspaceData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = packageName, instanceId = instanceId) + MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock) + mediaCarouselController.removePlayer(SMARTSPACE_KEY) + + verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!)) + } + + @Test + fun testMediaLoaded_ScrollToActivePlayer() { + listener.value.onMediaDataLoaded( + "playing local", + null, + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ) + ) + listener.value.onMediaDataLoaded( + "paused local", + null, + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ) + ) + // adding a media recommendation card. + listener.value.onSmartspaceMediaDataLoaded( + SMARTSPACE_KEY, + EMPTY_SMARTSPACE_MEDIA_DATA, + false + ) + mediaCarouselController.shouldScrollToKey = true + // switching between media players. + listener.value.onMediaDataLoaded( + "playing local", + "playing local", + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true + ) + ) + listener.value.onMediaDataLoaded( + "paused local", + "paused local", + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ) + ) + + assertEquals( + MediaPlayerData.getMediaPlayerIndex("paused local"), + mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex + ) + } + + @Test + fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() { + listener.value.onSmartspaceMediaDataLoaded( + SMARTSPACE_KEY, + EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true), + false + ) + listener.value.onMediaDataLoaded( + "playing local", + null, + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ) + ) + + var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local") + assertEquals( + playerIndex, + mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex + ) + assertEquals(playerIndex, 0) + + // Replaying the same media player one more time. + // And check that the card stays in its position. + mediaCarouselController.shouldScrollToKey = true + listener.value.onMediaDataLoaded( + "playing local", + null, + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false, + packageName = "PACKAGE_NAME" + ) + ) + playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local") + assertEquals(playerIndex, 0) + } + + @Test + fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() { + var result = false + mediaCarouselController.updateHostVisibility = { result = true } + + whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true) + listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) + + assertEquals(true, result) + } + + @Test + fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() { + var result = false + mediaCarouselController.updateHostVisibility = { result = true } + + whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false) + listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) + assertEquals(false, result) + + visualStabilityCallback.value.onReorderingAllowed() + assertEquals(true, result) + } + + @Test + fun testGetCurrentVisibleMediaContentIntent() { + val clickIntent1 = mock(PendingIntent::class.java) + val player1 = Triple("player1", DATA.copy(clickIntent = clickIntent1), 1000L) + clock.setCurrentTimeMillis(player1.third) + MediaPlayerData.addMediaPlayer( + player1.first, + player1.second.copy(notificationKey = player1.first), + panel, + clock, + isSsReactivated = false + ) + + assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1) + + val clickIntent2 = mock(PendingIntent::class.java) + val player2 = Triple("player2", DATA.copy(clickIntent = clickIntent2), 2000L) + clock.setCurrentTimeMillis(player2.third) + MediaPlayerData.addMediaPlayer( + player2.first, + player2.second.copy(notificationKey = player2.first), + panel, + clock, + isSsReactivated = false + ) + + // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is + // added to the front because it was active more recently. + assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2) + + val clickIntent3 = mock(PendingIntent::class.java) + val player3 = Triple("player3", DATA.copy(clickIntent = clickIntent3), 500L) + clock.setCurrentTimeMillis(player3.third) + MediaPlayerData.addMediaPlayer( + player3.first, + player3.second.copy(notificationKey = player3.first), + panel, + clock, + isSsReactivated = false + ) + + // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is + // added to the end because it was active less recently. + assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2) + } + + @Test + fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() { + val delta = 0.0001F + val paginationSquishMiddle = + TRANSFORM_BEZIER.getInterpolation( + (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION + ) + val paginationSquishEnd = + TRANSFORM_BEZIER.getInterpolation( + (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION + ) + whenever(mediaHostStatesManager.mediaHostStates) + .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState)) + whenever(mediaHostState.visible).thenReturn(true) + mediaCarouselController.currentEndLocation = LOCATION_QS + whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle) + mediaCarouselController.updatePageIndicatorAlpha() + assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta) + + whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd) + mediaCarouselController.updatePageIndicatorAlpha() + assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index 7de5719c03ec..584305334b6f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.animation.Animator import android.animation.AnimatorSet @@ -59,7 +59,20 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.bluetooth.BroadcastDialogController import com.android.systemui.broadcast.BroadcastSender -import com.android.systemui.media.MediaControlPanel.KEY_SMARTSPACE_APP_NAME +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.GutsViewHolder +import com.android.systemui.media.controls.models.player.MediaAction +import com.android.systemui.media.controls.models.player.MediaButton +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaDeviceData +import com.android.systemui.media.controls.models.player.MediaViewHolder +import com.android.systemui.media.controls.models.player.SeekBarViewModel +import com.android.systemui.media.controls.models.recommendation.KEY_SMARTSPACE_APP_NAME +import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.media.dialog.MediaOutputDialogFactory import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager @@ -164,8 +177,8 @@ public class MediaControlPanelTest : SysuiTestCase() { private lateinit var session: MediaSession private lateinit var device: MediaDeviceData - private val disabledDevice = MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null, - showBroadcastButton = false) + private val disabledDevice = + MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null, showBroadcastButton = false) private lateinit var mediaData: MediaData private val clock = FakeSystemClock() @Mock private lateinit var logger: MediaUiEventLogger @@ -212,24 +225,27 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE) context.setMockPackageManager(packageManager) - player = object : MediaControlPanel( - context, - bgExecutor, - mainExecutor, - activityStarter, - broadcastSender, - mediaViewController, - seekBarViewModel, - Lazy { mediaDataManager }, - mediaOutputDialogFactory, - mediaCarouselController, - falsingManager, - clock, - logger, - keyguardStateController, - activityIntentHelper, - lockscreenUserManager, - broadcastDialogController) { + player = + object : + MediaControlPanel( + context, + bgExecutor, + mainExecutor, + activityStarter, + broadcastSender, + mediaViewController, + seekBarViewModel, + Lazy { mediaDataManager }, + mediaOutputDialogFactory, + mediaCarouselController, + falsingManager, + clock, + logger, + keyguardStateController, + activityIntentHelper, + lockscreenUserManager, + broadcastDialogController + ) { override fun loadAnimator( animId: Int, otionInterpolator: Interpolator, @@ -250,18 +266,20 @@ public class MediaControlPanelTest : SysuiTestCase() { // Set valid recommendation data val extras = Bundle() extras.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME) - val intent = Intent().apply { - putExtras(extras) - setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } + val intent = + Intent().apply { + putExtras(extras) + setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } whenever(smartspaceAction.intent).thenReturn(intent) whenever(smartspaceAction.extras).thenReturn(extras) - smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( - packageName = PACKAGE, - instanceId = instanceId, - recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction), - cardAction = smartspaceAction - ) + smartspaceData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + packageName = PACKAGE, + instanceId = instanceId, + recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction), + cardAction = smartspaceAction + ) } private fun initGutsViewHolderMocks() { @@ -279,36 +297,39 @@ public class MediaControlPanelTest : SysuiTestCase() { } private fun initDeviceMediaData(shouldShowBroadcastButton: Boolean, name: String) { - device = MediaDeviceData(true, null, name, null, - showBroadcastButton = shouldShowBroadcastButton) + device = + MediaDeviceData(true, null, name, null, showBroadcastButton = shouldShowBroadcastButton) // Create media session - val metadataBuilder = MediaMetadata.Builder().apply { - putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) - putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) - } - val playbackBuilder = PlaybackState.Builder().apply { - setState(PlaybackState.STATE_PAUSED, 6000L, 1f) - setActions(PlaybackState.ACTION_PLAY) - } - session = MediaSession(context, SESSION_KEY).apply { - setMetadata(metadataBuilder.build()) - setPlaybackState(playbackBuilder.build()) - } + val metadataBuilder = + MediaMetadata.Builder().apply { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + } + val playbackBuilder = + PlaybackState.Builder().apply { + setState(PlaybackState.STATE_PAUSED, 6000L, 1f) + setActions(PlaybackState.ACTION_PLAY) + } + session = + MediaSession(context, SESSION_KEY).apply { + setMetadata(metadataBuilder.build()) + setPlaybackState(playbackBuilder.build()) + } session.setActive(true) - mediaData = MediaTestUtils.emptyMediaData.copy( + mediaData = + MediaTestUtils.emptyMediaData.copy( artist = ARTIST, song = TITLE, packageName = PACKAGE, token = session.sessionToken, device = device, - instanceId = instanceId) + instanceId = instanceId + ) } - /** - * Initialize elements in media view holder - */ + /** Initialize elements in media view holder */ private fun initMediaViewHolderMocks() { whenever(seekBarViewModel.progress).thenReturn(seekBarData) @@ -349,7 +370,8 @@ public class MediaControlPanelTest : SysuiTestCase() { action1.id, action2.id, action3.id, - action4.id) + action4.id + ) } whenever(viewHolder.player).thenReturn(view) @@ -394,9 +416,7 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier) } - /** - * Initialize elements for the recommendation view holder - */ + /** Initialize elements for the recommendation view holder */ private fun initRecommendationViewHolderMocks() { recTitle1 = TextView(context) recTitle2 = TextView(context) @@ -419,9 +439,8 @@ public class MediaControlPanelTest : SysuiTestCase() { .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3)) whenever(recommendationViewHolder.mediaTitles) .thenReturn(listOf(recTitle1, recTitle2, recTitle3)) - whenever(recommendationViewHolder.mediaSubtitles).thenReturn( - listOf(recSubtitle1, recSubtitle2, recSubtitle3) - ) + whenever(recommendationViewHolder.mediaSubtitles) + .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3)) whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder) @@ -453,12 +472,13 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindSemanticActions() { val icon = context.getDrawable(android.R.drawable.ic_media_play) val bg = context.getDrawable(R.drawable.qs_media_round_button_background) - val semanticActions = MediaButton( - playOrPause = MediaAction(icon, Runnable {}, "play", bg), - nextOrCustom = MediaAction(icon, Runnable {}, "next", bg), - custom0 = MediaAction(icon, null, "custom 0", bg), - custom1 = MediaAction(icon, null, "custom 1", bg) - ) + val semanticActions = + MediaButton( + playOrPause = MediaAction(icon, Runnable {}, "play", bg), + nextOrCustom = MediaAction(icon, Runnable {}, "next", bg), + custom0 = MediaAction(icon, null, "custom 0", bg), + custom1 = MediaAction(icon, null, "custom 1", bg) + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) player.bindPlayer(state, PACKAGE) @@ -501,15 +521,16 @@ public class MediaControlPanelTest : SysuiTestCase() { val bg = context.getDrawable(R.drawable.qs_media_round_button_background) // Setup button state: no prev or next button and their slots reserved - val semanticActions = MediaButton( - playOrPause = MediaAction(icon, Runnable {}, "play", bg), - nextOrCustom = null, - prevOrCustom = null, - custom0 = MediaAction(icon, null, "custom 0", bg), - custom1 = MediaAction(icon, null, "custom 1", bg), - false, - true - ) + val semanticActions = + MediaButton( + playOrPause = MediaAction(icon, Runnable {}, "play", bg), + nextOrCustom = null, + prevOrCustom = null, + custom0 = MediaAction(icon, null, "custom 0", bg), + custom1 = MediaAction(icon, null, "custom 1", bg), + false, + true + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -530,15 +551,16 @@ public class MediaControlPanelTest : SysuiTestCase() { val bg = context.getDrawable(R.drawable.qs_media_round_button_background) // Setup button state: no prev or next button and their slots reserved - val semanticActions = MediaButton( - playOrPause = MediaAction(icon, Runnable {}, "play", bg), - nextOrCustom = null, - prevOrCustom = null, - custom0 = MediaAction(icon, null, "custom 0", bg), - custom1 = MediaAction(icon, null, "custom 1", bg), - true, - false - ) + val semanticActions = + MediaButton( + playOrPause = MediaAction(icon, Runnable {}, "play", bg), + nextOrCustom = null, + prevOrCustom = null, + custom0 = MediaAction(icon, null, "custom 0", bg), + custom1 = MediaAction(icon, null, "custom 1", bg), + true, + false + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -646,10 +668,11 @@ public class MediaControlPanelTest : SysuiTestCase() { useRealConstraintSets() val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - playOrPause = MediaAction(icon, Runnable {}, "play", null), - nextOrCustom = MediaAction(icon, Runnable {}, "next", null) - ) + val semanticActions = + MediaButton( + playOrPause = MediaAction(icon, Runnable {}, "play", null), + nextOrCustom = MediaAction(icon, Runnable {}, "next", null) + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -719,9 +742,8 @@ public class MediaControlPanelTest : SysuiTestCase() { useRealConstraintSets() val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - nextOrCustom = MediaAction(icon, Runnable {}, "next", null) - ) + val semanticActions = + MediaButton(nextOrCustom = MediaAction(icon, Runnable {}, "next", null)) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -736,10 +758,11 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bind_notScrubbing_scrubbingViewsGone() { val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - prevOrCustom = MediaAction(icon, {}, "prev", null), - nextOrCustom = MediaAction(icon, {}, "next", null) - ) + val semanticActions = + MediaButton( + prevOrCustom = MediaAction(icon, {}, "prev", null), + nextOrCustom = MediaAction(icon, {}, "next", null) + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -770,10 +793,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() { val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - prevOrCustom = null, - nextOrCustom = MediaAction(icon, {}, "next", null) - ) + val semanticActions = + MediaButton(prevOrCustom = null, nextOrCustom = MediaAction(icon, {}, "next", null)) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) player.bindPlayer(state, PACKAGE) @@ -790,10 +811,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() { val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - prevOrCustom = MediaAction(icon, {}, "prev", null), - nextOrCustom = null - ) + val semanticActions = + MediaButton(prevOrCustom = MediaAction(icon, {}, "prev", null), nextOrCustom = null) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) player.bindPlayer(state, PACKAGE) @@ -810,10 +829,11 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun setIsScrubbing_true_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() { val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - prevOrCustom = MediaAction(icon, {}, "prev", null), - nextOrCustom = MediaAction(icon, {}, "next", null) - ) + val semanticActions = + MediaButton( + prevOrCustom = MediaAction(icon, {}, "prev", null), + nextOrCustom = MediaAction(icon, {}, "next", null) + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) player.bindPlayer(state, PACKAGE) @@ -832,10 +852,11 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun setIsScrubbing_trueThenFalse_scrubbingTimeGoneAtEnd() { val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - prevOrCustom = MediaAction(icon, {}, "prev", null), - nextOrCustom = MediaAction(icon, {}, "next", null) - ) + val semanticActions = + MediaButton( + prevOrCustom = MediaAction(icon, {}, "prev", null), + nextOrCustom = MediaAction(icon, {}, "next", null) + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -859,18 +880,20 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindNotificationActions() { val icon = context.getDrawable(android.R.drawable.ic_media_play) val bg = context.getDrawable(R.drawable.qs_media_round_button_background) - val actions = listOf( - MediaAction(icon, Runnable {}, "previous", bg), - MediaAction(icon, Runnable {}, "play", bg), - MediaAction(icon, null, "next", bg), - MediaAction(icon, null, "custom 0", bg), - MediaAction(icon, Runnable {}, "custom 1", bg) - ) - val state = mediaData.copy( - actions = actions, - actionsToShowInCompact = listOf(1, 2), - semanticActions = null - ) + val actions = + listOf( + MediaAction(icon, Runnable {}, "previous", bg), + MediaAction(icon, Runnable {}, "play", bg), + MediaAction(icon, null, "next", bg), + MediaAction(icon, null, "custom 0", bg), + MediaAction(icon, Runnable {}, "custom 1", bg) + ) + val state = + mediaData.copy( + actions = actions, + actionsToShowInCompact = listOf(1, 2), + semanticActions = null + ) player.attachPlayer(viewHolder) player.bindPlayer(state, PACKAGE) @@ -918,15 +941,12 @@ public class MediaControlPanelTest : SysuiTestCase() { val icon = context.getDrawable(R.drawable.ic_media_play) val bg = context.getDrawable(R.drawable.ic_media_play_container) - val semanticActions0 = MediaButton( - playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null) - ) - val semanticActions1 = MediaButton( - playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null) - ) - val semanticActions2 = MediaButton( - playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null) - ) + val semanticActions0 = + MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)) + val semanticActions1 = + MediaButton(playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null)) + val semanticActions2 = + MediaButton(playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null)) val state0 = mediaData.copy(semanticActions = semanticActions0) val state1 = mediaData.copy(semanticActions = semanticActions1) val state2 = mediaData.copy(semanticActions = semanticActions2) @@ -1089,11 +1109,10 @@ public class MediaControlPanelTest : SysuiTestCase() { val mockAvd0 = mock(AnimatedVectorDrawable::class.java) whenever(mockAvd0.mutate()).thenReturn(mockAvd0) - val semanticActions0 = MediaButton( - playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null) - ) - val state = mediaData.copy(resumption = true, semanticActions = semanticActions0, - isPlaying = false) + val semanticActions0 = + MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)) + val state = + mediaData.copy(resumption = true, semanticActions = semanticActions0, isPlaying = false) player.attachPlayer(viewHolder) player.bindPlayer(state, PACKAGE) assertThat(seamlessText.getText()).isEqualTo(APP_NAME) @@ -1432,9 +1451,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionPlayPauseClick_isLogged() { - val semanticActions = MediaButton( - playOrPause = MediaAction(null, Runnable {}, "play", null) - ) + val semanticActions = + MediaButton(playOrPause = MediaAction(null, Runnable {}, "play", null)) val data = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -1446,9 +1464,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionPrevClick_isLogged() { - val semanticActions = MediaButton( - prevOrCustom = MediaAction(null, Runnable {}, "previous", null) - ) + val semanticActions = + MediaButton(prevOrCustom = MediaAction(null, Runnable {}, "previous", null)) val data = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -1460,9 +1477,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionNextClick_isLogged() { - val semanticActions = MediaButton( - nextOrCustom = MediaAction(null, Runnable {}, "next", null) - ) + val semanticActions = + MediaButton(nextOrCustom = MediaAction(null, Runnable {}, "next", null)) val data = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -1474,9 +1490,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionCustom0Click_isLogged() { - val semanticActions = MediaButton( - custom0 = MediaAction(null, Runnable {}, "custom 0", null) - ) + val semanticActions = + MediaButton(custom0 = MediaAction(null, Runnable {}, "custom 0", null)) val data = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -1488,9 +1503,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionCustom1Click_isLogged() { - val semanticActions = MediaButton( - custom1 = MediaAction(null, Runnable {}, "custom 1", null) - ) + val semanticActions = + MediaButton(custom1 = MediaAction(null, Runnable {}, "custom 1", null)) val data = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -1502,13 +1516,14 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionCustom2Click_isLogged() { - val actions = listOf( - MediaAction(null, Runnable {}, "action 0", null), - MediaAction(null, Runnable {}, "action 1", null), - MediaAction(null, Runnable {}, "action 2", null), - MediaAction(null, Runnable {}, "action 3", null), - MediaAction(null, Runnable {}, "action 4", null) - ) + val actions = + listOf( + MediaAction(null, Runnable {}, "action 0", null), + MediaAction(null, Runnable {}, "action 1", null), + MediaAction(null, Runnable {}, "action 2", null), + MediaAction(null, Runnable {}, "action 3", null), + MediaAction(null, Runnable {}, "action 4", null) + ) val data = mediaData.copy(actions = actions) player.attachPlayer(viewHolder) @@ -1520,13 +1535,14 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionCustom3Click_isLogged() { - val actions = listOf( - MediaAction(null, Runnable {}, "action 0", null), - MediaAction(null, Runnable {}, "action 1", null), - MediaAction(null, Runnable {}, "action 2", null), - MediaAction(null, Runnable {}, "action 3", null), - MediaAction(null, Runnable {}, "action 4", null) - ) + val actions = + listOf( + MediaAction(null, Runnable {}, "action 0", null), + MediaAction(null, Runnable {}, "action 1", null), + MediaAction(null, Runnable {}, "action 2", null), + MediaAction(null, Runnable {}, "action 3", null), + MediaAction(null, Runnable {}, "action 4", null) + ) val data = mediaData.copy(actions = actions) player.attachPlayer(viewHolder) @@ -1538,13 +1554,14 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionCustom4Click_isLogged() { - val actions = listOf( - MediaAction(null, Runnable {}, "action 0", null), - MediaAction(null, Runnable {}, "action 1", null), - MediaAction(null, Runnable {}, "action 2", null), - MediaAction(null, Runnable {}, "action 3", null), - MediaAction(null, Runnable {}, "action 4", null) - ) + val actions = + listOf( + MediaAction(null, Runnable {}, "action 0", null), + MediaAction(null, Runnable {}, "action 1", null), + MediaAction(null, Runnable {}, "action 2", null), + MediaAction(null, Runnable {}, "action 3", null), + MediaAction(null, Runnable {}, "action 4", null) + ) val data = mediaData.copy(actions = actions) player.attachPlayer(viewHolder) @@ -1608,8 +1625,7 @@ public class MediaControlPanelTest : SysuiTestCase() { // THEN it shows without dismissing keyguard first captor.value.onClick(viewHolder.player) - verify(activityStarter).startActivity(eq(clickIntent), eq(true), - nullable(), eq(true)) + verify(activityStarter).startActivity(eq(clickIntent), eq(true), nullable(), eq(true)) } @Test @@ -1697,20 +1713,22 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindRecommendation_listHasTooFewRecs_notDisplayed() { player.attachRecommendation(recommendationViewHolder) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("subtitle1") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("subtitle2") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "title1") + .setSubtitle("subtitle1") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "title2") + .setSubtitle("subtitle2") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + ) ) - ) player.bindRecommendation(data) @@ -1722,30 +1740,32 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindRecommendation_listHasTooFewRecsWithIcons_notDisplayed() { player.attachRecommendation(recommendationViewHolder) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("subtitle1") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("subtitle2") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "empty icon 1") - .setSubtitle("subtitle2") - .setIcon(null) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "empty icon 2") - .setSubtitle("subtitle2") - .setIcon(null) - .setExtras(Bundle.EMPTY) - .build(), + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "title1") + .setSubtitle("subtitle1") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "title2") + .setSubtitle("subtitle2") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "empty icon 1") + .setSubtitle("subtitle2") + .setIcon(null) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "empty icon 2") + .setSubtitle("subtitle2") + .setIcon(null) + .setExtras(Bundle.EMPTY) + .build(), + ) ) - ) player.bindRecommendation(data) @@ -1765,25 +1785,27 @@ public class MediaControlPanelTest : SysuiTestCase() { val subtitle3 = "Subtitle3" val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", title1) - .setSubtitle(subtitle1) - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", title2) - .setSubtitle(subtitle2) - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", title3) - .setSubtitle(subtitle3) - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build() + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", title1) + .setSubtitle(subtitle1) + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", title2) + .setSubtitle(subtitle2) + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id3", title3) + .setSubtitle(subtitle3) + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build() + ) ) - ) player.bindRecommendation(data) assertThat(recTitle1.text).isEqualTo(title1) @@ -1798,15 +1820,17 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindRecommendation_noTitle_subtitleNotShown() { player.attachRecommendation(recommendationViewHolder) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "") - .setSubtitle("fake subtitle") - .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) - .setExtras(Bundle.EMPTY) - .build() + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "") + .setSubtitle("fake subtitle") + .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) + .setExtras(Bundle.EMPTY) + .build() + ) ) - ) player.bindRecommendation(data) assertThat(recSubtitle1.text).isEqualTo("") @@ -1818,25 +1842,27 @@ public class MediaControlPanelTest : SysuiTestCase() { player.attachRecommendation(recommendationViewHolder) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "") - .setSubtitle("fake subtitle") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("fake subtitle") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "") - .setSubtitle("fake subtitle") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build() + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "") + .setSubtitle("fake subtitle") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "title2") + .setSubtitle("fake subtitle") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id3", "") + .setSubtitle("fake subtitle") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build() + ) ) - ) player.bindRecommendation(data) assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.VISIBLE) @@ -1850,25 +1876,27 @@ public class MediaControlPanelTest : SysuiTestCase() { player.attachRecommendation(recommendationViewHolder) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "") - .setSubtitle("") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "title3") - .setSubtitle("subtitle3") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build() + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "") + .setSubtitle("") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "title2") + .setSubtitle("") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id3", "title3") + .setSubtitle("subtitle3") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build() + ) ) - ) player.bindRecommendation(data) assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.VISIBLE) @@ -1880,25 +1908,27 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindRecommendation_noneHaveSubtitles_subtitleViewsGone() { useRealConstraintSets() player.attachRecommendation(recommendationViewHolder) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("") - .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("") - .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "title3") - .setSubtitle("") - .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata)) - .setExtras(Bundle.EMPTY) - .build() + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "title1") + .setSubtitle("") + .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "title2") + .setSubtitle("") + .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id3", "title3") + .setSubtitle("") + .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata)) + .setExtras(Bundle.EMPTY) + .build() + ) ) - ) player.bindRecommendation(data) @@ -1911,25 +1941,27 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindRecommendation_noneHaveTitles_titleAndSubtitleViewsGone() { useRealConstraintSets() player.attachRecommendation(recommendationViewHolder) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "") - .setSubtitle("subtitle1") - .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "") - .setSubtitle("subtitle2") - .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "") - .setSubtitle("subtitle3") - .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata)) - .setExtras(Bundle.EMPTY) - .build() + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "") + .setSubtitle("subtitle1") + .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "") + .setSubtitle("subtitle2") + .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id3", "") + .setSubtitle("subtitle3") + .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata)) + .setExtras(Bundle.EMPTY) + .build() + ) ) - ) player.bindRecommendation(data) @@ -1942,20 +1974,23 @@ public class MediaControlPanelTest : SysuiTestCase() { } private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener = - withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) } + withArgCaptor { + verify(seekBarViewModel).setScrubbingChangeListener(capture()) + } - private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener = - withArgCaptor { verify(seekBarViewModel).setEnabledChangeListener(capture()) } + private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener = withArgCaptor { + verify(seekBarViewModel).setEnabledChangeListener(capture()) + } /** - * Update our test to use real ConstraintSets instead of mocks. + * Update our test to use real ConstraintSets instead of mocks. * - * Some item visibilities, such as the seekbar visibility, are dependent on other action's - * visibilities. If we use mocks for the ConstraintSets, then action visibility changes are - * just thrown away instead of being saved for reference later. This method sets us up to use - * ConstraintSets so that we do save visibility changes. + * Some item visibilities, such as the seekbar visibility, are dependent on other action's + * visibilities. If we use mocks for the ConstraintSets, then action visibility changes are just + * thrown away instead of being saved for reference later. This method sets us up to use + * ConstraintSets so that we do save visibility changes. * - * TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests? + * TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests? */ private fun useRealConstraintSets() { expandedSet = ConstraintSet() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 954b4386b71d..071604dc5790 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.graphics.Rect import android.provider.Settings @@ -84,10 +84,8 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private lateinit var statusBarCallback: ArgumentCaptor<(StatusBarStateController.StateListener)> @Captor private lateinit var dreamOverlayCallback: - ArgumentCaptor<(DreamOverlayStateController.Callback)> - @JvmField - @Rule - val mockito = MockitoJUnit.rule() + ArgumentCaptor<(DreamOverlayStateController.Callback)> + @JvmField @Rule val mockito = MockitoJUnit.rule() private lateinit var mediaHierarchyManager: MediaHierarchyManager private lateinit var mediaFrame: ViewGroup private val configurationController = FakeConfigurationController() @@ -98,13 +96,15 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Before fun setup() { - context.getOrCreateTestableResources().addOverride( - R.bool.config_use_split_notification_shade, false) + context + .getOrCreateTestableResources() + .addOverride(R.bool.config_use_split_notification_shade, false) mediaFrame = FrameLayout(context) testableLooper = TestableLooper.get(this) fakeHandler = FakeHandler(testableLooper.looper) whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame) - mediaHierarchyManager = MediaHierarchyManager( + mediaHierarchyManager = + MediaHierarchyManager( context, statusBarStateController, keyguardStateController, @@ -116,7 +116,8 @@ class MediaHierarchyManagerTest : SysuiTestCase() { wakefulnessLifecycle, notifPanelEvents, settings, - fakeHandler,) + fakeHandler, + ) verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture()) verify(statusBarStateController).addCallback(statusBarCallback.capture()) verify(dreamOverlayStateController).addCallback(dreamOverlayCallback.capture()) @@ -125,7 +126,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) whenever(mediaCarouselController.mediaCarouselScrollHandler) - .thenReturn(mediaCarouselScrollHandler) + .thenReturn(mediaCarouselScrollHandler) val observer = wakefullnessObserver.value assertNotNull("lifecycle observer wasn't registered", observer) observer.onFinishedWakingUp() @@ -151,30 +152,53 @@ class MediaHierarchyManagerTest : SysuiTestCase() { fun testBlockedWhenScreenTurningOff() { // Let's set it onto QS: mediaHierarchyManager.qsExpansion = 1.0f - verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) + verify(mediaCarouselController) + .onDesiredLocationChanged( + ArgumentMatchers.anyInt(), + any(MediaHostState::class.java), + anyBoolean(), + anyLong(), + anyLong() + ) val observer = wakefullnessObserver.value assertNotNull("lifecycle observer wasn't registered", observer) observer.onStartedGoingToSleep() clearInvocations(mediaCarouselController) mediaHierarchyManager.qsExpansion = 0.0f verify(mediaCarouselController, times(0)) - .onDesiredLocationChanged(ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) + .onDesiredLocationChanged( + ArgumentMatchers.anyInt(), + any(MediaHostState::class.java), + anyBoolean(), + anyLong(), + anyLong() + ) } @Test fun testAllowedWhenNotTurningOff() { // Let's set it onto QS: mediaHierarchyManager.qsExpansion = 1.0f - verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) + verify(mediaCarouselController) + .onDesiredLocationChanged( + ArgumentMatchers.anyInt(), + any(MediaHostState::class.java), + anyBoolean(), + anyLong(), + anyLong() + ) val observer = wakefullnessObserver.value assertNotNull("lifecycle observer wasn't registered", observer) clearInvocations(mediaCarouselController) mediaHierarchyManager.qsExpansion = 0.0f - verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) + verify(mediaCarouselController) + .onDesiredLocationChanged( + ArgumentMatchers.anyInt(), + any(MediaHostState::class.java), + anyBoolean(), + anyLong(), + anyLong() + ) } @Test @@ -183,22 +207,26 @@ class MediaHierarchyManagerTest : SysuiTestCase() { // Let's transition all the way to full shade mediaHierarchyManager.setTransitionToFullShadeAmount(100000f) - verify(mediaCarouselController).onDesiredLocationChanged( - eq(MediaHierarchyManager.LOCATION_QQS), - any(MediaHostState::class.java), - eq(false), - anyLong(), - anyLong()) + verify(mediaCarouselController) + .onDesiredLocationChanged( + eq(MediaHierarchyManager.LOCATION_QQS), + any(MediaHostState::class.java), + eq(false), + anyLong(), + anyLong() + ) clearInvocations(mediaCarouselController) // Let's go back to the lock screen mediaHierarchyManager.setTransitionToFullShadeAmount(0.0f) - verify(mediaCarouselController).onDesiredLocationChanged( - eq(MediaHierarchyManager.LOCATION_LOCKSCREEN), - any(MediaHostState::class.java), - eq(false), - anyLong(), - anyLong()) + verify(mediaCarouselController) + .onDesiredLocationChanged( + eq(MediaHierarchyManager.LOCATION_LOCKSCREEN), + any(MediaHostState::class.java), + eq(false), + anyLong(), + anyLong() + ) // Let's make sure alpha is set mediaHierarchyManager.setTransitionToFullShadeAmount(2.0f) @@ -302,7 +330,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { val expectedTranslation = LOCKSCREEN_TOP - QS_TOP assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()) - .isEqualTo(expectedTranslation) + .isEqualTo(expectedTranslation) } @Test @@ -343,27 +371,31 @@ class MediaHierarchyManagerTest : SysuiTestCase() { fun testDream() { goToDream() setMediaDreamComplicationEnabled(true) - verify(mediaCarouselController).onDesiredLocationChanged( + verify(mediaCarouselController) + .onDesiredLocationChanged( eq(MediaHierarchyManager.LOCATION_DREAM_OVERLAY), nullable(), eq(false), anyLong(), - anyLong()) + anyLong() + ) clearInvocations(mediaCarouselController) setMediaDreamComplicationEnabled(false) - verify(mediaCarouselController).onDesiredLocationChanged( + verify(mediaCarouselController) + .onDesiredLocationChanged( eq(MediaHierarchyManager.LOCATION_QQS), any(MediaHostState::class.java), eq(false), anyLong(), - anyLong()) + anyLong() + ) } private fun enableSplitShade() { - context.getOrCreateTestableResources().addOverride( - R.bool.config_use_split_notification_shade, true - ) + context + .getOrCreateTestableResources() + .addOverride(R.bool.config_use_split_notification_shade, true) configurationController.notifyConfigurationChanged() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt index 6e38d26411ee..32b822d798f8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt @@ -14,11 +14,13 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -33,13 +35,10 @@ import org.mockito.junit.MockitoJUnit @RunWith(AndroidTestingRunner::class) public class MediaPlayerDataTest : SysuiTestCase() { - @Mock - private lateinit var playerIsPlaying: MediaControlPanel + @Mock private lateinit var playerIsPlaying: MediaControlPanel private var systemClock: FakeSystemClock = FakeSystemClock() - @JvmField - @Rule - val mockito = MockitoJUnit.rule() + @JvmField @Rule val mockito = MockitoJUnit.rule() companion object { val LOCAL = MediaData.PLAYBACK_LOCAL @@ -61,10 +60,20 @@ public class MediaPlayerDataTest : SysuiTestCase() { val playerIsRemote = mock(MediaControlPanel::class.java) val dataIsRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION) - MediaPlayerData.addMediaPlayer("2", dataIsRemote, playerIsRemote, systemClock, - isSsReactivated = false) - MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + "2", + dataIsRemote, + playerIsRemote, + systemClock, + isSsReactivated = false + ) + MediaPlayerData.addMediaPlayer( + "1", + dataIsPlaying, + playerIsPlaying, + systemClock, + isSsReactivated = false + ) val players = MediaPlayerData.players() assertThat(players).hasSize(2) @@ -79,22 +88,42 @@ public class MediaPlayerDataTest : SysuiTestCase() { val playerIsPlaying2 = mock(MediaControlPanel::class.java) var dataIsPlaying2 = createMediaData("app2", !PLAYING, LOCAL, !RESUMPTION) - MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + "1", + dataIsPlaying1, + playerIsPlaying1, + systemClock, + isSsReactivated = false + ) systemClock.advanceTime(1) - MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + "2", + dataIsPlaying2, + playerIsPlaying2, + systemClock, + isSsReactivated = false + ) systemClock.advanceTime(1) dataIsPlaying1 = createMediaData("app1", !PLAYING, LOCAL, !RESUMPTION) dataIsPlaying2 = createMediaData("app2", PLAYING, LOCAL, !RESUMPTION) - MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + "1", + dataIsPlaying1, + playerIsPlaying1, + systemClock, + isSsReactivated = false + ) systemClock.advanceTime(1) - MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + "2", + dataIsPlaying2, + playerIsPlaying2, + systemClock, + isSsReactivated = false + ) systemClock.advanceTime(1) val players = MediaPlayerData.players() @@ -122,26 +151,60 @@ public class MediaPlayerDataTest : SysuiTestCase() { val dataUndetermined = createMediaData("app6", UNDETERMINED, LOCAL, RESUMPTION) MediaPlayerData.addMediaPlayer( - "3", dataIsStoppedAndLocal, playerIsStoppedAndLocal, systemClock, - isSsReactivated = false) + "3", + dataIsStoppedAndLocal, + playerIsStoppedAndLocal, + systemClock, + isSsReactivated = false + ) MediaPlayerData.addMediaPlayer( - "5", dataIsStoppedAndRemote, playerIsStoppedAndRemote, systemClock, - isSsReactivated = false) - MediaPlayerData.addMediaPlayer("4", dataCanResume, playerCanResume, systemClock, - isSsReactivated = false) - MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock, - isSsReactivated = false) + "5", + dataIsStoppedAndRemote, + playerIsStoppedAndRemote, + systemClock, + isSsReactivated = false + ) MediaPlayerData.addMediaPlayer( - "2", dataIsPlayingAndRemote, playerIsPlayingAndRemote, systemClock, - isSsReactivated = false) - MediaPlayerData.addMediaPlayer("6", dataUndetermined, playerUndetermined, systemClock, - isSsReactivated = false) + "4", + dataCanResume, + playerCanResume, + systemClock, + isSsReactivated = false + ) + MediaPlayerData.addMediaPlayer( + "1", + dataIsPlaying, + playerIsPlaying, + systemClock, + isSsReactivated = false + ) + MediaPlayerData.addMediaPlayer( + "2", + dataIsPlayingAndRemote, + playerIsPlayingAndRemote, + systemClock, + isSsReactivated = false + ) + MediaPlayerData.addMediaPlayer( + "6", + dataUndetermined, + playerUndetermined, + systemClock, + isSsReactivated = false + ) val players = MediaPlayerData.players() assertThat(players).hasSize(6) - assertThat(players).containsExactly(playerIsPlaying, playerIsPlayingAndRemote, - playerIsStoppedAndRemote, playerIsStoppedAndLocal, playerUndetermined, - playerCanResume).inOrder() + assertThat(players) + .containsExactly( + playerIsPlaying, + playerIsPlayingAndRemote, + playerIsStoppedAndRemote, + playerIsStoppedAndLocal, + playerUndetermined, + playerCanResume + ) + .inOrder() } @Test @@ -153,13 +216,23 @@ public class MediaPlayerDataTest : SysuiTestCase() { assertThat(MediaPlayerData.players()).hasSize(0) - MediaPlayerData.addMediaPlayer(keyA, data, playerIsPlaying, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + keyA, + data, + playerIsPlaying, + systemClock, + isSsReactivated = false + ) systemClock.advanceTime(1) assertThat(MediaPlayerData.players()).hasSize(1) - MediaPlayerData.addMediaPlayer(keyB, data, playerIsPlaying, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + keyB, + data, + playerIsPlaying, + systemClock, + isSsReactivated = false + ) systemClock.advanceTime(1) assertThat(MediaPlayerData.players()).hasSize(2) @@ -177,12 +250,13 @@ public class MediaPlayerDataTest : SysuiTestCase() { isPlaying: Boolean?, location: Int, resumption: Boolean - ) = MediaTestUtils.emptyMediaData.copy( - app = app, - packageName = "package: $app", - playbackLocation = location, - resumption = resumption, - notificationKey = "key: $app", - isPlaying = isPlaying - ) + ) = + MediaTestUtils.emptyMediaData.copy( + app = app, + packageName = "package: $app", + playbackLocation = location, + resumption = resumption, + notificationKey = "key: $app", + isPlaying = isPlaying + ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt index 622a512720d9..6b7615557d83 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -22,13 +22,13 @@ import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION -import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY -import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY -import com.android.systemui.media.MediaCarouselController.Companion.DURATION -import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY -import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY -import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER import com.android.systemui.util.animation.MeasurementInput import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionViewState diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MetadataAnimationHandlerTest.kt index 311aa9649911..323b7818ed3d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MetadataAnimationHandlerTest.kt @@ -14,9 +14,8 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui -import org.mockito.Mockito.`when` as whenever import android.animation.Animator import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner @@ -29,10 +28,11 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.times import org.mockito.Mockito.mock import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit @SmallTest @@ -55,8 +55,7 @@ class MetadataAnimationHandlerTest : SysuiTestCase() { handler = MetadataAnimationHandler(exitAnimator, enterAnimator) } - @After - fun tearDown() {} + @After fun tearDown() {} @Test fun firstBind_startsAnimationSet() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/SquigglyProgressTest.kt index d087b0fe4413..d6cff81c0aaa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/SquigglyProgressTest.kt @@ -1,4 +1,20 @@ -package com.android.systemui.media +/* + * 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.media.controls.ui import android.graphics.Canvas import android.graphics.Color @@ -107,7 +123,6 @@ class SquigglyProgressTest : SysuiTestCase() { val (wavePaint, linePaint) = paintCaptor.getAllValues() assertThat(wavePaint.color).isEqualTo(tint) - assertThat(linePaint.color).isEqualTo( - ColorUtils.setAlphaComponent(tint, DISABLED_ALPHA)) + assertThat(linePaint.color).isEqualTo(ColorUtils.setAlphaComponent(tint, DISABLED_ALPHA)) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java index 29188da46562..ce885c0bba2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java @@ -25,7 +25,7 @@ import android.widget.FrameLayout; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.media.MediaHost; +import com.android.systemui.media.controls.ui.MediaHost; import com.android.systemui.util.animation.UniqueObjectHostView; import org.junit.Before; diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java index af530163e289..ed928a35a20e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java @@ -33,8 +33,8 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dreams.complication.DreamMediaEntryComplication; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.media.MediaData; -import com.android.systemui.media.MediaDataManager; +import com.android.systemui.media.controls.models.player.MediaData; +import com.android.systemui.media.controls.pipeline.MediaDataManager; import org.junit.Before; import org.junit.Test; diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt index 1078cdaa57c4..e009e8651f2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt @@ -19,9 +19,9 @@ package com.android.systemui.media.taptotransfer.common import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogcatEchoTracker import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt index 7c83cb74bb77..6a4c0f60466d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt @@ -22,6 +22,9 @@ import android.graphics.drawable.Drawable import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -62,6 +65,34 @@ class MediaTttUtilsTest : SysuiTestCase() { } @Test + fun getIconFromPackageName_nullPackageName_returnsDefault() { + val icon = MediaTttUtils.getIconFromPackageName(context, appPackageName = null, logger) + + val expectedDesc = + ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name) + .loadContentDescription(context) + assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc) + } + + @Test + fun getIconFromPackageName_invalidPackageName_returnsDefault() { + val icon = MediaTttUtils.getIconFromPackageName(context, "fakePackageName", logger) + + val expectedDesc = + ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name) + .loadContentDescription(context) + assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc) + } + + @Test + fun getIconFromPackageName_validPackageName_returnsAppInfo() { + val icon = MediaTttUtils.getIconFromPackageName(context, PACKAGE_NAME, logger) + + assertThat(icon) + .isEqualTo(Icon.Loaded(appIconFromPackageName, ContentDescription.Loaded(APP_NAME))) + } + + @Test fun getIconInfoFromPackageName_nullPackageName_returnsDefault() { val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null, logger) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index 616a349520ee..fdeb3f5eb857 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -17,14 +17,19 @@ package com.android.systemui.media.taptotransfer.sender import android.app.StatusBarManager +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable import android.media.MediaRoute2Info import android.os.PowerManager +import android.os.VibrationEffect import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager +import android.widget.ImageView import android.widget.TextView import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake @@ -32,16 +37,18 @@ import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.Text.Companion.loadText import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat @@ -60,20 +67,29 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class MediaTttSenderCoordinatorTest : SysuiTestCase() { + + // Note: This tests are a bit like integration tests because they use a real instance of + // [ChipbarCoordinator] and verify that the coordinator displays the correct view, based on + // the inputs from [MediaTttSenderCoordinator]. + private lateinit var underTest: MediaTttSenderCoordinator @Mock private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var applicationInfo: ApplicationInfo @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var falsingCollector: FalsingCollector @Mock private lateinit var logger: MediaTttLogger @Mock private lateinit var mediaTttFlags: MediaTttFlags + @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var powerManager: PowerManager @Mock private lateinit var viewUtil: ViewUtil @Mock private lateinit var windowManager: WindowManager + @Mock private lateinit var vibratorHelper: VibratorHelper private lateinit var chipbarCoordinator: ChipbarCoordinator private lateinit var commandQueueCallback: CommandQueue.Callbacks + private lateinit var fakeAppIconDrawable: Drawable private lateinit var fakeClock: FakeSystemClock private lateinit var fakeExecutor: FakeExecutor private lateinit var uiEventLoggerFake: UiEventLoggerFake @@ -83,7 +99,19 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true) - whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(1000) + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) + + fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!! + whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME) + whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable) + whenever( + packageManager.getApplicationInfo( + eq(PACKAGE_NAME), + any<PackageManager.ApplicationInfoFlags>() + ) + ) + .thenReturn(applicationInfo) + context.setMockPackageManager(packageManager) fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) @@ -100,10 +128,10 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { accessibilityManager, configurationController, powerManager, - uiEventLogger, falsingManager, falsingCollector, viewUtil, + vibratorHelper, ) chipbarCoordinator.start() @@ -149,10 +177,17 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(almostCloseToStartCast().state.getChipTextString(context, OTHER_DEVICE_NAME)) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) } @Test @@ -163,10 +198,17 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(almostCloseToEndCast().state.getChipTextString(context, OTHER_DEVICE_NAME)) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) } @Test @@ -177,12 +219,17 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) } @Test @@ -193,12 +240,17 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) } @Test @@ -209,12 +261,66 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToReceiverSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id) + verify(vibratorHelper, never()).vibrate(any<VibrationEffect>()) + } + + @Test + fun transferToReceiverSucceeded_nullUndoCallback_noUndo() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun transferToReceiverSucceeded_withUndoRunnable_undoVisible() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue() + } + + @Test + fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { + var undoCallbackCalled = false + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + }, + ) + + getChipbarView().getUndoButton().performClick() + + // Event index 1 since initially displaying the succeeded chip would also log an event + assertThat(uiEventLoggerFake.eventId(1)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id) + assertThat(undoCallbackCalled).isTrue() + assertThat(getChipbarView().getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) } @Test @@ -225,12 +331,68 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToThisDeviceSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id) + verify(vibratorHelper, never()).vibrate(any<VibrationEffect>()) + } + + @Test + fun transferToThisDeviceSucceeded_nullUndoCallback_noUndo() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun transferToThisDeviceSucceeded_withUndoRunnable_undoVisible() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue() + } + + @Test + fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { + var undoCallbackCalled = false + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + }, + ) + + getChipbarView().getUndoButton().performClick() + + // Event index 1 since initially displaying the succeeded chip would also log an event + assertThat(uiEventLoggerFake.eventId(1)) + .isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id + ) + assertThat(undoCallbackCalled).isTrue() + assertThat(getChipbarView().getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) } @Test @@ -241,12 +403,17 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToReceiverFailed().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) } @Test @@ -257,12 +424,17 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToThisDeviceFailed().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) } @Test @@ -316,7 +488,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { } @Test - fun transferToReceiverTriggeredThenFarFromReceiver_viewStillDisplayed() { + fun transferToReceiverTriggeredThenFarFromReceiver_viewStillDisplayedButStillTimesOut() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo, @@ -332,10 +504,14 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { verify(windowManager, never()).removeView(any()) verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + verify(windowManager).removeView(any()) } @Test - fun transferToThisDeviceTriggeredThenFarFromReceiver_viewStillDisplayed() { + fun transferToThisDeviceTriggeredThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo, @@ -351,10 +527,14 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { verify(windowManager, never()).removeView(any()) verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + verify(windowManager).removeView(any()) } @Test - fun transferToReceiverSucceededThenFarFromReceiver_viewStillDisplayed() { + fun transferToReceiverSucceededThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, @@ -370,10 +550,14 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { verify(windowManager, never()).removeView(any()) verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + verify(windowManager).removeView(any()) } @Test - fun transferToThisDeviceSucceededThenFarFromReceiver_viewStillDisplayed() { + fun transferToThisDeviceSucceededThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, @@ -389,54 +573,119 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { verify(windowManager, never()).removeView(any()) verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + verify(windowManager).removeView(any()) } - private fun getChipView(): ViewGroup { + @Test + fun transferToReceiverSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText()) + + // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should + // verify that the new state it triggers operates just like any other state. + getChipbarView().getUndoButton().performClick() + fakeExecutor.runAllReady() + + // Verify that the click updated us to the triggered state + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + // Verify that we didn't remove the chipbar because it's in the triggered state + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + // Verify we eventually remove the chipbar + verify(windowManager).removeView(any()) + } + + @Test + fun transferToThisDeviceSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText()) + + // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should + // verify that the new state it triggers operates just like any other state. + getChipbarView().getUndoButton().performClick() + fakeExecutor.runAllReady() + + // Verify that the click updated us to the triggered state + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + // Verify that we didn't remove the chipbar because it's in the triggered state + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + // Verify we eventually remove the chipbar + verify(windowManager).removeView(any()) + } + + private fun getChipbarView(): ViewGroup { val viewCaptor = ArgumentCaptor.forClass(View::class.java) verify(windowManager).addView(viewCaptor.capture(), any()) return viewCaptor.value as ViewGroup } + private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.start_icon) + private fun ViewGroup.getChipText(): String = (this.requireViewById<TextView>(R.id.text)).text as String - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToStartCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToEndCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo) + private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading) - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo) + private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error) - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback) + private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button) - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) + private fun ChipStateSender.getExpectedStateText(): String? { + return this.getChipTextString(context, OTHER_DEVICE_NAME).loadText(context) + } } +private const val APP_NAME = "Fake app name" private const val OTHER_DEVICE_NAME = "My Tablet" +private const val PACKAGE_NAME = "com.android.systemui" +private const val TIMEOUT = 10000 private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) .addFeature("feature") - .setClientPackageName("com.android.systemui") + .setClientPackageName(PACKAGE_NAME) .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java index 0badd861787d..1bc4719c70b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java @@ -147,6 +147,18 @@ public class ColorSchemeTest extends SysuiTestCase { } @Test + public void testMonochromatic() { + int colorInt = 0xffB3588A; // H350 C50 T50 + ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */, + Style.MONOCHROMATIC /* style */); + int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2); + Assert.assertTrue( + Color.red(neutralMid) == Color.green(neutralMid) + && Color.green(neutralMid) == Color.blue(neutralMid) + ); + } + + @Test @SuppressWarnings("ResultOfMethodCallIgnored") public void testToString() { new ColorScheme(Color.TRANSPARENT, false /* darkTheme */).toString(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt index e2c6ff996199..d6db62aa2f72 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt @@ -20,7 +20,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogcatEchoTracker import com.android.systemui.statusbar.DisableFlagsLogger import com.google.common.truth.Truth.assertThat import org.junit.Test 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 d2c2d58820bc..cd7a949443c9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java @@ -50,7 +50,7 @@ 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.media.controls.ui.MediaHost; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSFragmentComponent; diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java index b847ad07cd72..caf8321949ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java @@ -44,7 +44,7 @@ import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaHost; +import com.android.systemui.media.controls.ui.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTileView; import com.android.systemui.qs.customize.QSCustomizerController; diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt index e539705d9ede..3c867ab32725 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt @@ -7,8 +7,8 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags -import com.android.systemui.media.MediaHost -import com.android.systemui.media.MediaHostState +import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.media.controls.ui.MediaHostState import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.customize.QSCustomizerController 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 1c686c66e31e..5e9c1aaad309 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java @@ -22,7 +22,6 @@ import static junit.framework.Assert.assertNotNull; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -52,6 +51,7 @@ import android.text.SpannableStringBuilder; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import android.widget.TextView; import com.android.systemui.R; @@ -97,6 +97,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { private static final int DEFAULT_ICON_ID = R.drawable.ic_info_outline; private ViewGroup mRootView; + private ViewGroup mSecurityFooterView; private TextView mFooterText; private TestableImageView mPrimaryFooterIcon; private QSSecurityFooter mFooter; @@ -121,21 +122,26 @@ public class QSSecurityFooterTest extends SysuiTestCase { Looper looper = mTestableLooper.getLooper(); Handler mainHandler = new Handler(looper); when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class)); - mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext) + mSecurityFooterView = (ViewGroup) new LayoutInflaterBuilder(mContext) .replace("ImageView", TestableImageView.class) .build().inflate(R.layout.quick_settings_security_footer, null, false); 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); + mFooter = new QSSecurityFooter(mSecurityFooterView, mainHandler, mSecurityController, + looper, mBroadcastDispatcher, mFooterUtils); + mFooterText = mSecurityFooterView.findViewById(R.id.footer_text); + mPrimaryFooterIcon = mSecurityFooterView.findViewById(R.id.primary_footer_icon); when(mSecurityController.getDeviceOwnerComponentOnAnyUser()) .thenReturn(DEVICE_OWNER_COMPONENT); when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT)) .thenReturn(DEVICE_OWNER_TYPE_DEFAULT); + + // mSecurityFooterView must have a ViewGroup parent so that + // DialogLaunchAnimator.Controller.fromView() does not return null. + mRootView = new FrameLayout(mContext); + mRootView.addView(mSecurityFooterView); ViewUtils.attachView(mRootView); mFooter.init(); @@ -153,7 +159,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertEquals(View.GONE, mRootView.getVisibility()); + assertEquals(View.GONE, mSecurityFooterView.getVisibility()); } @Test @@ -165,7 +171,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { TestableLooper.get(this).processAllMessages(); assertEquals(mContext.getString(R.string.quick_settings_disclosure_management), mFooterText.getText()); - assertEquals(View.VISIBLE, mRootView.getVisibility()); + assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } @@ -181,7 +187,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { assertEquals(mContext.getString(R.string.quick_settings_disclosure_named_management, MANAGING_ORGANIZATION), mFooterText.getText()); - assertEquals(View.VISIBLE, mRootView.getVisibility()); + assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } @@ -200,7 +206,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { assertEquals(mContext.getString( R.string.quick_settings_financed_disclosure_named_management, MANAGING_ORGANIZATION), mFooterText.getText()); - assertEquals(View.VISIBLE, mRootView.getVisibility()); + assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } @@ -217,7 +223,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertEquals(View.GONE, mRootView.getVisibility()); + assertEquals(View.GONE, mSecurityFooterView.getVisibility()); } @Test @@ -227,8 +233,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertFalse(mRootView.isClickable()); - assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility()); + assertFalse(mSecurityFooterView.isClickable()); + assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility()); } @Test @@ -241,8 +247,9 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertTrue(mRootView.isClickable()); - assertEquals(View.VISIBLE, mRootView.findViewById(R.id.footer_icon).getVisibility()); + assertTrue(mSecurityFooterView.isClickable()); + assertEquals(View.VISIBLE, + mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility()); } @Test @@ -254,8 +261,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertFalse(mRootView.isClickable()); - assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility()); + assertFalse(mSecurityFooterView.isClickable()); + assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility()); } @Test @@ -734,11 +741,11 @@ public class QSSecurityFooterTest extends SysuiTestCase { @Test public void testDialogUsesDialogLauncher() { when(mSecurityController.isDeviceManaged()).thenReturn(true); - mFooter.onClick(mRootView); + mFooter.onClick(mSecurityFooterView); mTestableLooper.processAllMessages(); - verify(mDialogLaunchAnimator).showFromView(any(), eq(mRootView), any()); + verify(mDialogLaunchAnimator).show(any(), any()); } @Test @@ -775,7 +782,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { ArgumentCaptor<AlertDialog> dialogCaptor = ArgumentCaptor.forClass(AlertDialog.class); mTestableLooper.processAllMessages(); - verify(mDialogLaunchAnimator).showFromView(dialogCaptor.capture(), any(), any()); + verify(mDialogLaunchAnimator).show(dialogCaptor.capture(), any()); AlertDialog dialog = dialogCaptor.getValue(); dialog.create(); @@ -817,8 +824,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { verify(mBroadcastDispatcher).registerReceiverWithHandler(captor.capture(), any(), any(), any()); - // Pretend view is not visible temporarily - mRootView.onVisibilityAggregated(false); + // Pretend view is not attached anymore. + mRootView.removeView(mSecurityFooterView); captor.getValue().onReceive(mContext, new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG)); mTestableLooper.processAllMessages(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java index 3c58b6fc1354..c452872a527e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java @@ -52,6 +52,7 @@ import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.dump.DumpManager; +import com.android.systemui.dump.nano.SystemUIProtoDump; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.qs.QSTile; @@ -114,8 +115,6 @@ public class QSTileHostTest extends SysuiTestCase { @Mock private DumpManager mDumpManager; @Mock - private QSTile.State mMockState; - @Mock private CentralSurfaces mCentralSurfaces; @Mock private QSLogger mQSLogger; @@ -195,7 +194,6 @@ public class QSTileHostTest extends SysuiTestCase { } private void setUpTileFactory() { - when(mMockState.toString()).thenReturn(MOCK_STATE_STRING); // Only create this kind of tiles when(mDefaultFactory.createTile(anyString())).thenAnswer( invocation -> { @@ -209,7 +207,11 @@ public class QSTileHostTest extends SysuiTestCase { } else if ("na".equals(spec)) { return new NotAvailableTile(mQSTileHost); } else if (CUSTOM_TILE_SPEC.equals(spec)) { - return mCustomTile; + QSTile tile = mCustomTile; + QSTile.State s = mock(QSTile.State.class); + s.spec = spec; + when(mCustomTile.getState()).thenReturn(s); + return tile; } else if ("internet".equals(spec) || "wifi".equals(spec) || "cell".equals(spec)) { @@ -647,7 +649,7 @@ public class QSTileHostTest extends SysuiTestCase { @Test public void testSetTileRemoved_removedBySystem() { int user = mUserTracker.getUserId(); - saveSetting("spec1" + CUSTOM_TILE_SPEC); + saveSetting("spec1," + CUSTOM_TILE_SPEC); // This will be done by TileServiceManager mQSTileHost.setTileAdded(CUSTOM_TILE, user, true); @@ -658,6 +660,27 @@ public class QSTileHostTest extends SysuiTestCase { .getBoolean(CUSTOM_TILE.flattenToString(), false)); } + @Test + public void testProtoDump_noTiles() { + SystemUIProtoDump proto = new SystemUIProtoDump(); + mQSTileHost.dumpProto(proto, new String[0]); + + assertEquals(0, proto.tiles.length); + } + + @Test + public void testTilesInOrder() { + saveSetting("spec1," + CUSTOM_TILE_SPEC); + + SystemUIProtoDump proto = new SystemUIProtoDump(); + mQSTileHost.dumpProto(proto, new String[0]); + + assertEquals(2, proto.tiles.length); + assertEquals("spec1", proto.tiles[0].getSpec()); + assertEquals(CUSTOM_TILE.getPackageName(), proto.tiles[1].getComponentName().packageName); + assertEquals(CUSTOM_TILE.getClassName(), proto.tiles[1].getComponentName().className); + } + private SharedPreferences getSharedPreferenecesForUser(int user) { return mUserFileManager.getSharedPreferences(QSTileHost.TILES, 0, user); } @@ -707,12 +730,9 @@ public class QSTileHostTest extends SysuiTestCase { @Override public State newTileState() { - return mMockState; - } - - @Override - public State getState() { - return mMockState; + State s = mock(QSTile.State.class); + when(s.toString()).thenReturn(MOCK_STATE_STRING); + return s; } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt index 6af8e4904a1e..f53e997a331c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt @@ -23,8 +23,8 @@ import com.android.internal.logging.MetricsLogger import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.media.MediaHost -import com.android.systemui.media.MediaHostState +import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.media.controls.ui.MediaHostState import com.android.systemui.plugins.qs.QSTile import com.android.systemui.plugins.qs.QSTileView import com.android.systemui.qs.customize.QSCustomizerController diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt new file mode 100644 index 000000000000..629c663943db --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt @@ -0,0 +1,104 @@ +package com.android.systemui.qs + +import android.content.ComponentName +import android.service.quicksettings.Tile +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.external.CustomTile +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class TileStateToProtoTest : SysuiTestCase() { + + companion object { + private const val TEST_LABEL = "label" + private const val TEST_SUBTITLE = "subtitle" + private const val TEST_SPEC = "spec" + private val TEST_COMPONENT = ComponentName("test_pkg", "test_cls") + } + + @Test + fun platformTile_INACTIVE() { + val state = + QSTile.State().apply { + spec = TEST_SPEC + label = TEST_LABEL + secondaryLabel = TEST_SUBTITLE + state = Tile.STATE_INACTIVE + } + val proto = state.toProto() + + assertThat(proto).isNotNull() + assertThat(proto?.hasSpec()).isTrue() + assertThat(proto?.spec).isEqualTo(TEST_SPEC) + assertThat(proto?.hasComponentName()).isFalse() + assertThat(proto?.label).isEqualTo(TEST_LABEL) + assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE) + assertThat(proto?.state).isEqualTo(Tile.STATE_INACTIVE) + assertThat(proto?.hasBooleanState()).isFalse() + } + + @Test + fun componentTile_UNAVAILABLE() { + val state = + QSTile.State().apply { + spec = CustomTile.toSpec(TEST_COMPONENT) + label = TEST_LABEL + secondaryLabel = TEST_SUBTITLE + state = Tile.STATE_UNAVAILABLE + } + val proto = state.toProto() + + assertThat(proto).isNotNull() + assertThat(proto?.hasSpec()).isFalse() + assertThat(proto?.hasComponentName()).isTrue() + val componentName = proto?.componentName + assertThat(componentName?.packageName).isEqualTo(TEST_COMPONENT.packageName) + assertThat(componentName?.className).isEqualTo(TEST_COMPONENT.className) + assertThat(proto?.label).isEqualTo(TEST_LABEL) + assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE) + assertThat(proto?.state).isEqualTo(Tile.STATE_UNAVAILABLE) + assertThat(proto?.hasBooleanState()).isFalse() + } + + @Test + fun booleanState_ACTIVE() { + val state = + QSTile.BooleanState().apply { + spec = TEST_SPEC + label = TEST_LABEL + secondaryLabel = TEST_SUBTITLE + state = Tile.STATE_ACTIVE + value = true + } + val proto = state.toProto() + + assertThat(proto).isNotNull() + assertThat(proto?.hasSpec()).isTrue() + assertThat(proto?.spec).isEqualTo(TEST_SPEC) + assertThat(proto?.hasComponentName()).isFalse() + assertThat(proto?.label).isEqualTo(TEST_LABEL) + assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE) + assertThat(proto?.state).isEqualTo(Tile.STATE_ACTIVE) + assertThat(proto?.hasBooleanState()).isTrue() + assertThat(proto?.booleanState).isTrue() + } + + @Test + fun noSpec_returnsNull() { + val state = + QSTile.State().apply { + label = TEST_LABEL + secondaryLabel = TEST_SUBTITLE + state = Tile.STATE_ACTIVE + } + val proto = state.toProto() + + assertThat(proto).isNull() + } +} 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 index 3c258077c29d..2c2ddbb9b8c5 100644 --- 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 @@ -23,13 +23,13 @@ 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.animation.Expandable import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.globalactions.GlobalActionsDialogLite @@ -70,13 +70,13 @@ class FooterActionsInteractorTest : SysuiTestCase() { 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) + underTest.showDeviceMonitoringDialog(quickSettingsContext, null) verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null) + + val expandable = mock<Expandable>() + underTest.showDeviceMonitoringDialog(quickSettingsContext, expandable) + verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, expandable) } @Test @@ -85,8 +85,8 @@ class FooterActionsInteractorTest : SysuiTestCase() { val underTest = utils.footerActionsInteractor(uiEventLogger = uiEventLogger) val globalActionsDialogLite = mock<GlobalActionsDialogLite>() - val view = mock<View>() - underTest.showPowerMenuDialog(globalActionsDialogLite, view) + val expandable = mock<Expandable>() + underTest.showPowerMenuDialog(globalActionsDialogLite, expandable) // Event is logged. val logs = uiEventLogger.logs @@ -99,7 +99,7 @@ class FooterActionsInteractorTest : SysuiTestCase() { .showOrHideDialog( /* keyguardShowing= */ false, /* isDeviceProvisioned= */ true, - view, + expandable, ) } @@ -167,11 +167,11 @@ class FooterActionsInteractorTest : SysuiTestCase() { userSwitchDialogController = userSwitchDialogController, ) - val view = mock<View>() - underTest.showUserSwitcher(view) + val expandable = mock<Expandable>() + underTest.showUserSwitcher(context, expandable) // Dialog is shown. - verify(userSwitchDialogController).showDialog(view) + verify(userSwitchDialogController).showDialog(context, expandable) } @Test @@ -184,12 +184,9 @@ class FooterActionsInteractorTest : SysuiTestCase() { 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) + // The clicked expandable. + val expandable = mock<Expandable>() + underTest.showUserSwitcher(context, expandable) // Dialog is shown. val intentCaptor = argumentCaptor<Intent>() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt index da52a9b1a3c2..3131f60893c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt @@ -30,9 +30,11 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.qs.QSUserSwitcherEvent +import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.user.data.source.UserRecord import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -40,20 +42,27 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest class UserDetailViewAdapterTest : SysuiTestCase() { - @Mock private lateinit var mUserSwitcherController: UserSwitcherController - @Mock private lateinit var mParent: ViewGroup - @Mock private lateinit var mUserDetailItemView: UserDetailItemView - @Mock private lateinit var mOtherView: View - @Mock private lateinit var mInflatedUserDetailItemView: UserDetailItemView - @Mock private lateinit var mLayoutInflater: LayoutInflater + @Mock + private lateinit var mUserSwitcherController: UserSwitcherController + @Mock + private lateinit var mParent: ViewGroup + @Mock + private lateinit var mUserDetailItemView: UserDetailItemView + @Mock + private lateinit var mOtherView: View + @Mock + private lateinit var mInflatedUserDetailItemView: UserDetailItemView + @Mock + private lateinit var mLayoutInflater: LayoutInflater private var falsingManagerFake: FalsingManagerFake = FalsingManagerFake() private lateinit var adapter: UserDetailView.Adapter private lateinit var uiEventLogger: UiEventLoggerFake @@ -66,10 +75,12 @@ class UserDetailViewAdapterTest : SysuiTestCase() { mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, mLayoutInflater) `when`(mLayoutInflater.inflate(anyInt(), any(ViewGroup::class.java), anyBoolean())) - .thenReturn(mInflatedUserDetailItemView) + .thenReturn(mInflatedUserDetailItemView) `when`(mParent.context).thenReturn(mContext) - adapter = UserDetailView.Adapter(mContext, mUserSwitcherController, uiEventLogger, - falsingManagerFake) + adapter = UserDetailView.Adapter( + mContext, mUserSwitcherController, uiEventLogger, + falsingManagerFake + ) mPicture = UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.ic_avatar_user)) } @@ -139,6 +150,20 @@ class UserDetailViewAdapterTest : SysuiTestCase() { clickableTest(false, false, mUserDetailItemView, true) } + @Test + fun testManageUsersIsNotAvailable() { + assertNull(adapter.users.find { it.isManageUsers }) + } + + @Test + fun clickDismissDialog() { + val shower: UserSwitchDialogController.DialogShower = + mock(UserSwitchDialogController.DialogShower::class.java) + adapter.injectDialogShower(shower) + adapter.onUserListItemClicked(createUserRecord(current = true, guest = false), shower) + verify(shower).dismiss() + } + private fun createUserRecord(current: Boolean, guest: Boolean) = UserRecord( UserInfo(0 /* id */, "name", 0 /* flags */), diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt index 9d908fdfb976..0a34810f4d3f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt @@ -20,12 +20,12 @@ import android.content.DialogInterface import android.content.Intent import android.provider.Settings import android.testing.AndroidTestingRunner -import android.view.View import android.widget.Button import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PseudoGridView @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -63,7 +64,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { @Mock private lateinit var userDetailViewAdapter: UserDetailView.Adapter @Mock - private lateinit var launchView: View + private lateinit var launchExpandable: Expandable @Mock private lateinit var neutralButton: Button @Mock @@ -79,7 +80,6 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - `when`(launchView.context).thenReturn(mContext) `when`(dialog.context).thenReturn(mContext) controller = UserSwitchDialogController( @@ -94,32 +94,34 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { @Test fun showDialog_callsDialogShow() { - controller.showDialog(launchView) - verify(dialogLaunchAnimator).showFromView(eq(dialog), eq(launchView), any(), anyBoolean()) + val launchController = mock<DialogLaunchAnimator.Controller>() + `when`(launchExpandable.dialogLaunchController(any())).thenReturn(launchController) + controller.showDialog(context, launchExpandable) + verify(dialogLaunchAnimator).show(eq(dialog), eq(launchController), anyBoolean()) verify(uiEventLogger).log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN) } @Test fun dialog_showForAllUsers() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog).setShowForAllUsers(true) } @Test fun dialog_cancelOnTouchOutside() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog).setCanceledOnTouchOutside(true) } @Test fun adapterAndGridLinked() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(userDetailViewAdapter).linkToViewGroup(any<PseudoGridView>()) } @Test fun doneButtonLogsCorrectly() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog).setPositiveButton(anyInt(), capture(clickCaptor)) @@ -132,7 +134,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun clickSettingsButton_noFalsing_opensSettings() { `when`(falsingManager.isFalseTap(anyInt())).thenReturn(false) - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog) .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */) @@ -153,7 +155,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun clickSettingsButton_Falsing_notOpensSettings() { `when`(falsingManager.isFalseTap(anyInt())).thenReturn(true) - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog) .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */) diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt new file mode 100644 index 000000000000..1130bda9b881 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt @@ -0,0 +1,107 @@ +/* + * 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.brightness + +import android.content.Intent +import android.graphics.Rect +import android.os.Handler +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.View +import android.view.ViewGroup +import androidx.test.filters.SmallTest +import androidx.test.rule.ActivityTestRule +import androidx.test.runner.intercepting.SingleActivityFactory +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.util.mockito.any +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class BrightnessDialogTest : SysuiTestCase() { + + @Mock private lateinit var brightnessSliderControllerFactory: BrightnessSliderController.Factory + @Mock private lateinit var backgroundHandler: Handler + @Mock private lateinit var brightnessSliderController: BrightnessSliderController + + @Rule + @JvmField + var activityRule = + ActivityTestRule( + object : SingleActivityFactory<TestDialog>(TestDialog::class.java) { + override fun create(intent: Intent?): TestDialog { + return TestDialog( + fakeBroadcastDispatcher, + brightnessSliderControllerFactory, + backgroundHandler + ) + } + }, + false, + false + ) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + `when`(brightnessSliderControllerFactory.create(any(), any())) + .thenReturn(brightnessSliderController) + `when`(brightnessSliderController.rootView).thenReturn(View(context)) + + activityRule.launchActivity(null) + } + + @After + fun tearDown() { + activityRule.finishActivity() + } + + @Test + fun testGestureExclusion() { + val frame = activityRule.activity.requireViewById<View>(R.id.brightness_mirror_container) + + val lp = frame.layoutParams as ViewGroup.MarginLayoutParams + val horizontalMargin = + activityRule.activity.resources.getDimensionPixelSize( + R.dimen.notification_side_paddings + ) + assertThat(lp.leftMargin).isEqualTo(horizontalMargin) + assertThat(lp.rightMargin).isEqualTo(horizontalMargin) + + assertThat(frame.systemGestureExclusionRects.size).isEqualTo(1) + val exclusion = frame.systemGestureExclusionRects[0] + assertThat(exclusion) + .isEqualTo(Rect(-horizontalMargin, 0, frame.width + horizontalMargin, frame.height)) + } + + class TestDialog( + broadcastDispatcher: BroadcastDispatcher, + brightnessSliderControllerFactory: BrightnessSliderController.Factory, + backgroundHandler: Handler + ) : BrightnessDialog(broadcastDispatcher, brightnessSliderControllerFactory, backgroundHandler) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt index 0151822f871c..14a3bc147808 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt @@ -659,6 +659,51 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() { verify(privacyIconsController, never()).onParentInvisible() } + @Test + fun clockPivotYInCenter() { + val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java) + verify(clock).addOnLayoutChangeListener(capture(captor)) + var height = 100 + val width = 50 + + clock.executeLayoutChange(0, 0, width, height, captor.value) + verify(clock).pivotY = height.toFloat() / 2 + + height = 150 + clock.executeLayoutChange(0, 0, width, height, captor.value) + verify(clock).pivotY = height.toFloat() / 2 + } + + private fun View.executeLayoutChange( + left: Int, + top: Int, + right: Int, + bottom: Int, + listener: View.OnLayoutChangeListener + ) { + val oldLeft = this.left + val oldTop = this.top + val oldRight = this.right + val oldBottom = this.bottom + whenever(this.left).thenReturn(left) + whenever(this.top).thenReturn(top) + whenever(this.right).thenReturn(right) + whenever(this.bottom).thenReturn(bottom) + whenever(this.height).thenReturn(bottom - top) + whenever(this.width).thenReturn(right - left) + listener.onLayoutChange( + this, + oldLeft, + oldTop, + oldRight, + oldBottom, + left, + top, + right, + bottom + ) + } + private fun createWindowInsets( topCutout: Rect? = Rect() ): WindowInsets { 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 c0dae03023c5..02f28a235b95 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -18,6 +18,7 @@ package com.android.systemui.shade; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED; @@ -33,11 +34,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -76,6 +79,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.internal.util.CollectionUtils; import com.android.internal.util.LatencyTracker; +import com.android.keyguard.FaceAuthApiRequestReason; import com.android.keyguard.KeyguardClockSwitch; import com.android.keyguard.KeyguardClockSwitchController; import com.android.keyguard.KeyguardStatusView; @@ -93,7 +97,6 @@ import com.android.systemui.biometrics.AuthController; import com.android.systemui.camera.CameraGestureHelper; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; -import com.android.systemui.controls.dagger.ControlsComponent; import com.android.systemui.doze.DozeLog; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; @@ -102,14 +105,15 @@ import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; -import com.android.systemui.media.KeyguardMediaController; -import com.android.systemui.media.MediaDataManager; -import com.android.systemui.media.MediaHierarchyManager; +import com.android.systemui.media.controls.pipeline.MediaDataManager; +import com.android.systemui.media.controls.ui.KeyguardMediaController; +import com.android.systemui.media.controls.ui.MediaHierarchyManager; import com.android.systemui.model.SysUiState; +import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; -import com.android.systemui.qrcodescanner.controller.QRCodeScannerController; +import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSFragment; import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.shade.transition.ShadeTransitionController; @@ -165,7 +169,6 @@ import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.time.SystemClock; -import com.android.systemui.wallet.controller.QuickAccessWalletController; import com.android.wm.shell.animation.FlingAnimationUtils; import org.junit.After; @@ -173,6 +176,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; @@ -251,17 +256,15 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private KeyguardMediaController mKeyguardMediaController; @Mock private PrivacyDotViewController mPrivacyDotViewController; @Mock private NavigationModeController mNavigationModeController; + @Mock private NavigationBarController mNavigationBarController; @Mock private LargeScreenShadeHeaderController mLargeScreenShadeHeaderController; @Mock private ContentResolver mContentResolver; @Mock private TapAgainViewController mTapAgainViewController; @Mock private KeyguardIndicationController mKeyguardIndicationController; @Mock private FragmentService mFragmentService; @Mock private FragmentHostManager mFragmentHostManager; - @Mock private QuickAccessWalletController mQuickAccessWalletController; - @Mock private QRCodeScannerController mQrCodeScannerController; @Mock private NotificationRemoteInputManager mNotificationRemoteInputManager; @Mock private RecordingController mRecordingController; - @Mock private ControlsComponent mControlsComponent; @Mock private LockscreenGestureLogger mLockscreenGestureLogger; @Mock private DumpManager mDumpManager; @Mock private InteractionJankMonitor mInteractionJankMonitor; @@ -282,6 +285,10 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private ViewTreeObserver mViewTreeObserver; @Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel; @Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor; + @Mock private MotionEvent mDownMotionEvent; + @Captor + private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener> + mEmptySpaceClickListenerCaptor; private NotificationPanelViewController.TouchHandler mTouchHandler; private ConfigurationController mConfigurationController; @@ -373,6 +380,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { NotificationWakeUpCoordinator coordinator = new NotificationWakeUpCoordinator( + mDumpManager, mock(HeadsUpManagerPhone.class), new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager, mInteractionJankMonitor), @@ -388,6 +396,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mConfigurationController, mStatusBarStateController, mFalsingManager, + mShadeExpansionStateManager, mLockscreenShadeTransitionController, new FalsingCollectorFake(), mDumpManager); @@ -425,6 +434,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { when(mView.getViewTreeObserver()).thenReturn(mViewTreeObserver); when(mView.getParent()).thenReturn(mViewParent); when(mQs.getHeader()).thenReturn(mQsHeader); + when(mDownMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN); + when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState); mMainHandler = new Handler(Looper.getMainLooper()); NotificationPanelViewController.PanelEventsEmitter panelEventsEmitter = @@ -468,6 +479,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mPrivacyDotViewController, mTapAgainViewController, mNavigationModeController, + mNavigationBarController, mFragmentService, mContentResolver, mRecordingController, @@ -512,6 +524,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { .addCallback(mNotificationPanelViewController.mStatusBarStateListener); mNotificationPanelViewController .setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class)); + verify(mNotificationStackScrollLayoutController) + .setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture()); } @After @@ -750,6 +764,38 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { } @Test + public void testOnTouchEvent_expansionResumesAfterBriefTouch() { + // Start shade collapse with swipe up + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */, + 0 /* metaState */)); + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + assertThat(mNotificationPanelViewController.getClosing()).isTrue(); + assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue(); + + // simulate touch that does not exceed touch slop + onTouchEvent(MotionEvent.obtain(2L /* downTime */, + 2L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + mNotificationPanelViewController.setTouchSlopExceeded(false); + + onTouchEvent(MotionEvent.obtain(2L /* downTime */, + 2L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + // fling should still be called after a touch that does not exceed touch slop + assertThat(mNotificationPanelViewController.getClosing()).isTrue(); + assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue(); + } + + @Test public void handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() { when(mCommandQueue.panelsEnabled()).thenReturn(false); @@ -1540,6 +1586,103 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { ); } + @Test + public void onEmptySpaceClicked_notDozingAndOnKeyguard_requestsFaceAuth() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(KEYGUARD); + mNotificationPanelViewController.setDozing(false, false); + + // This sets the dozing state that is read when onMiddleClicked is eventually invoked. + mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mUpdateMonitor).requestFaceAuth(true, + FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED); + } + + @Test + public void onEmptySpaceClicked_notDozingAndFaceDetectionIsNotRunning_startsUnlockAnimation() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(KEYGUARD); + mNotificationPanelViewController.setDozing(false, false); + when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(false); + + // This sets the dozing state that is read when onMiddleClicked is eventually invoked. + mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mNotificationStackScrollLayoutController).setUnlockHintRunning(true); + } + + @Test + public void onEmptySpaceClicked_notDozingAndFaceDetectionIsRunning_doesNotStartUnlockHint() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(KEYGUARD); + mNotificationPanelViewController.setDozing(false, false); + when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(true); + + // This sets the dozing state that is read when onMiddleClicked is eventually invoked. + mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mNotificationStackScrollLayoutController, never()).setUnlockHintRunning(true); + } + + @Test + public void onEmptySpaceClicked_whenDozingAndOnKeyguard_doesNotRequestFaceAuth() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(KEYGUARD); + mNotificationPanelViewController.setDozing(true, false); + + // This sets the dozing state that is read when onMiddleClicked is eventually invoked. + mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString()); + } + + @Test + public void onEmptySpaceClicked_whenStatusBarShadeLocked_doesNotRequestFaceAuth() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(SHADE_LOCKED); + + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString()); + + } + + /** + * When shade is flinging to close and this fling is not intercepted, + * {@link AmbientState#setIsClosing(boolean)} should be called before + * {@link NotificationStackScrollLayoutController#onExpansionStopped()} + * to ensure scrollY can be correctly set to be 0 + */ + @Test + public void onShadeFlingClosingEnd_mAmbientStateSetClose_thenOnExpansionStopped() { + // Given: Shade is expanded + mNotificationPanelViewController.notifyExpandingFinished(); + mNotificationPanelViewController.setIsClosing(false); + + // When: Shade flings to close not canceled + mNotificationPanelViewController.notifyExpandingStarted(); + mNotificationPanelViewController.setIsClosing(true); + mNotificationPanelViewController.onFlingEnd(false); + + // Then: AmbientState's mIsClosing should be set to false + // before mNotificationStackScrollLayoutController.onExpansionStopped() is called + // to ensure NotificationStackScrollLayout.resetScrollPosition() -> resetScrollPosition + // -> setOwnScrollY(0) can set scrollY to 0 when shade is closed + InOrder inOrder = inOrder(mAmbientState, mNotificationStackScrollLayoutController); + inOrder.verify(mAmbientState).setIsClosing(false); + inOrder.verify(mNotificationStackScrollLayoutController).onExpansionStopped(); + } + private static MotionEvent createMotionEvent(int x, int y, int action) { return MotionEvent.obtain( /* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt index 12ef036d89d0..bdafc7df33bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt @@ -66,6 +66,8 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { @Mock private lateinit var largeScreenShadeHeaderController: LargeScreenShadeHeaderController @Mock + private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager + @Mock private lateinit var featureFlags: FeatureFlags @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> @@ -96,6 +98,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { navigationModeController, overviewProxyService, largeScreenShadeHeaderController, + shadeExpansionStateManager, featureFlags, delayableExecutor ) @@ -380,6 +383,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { navigationModeController, overviewProxyService, largeScreenShadeHeaderController, + shadeExpansionStateManager, featureFlags, delayableExecutor ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index ad3d3d2958cb..95cf9d60b511 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -88,6 +88,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @Mock private KeyguardStateController mKeyguardStateController; @Mock private ScreenOffAnimationController mScreenOffAnimationController; @Mock private AuthController mAuthController; + @Mock private ShadeExpansionStateManager mShadeExpansionStateManager; @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters; private NotificationShadeWindowControllerImpl mNotificationShadeWindowController; @@ -103,7 +104,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController, mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController, mColorExtractor, mDumpManager, mKeyguardStateController, - mScreenOffAnimationController, mAuthController) { + mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager) { @Override protected boolean isDebuggable() { return false; diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt index eb34561d15a0..cc45cf88fa18 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt @@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.animation.TextAnimator +import com.android.systemui.util.mockito.any import org.junit.Before import org.junit.Rule import org.junit.Test @@ -55,7 +56,7 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.animateAppearOnLockscreen() clockView.measure(50, 50) - verify(mockTextAnimator).glyphFilter = null + verify(mockTextAnimator).glyphFilter = any() verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, false, 350L, null, 0L, null) verifyNoMoreInteractions(mockTextAnimator) } @@ -66,7 +67,7 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.measure(50, 50) clockView.animateAppearOnLockscreen() - verify(mockTextAnimator, times(2)).glyphFilter = null + verify(mockTextAnimator, times(2)).glyphFilter = any() verify(mockTextAnimator).setTextStyle(100, -1.0f, 200, false, 0L, null, 0L, null) verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, true, 350L, null, 0L, null) verifyNoMoreInteractions(mockTextAnimator) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt index ffb41e5378bd..70cbc64c79f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt @@ -19,6 +19,7 @@ import android.content.ContentResolver import android.content.Context import android.graphics.drawable.Drawable import android.os.Handler +import android.os.UserHandle import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -104,13 +105,14 @@ class ClockRegistryTest : SysuiTestCase() { mockContext, mockPluginManager, mockHandler, - fakeDefaultProvider + isEnabled = true, + userHandle = UserHandle.USER_ALL, + defaultClockProvider = fakeDefaultProvider ) { override var currentClockId: ClockId get() = settingValue set(value) { settingValue = value } } - registry.isEnabled = true verify(mockPluginManager) .addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java), eq(true)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java index cf5fa87272c7..64dc9568030b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java @@ -16,6 +16,11 @@ package com.android.systemui.shared.system; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.view.RemoteAnimationTarget.MODE_CHANGING; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; @@ -25,11 +30,6 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_STANDARD; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CHANGING; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -40,6 +40,7 @@ import android.app.ActivityManager; import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -73,12 +74,12 @@ public class RemoteTransitionTest extends SysuiTestCase { .addChange(TRANSIT_OPEN, FLAG_IS_WALLPAPER, null /* taskInfo */) .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */).build(); // Check apps extraction - RemoteAnimationTargetCompat[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined, + RemoteAnimationTarget[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined, mock(SurfaceControl.Transaction.class), null /* leashes */); assertEquals(2, wrapped.length); int changeLayer = -1; int closeLayer = -1; - for (RemoteAnimationTargetCompat t : wrapped) { + for (RemoteAnimationTarget t : wrapped) { if (t.mode == MODE_CHANGING) { changeLayer = t.prefixOrderIndex; } else if (t.mode == MODE_CLOSING) { @@ -91,14 +92,14 @@ public class RemoteTransitionTest extends SysuiTestCase { assertTrue(closeLayer < changeLayer); // Check wallpaper extraction - RemoteAnimationTargetCompat[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined, + RemoteAnimationTarget[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined, true /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */); assertEquals(1, wallps.length); assertTrue(wallps[0].prefixOrderIndex < closeLayer); assertEquals(MODE_OPENING, wallps[0].mode); // Check non-apps extraction - RemoteAnimationTargetCompat[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined, + RemoteAnimationTarget[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined, false /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */); assertEquals(1, nonApps.length); assertTrue(nonApps[0].prefixOrderIndex < closeLayer); @@ -115,9 +116,9 @@ public class RemoteTransitionTest extends SysuiTestCase { change.setTaskInfo(createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_HOME)); change.setEndAbsBounds(endBounds); change.setEndRelOffset(0, 0); - final RemoteAnimationTargetCompat wrapped = new RemoteAnimationTargetCompat(change, - 0 /* order */, tinfo, mock(SurfaceControl.Transaction.class)); - assertEquals(ACTIVITY_TYPE_HOME, wrapped.activityType); + RemoteAnimationTarget wrapped = RemoteAnimationTargetCompat.newTarget( + change, 0 /* order */, tinfo, mock(SurfaceControl.Transaction.class), null); + assertEquals(ACTIVITY_TYPE_HOME, wrapped.windowConfiguration.getActivityType()); assertEquals(new Rect(0, 0, 100, 140), wrapped.localBounds); assertEquals(endBounds, wrapped.screenSpaceBounds); assertTrue(wrapped.isTranslucent); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt index 5b34a95d4fb0..b761647e24e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt @@ -17,58 +17,58 @@ import org.mockito.MockitoAnnotations @SmallTest class UncaughtExceptionPreHandlerTest : SysuiTestCase() { - private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager + private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager - @Mock private lateinit var mockHandler: UncaughtExceptionHandler + @Mock private lateinit var mockHandler: UncaughtExceptionHandler - @Mock private lateinit var mockHandler2: UncaughtExceptionHandler + @Mock private lateinit var mockHandler2: UncaughtExceptionHandler - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - Thread.setUncaughtExceptionPreHandler(null) - preHandlerManager = UncaughtExceptionPreHandlerManager() - } + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + Thread.setUncaughtExceptionPreHandler(null) + preHandlerManager = UncaughtExceptionPreHandlerManager() + } - @Test - fun registerHandler_registersOnceOnly() { - preHandlerManager.registerHandler(mockHandler) - preHandlerManager.registerHandler(mockHandler) - preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) - verify(mockHandler, only()).uncaughtException(any(), any()) - } + @Test + fun registerHandler_registersOnceOnly() { + preHandlerManager.registerHandler(mockHandler) + preHandlerManager.registerHandler(mockHandler) + preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) + verify(mockHandler, only()).uncaughtException(any(), any()) + } - @Test - fun registerHandler_setsUncaughtExceptionPreHandler() { - Thread.setUncaughtExceptionPreHandler(null) - preHandlerManager.registerHandler(mockHandler) - assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull() - } + @Test + fun registerHandler_setsUncaughtExceptionPreHandler() { + Thread.setUncaughtExceptionPreHandler(null) + preHandlerManager.registerHandler(mockHandler) + assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull() + } - @Test - fun registerHandler_preservesOriginalHandler() { - Thread.setUncaughtExceptionPreHandler(mockHandler) - preHandlerManager.registerHandler(mockHandler2) - preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) - verify(mockHandler, only()).uncaughtException(any(), any()) - } + @Test + fun registerHandler_preservesOriginalHandler() { + Thread.setUncaughtExceptionPreHandler(mockHandler) + preHandlerManager.registerHandler(mockHandler2) + preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) + verify(mockHandler, only()).uncaughtException(any(), any()) + } - @Test - @Ignore - fun registerHandler_toleratesHandlersThatThrow() { - `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException()) - preHandlerManager.registerHandler(mockHandler2) - preHandlerManager.registerHandler(mockHandler) - preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) - verify(mockHandler2, only()).uncaughtException(any(), any()) - verify(mockHandler, only()).uncaughtException(any(), any()) - } + @Test + @Ignore + fun registerHandler_toleratesHandlersThatThrow() { + `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException()) + preHandlerManager.registerHandler(mockHandler2) + preHandlerManager.registerHandler(mockHandler) + preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) + verify(mockHandler2, only()).uncaughtException(any(), any()) + verify(mockHandler, only()).uncaughtException(any(), any()) + } - @Test - fun registerHandler_doesNotSetUpTwice() { - UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2) - assertThrows(IllegalStateException::class.java) { - preHandlerManager.registerHandler(mockHandler) + @Test + fun registerHandler_doesNotSetUpTwice() { + UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2) + assertThrows(IllegalStateException::class.java) { + preHandlerManager.registerHandler(mockHandler) + } } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt index 8cb530c355bd..5fc0ffe42f55 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt @@ -4,7 +4,7 @@ import android.testing.AndroidTestingRunner import android.util.DisplayMetrics import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.phone.LSShadeTransitionLogger import com.android.systemui.statusbar.phone.LockscreenGestureLogger diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt index 8643e86acef2..3d11ced6207d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt @@ -10,7 +10,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.WakefulnessLifecycle -import com.android.systemui.media.MediaHierarchyManager +import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QS import com.android.systemui.shade.NotificationPanelViewController diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt index 44cbe51a30ac..fbb8ebfb3e3b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager @@ -56,6 +57,7 @@ class PulseExpansionHandlerTest : SysuiTestCase() { private val configurationController: ConfigurationController = mock() private val statusBarStateController: StatusBarStateController = mock() private val falsingManager: FalsingManager = mock() + private val shadeExpansionStateManager: ShadeExpansionStateManager = mock() private val lockscreenShadeTransitionController: LockscreenShadeTransitionController = mock() private val falsingCollector: FalsingCollector = mock() private val dumpManager: DumpManager = mock() @@ -65,7 +67,8 @@ class PulseExpansionHandlerTest : SysuiTestCase() { fun setUp() { whenever(expandableView.collapsedHeight).thenReturn(collapsedHeight) - pulseExpansionHandler = PulseExpansionHandler( + pulseExpansionHandler = + PulseExpansionHandler( mContext, wakeUpCoordinator, bypassController, @@ -74,10 +77,11 @@ class PulseExpansionHandlerTest : SysuiTestCase() { configurationController, statusBarStateController, falsingManager, + shadeExpansionStateManager, lockscreenShadeTransitionController, falsingCollector, dumpManager - ) + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java index f8a0d2fc415c..9c65fac1af45 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java @@ -70,7 +70,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.telephony.TelephonyListenerManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java index ed8a3e16cdd1..4bed4a19b3d9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java @@ -38,7 +38,7 @@ import android.testing.TestableLooper.RunWithLooper; import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.net.DataUsageController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.CarrierConfigTracker; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java index a76676e01c15..d5f5105036d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java @@ -43,7 +43,7 @@ import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.net.DataUsageController; import com.android.systemui.R; import com.android.systemui.dump.DumpManager; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.CarrierConfigTracker; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java index 4b458f5a9123..dda7fadde2d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java @@ -31,8 +31,8 @@ public class GroupEntryBuilder { private long mCreationTime = 0; @Nullable private GroupEntry mParent = GroupEntry.ROOT_ENTRY; private NotifSection mNotifSection; - private NotificationEntry mSummary = null; - private List<NotificationEntry> mChildren = new ArrayList<>(); + @Nullable private NotificationEntry mSummary = null; + private final List<NotificationEntry> mChildren = new ArrayList<>(); /** Builds a new instance of GroupEntry */ public GroupEntry build() { @@ -41,7 +41,9 @@ public class GroupEntryBuilder { ge.getAttachState().setSection(mNotifSection); ge.setSummary(mSummary); - mSummary.setParent(ge); + if (mSummary != null) { + mSummary.setParent(ge); + } for (NotificationEntry child : mChildren) { ge.addChild(child); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java index 851517e1e35b..3b05321e1a6b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java @@ -1498,45 +1498,8 @@ public class NotifCollectionTest extends SysuiTestCase { } @Test - public void testMissingRankingWhenRemovalFeatureIsDisabled() { + public void testMissingRanking() { // GIVEN a pipeline with one two notifications - when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(false); - String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key; - String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key; - NotificationEntry entry1 = mCollectionListener.getEntry(key1); - NotificationEntry entry2 = mCollectionListener.getEntry(key2); - clearInvocations(mCollectionListener); - - // GIVEN the message for removing key1 gets does not reach NotifCollection - Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1); - // WHEN the message for removing key2 arrives - mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL); - - // THEN only entry2 gets removed - verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL)); - verify(mCollectionListener).onEntryCleanUp(eq(entry2)); - verify(mCollectionListener).onRankingApplied(); - verifyNoMoreInteractions(mCollectionListener); - verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any()); - verify(mLogger, never()).logRecoveredRankings(any(), anyInt()); - clearInvocations(mCollectionListener, mLogger); - - // WHEN a ranking update includes key1 again - mNoMan.setRanking(key1, ranking1); - mNoMan.issueRankingUpdate(); - - // VERIFY that we do nothing but log the 'recovery' - verify(mCollectionListener).onRankingUpdate(any()); - verify(mCollectionListener).onRankingApplied(); - verifyNoMoreInteractions(mCollectionListener); - verify(mLogger, never()).logMissingRankings(any(), anyInt(), any()); - verify(mLogger).logRecoveredRankings(eq(List.of(key1)), eq(0)); - } - - @Test - public void testMissingRankingWhenRemovalFeatureIsEnabled() { - // GIVEN a pipeline with one two notifications - when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(true); String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key; String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key; NotificationEntry entry1 = mCollectionListener.getEntry(key1); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java index 82e32b2fdc64..09f8a10f88c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java @@ -34,10 +34,12 @@ import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; 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 static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -135,6 +137,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(this); allowTestableLooperAsMainThread(); + when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true); mListBuilder = new ShadeListBuilder( mDumpManager, @@ -1995,22 +1998,89 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test + public void testActiveOrdering_withLegacyStability() { + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false); + assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change + assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X + assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change + assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X + assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap + } + + @Test + public void testStableOrdering_withLegacyStability() { + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false); + mStabilityManager.setAllowEntryReordering(false); + assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change + assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG", false); // X + assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // no change + assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG", false); // Z and X + assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG", false); // Z and X + gap + } + + @Test public void testStableOrdering() { + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true); mStabilityManager.setAllowEntryReordering(false); - assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG"); // X - assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG"); // no change - assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG"); // Z and X - assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG"); // Z and X + gap - verify(mStabilityManager, times(4)).onEntryReorderSuppressed(); + // No input or output + assertOrder("", "", "", true); + // Remove everything + assertOrder("ABCDEFG", "", "", true); + // Literally no changes + assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); + + // No stable order + assertOrder("", "ABCDEFG", "ABCDEFG", true); + + // F moved after A, and... + assertOrder("ABCDEFG", "AFBCDEG", "ABCDEFG", false); // No other changes + assertOrder("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false); // Insert X before F + assertOrder("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false); // Insert X after F + assertOrder("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false); // Insert X where F was + + // B moved after F, and... + assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // No other changes + assertOrder("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false); // Insert X before B + assertOrder("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false); // Insert X after B + assertOrder("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false); // Insert X where B was + + // Swap F and B, and... + assertOrder("ABCDEFG", "AFCDEBG", "ABCDEFG", false); // No other changes + assertOrder("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false); // Insert X before F + assertOrder("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false); // Insert X after F + assertOrder("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false); // Insert X between CD (or: ABCXDEFG) + assertOrder("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false); // Insert X between DE (or: ABCDEFXG) + assertOrder("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false); // Insert X before B + assertOrder("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false); // Insert X after B + + // Remove a bunch of entries at once + assertOrder("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true); + + // Remove a bunch of entries and scramble + assertOrder("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false); + + // Add a bunch of entries at once + assertOrder("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true); + + // Add a bunch of entries and reverse originals + // NOTE: Some of these don't have obviously correct answers + assertOrder("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false); // appended + assertOrder("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false); // prepended + assertOrder("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false); // closer to back: append + assertOrder("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false); // closer to front: prepend + assertOrder("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false); // split new entries + + // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout + assertOrder("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false); } @Test public void testActiveOrdering() { - assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG"); // X - assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG"); // no change - assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG"); // Z and X - assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG"); // Z and X + gap - verify(mStabilityManager, never()).onEntryReorderSuppressed(); + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true); + assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X + assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change + assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X + assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap } @Test @@ -2062,6 +2132,52 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test + public void stableOrderingDisregardedWithSectionChange() { + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true); + // GIVEN the first sectioner's packages can be changed from run-to-run + List<String> mutableSectionerPackages = new ArrayList<>(); + mutableSectionerPackages.add(PACKAGE_1); + mListBuilder.setSectioners(asList( + new PackageSectioner(mutableSectionerPackages, null), + new PackageSectioner(List.of(PACKAGE_1, PACKAGE_2, PACKAGE_3), null))); + mStabilityManager.setAllowEntryReordering(false); + + // WHEN the list is originally built with reordering disabled (and section changes allowed) + addNotif(0, PACKAGE_1).setRank(4); + addNotif(1, PACKAGE_1).setRank(5); + addNotif(2, PACKAGE_2).setRank(1); + addNotif(3, PACKAGE_2).setRank(2); + addNotif(4, PACKAGE_3).setRank(3); + dispatchBuild(); + + // VERIFY the order and that entry reordering has not been suppressed + verifyBuiltList( + notif(0), + notif(1), + notif(2), + notif(3), + notif(4) + ); + verify(mStabilityManager, never()).onEntryReorderSuppressed(); + + // WHEN the first section now claims PACKAGE_3 notifications + mutableSectionerPackages.add(PACKAGE_3); + dispatchBuild(); + + // VERIFY the re-sectioned notification is inserted at #1 of the first section, which + // is the correct position based on its rank, rather than #3 in the new section simply + // because it was #3 in its previous section. + verifyBuiltList( + notif(4), + notif(0), + notif(1), + notif(2), + notif(3) + ); + verify(mStabilityManager, never()).onEntryReorderSuppressed(); + } + + @Test public void testStableChildOrdering() { // WHEN the list is originally built with reordering disabled mStabilityManager.setAllowEntryReordering(false); @@ -2112,6 +2228,85 @@ public class ShadeListBuilderTest extends SysuiTestCase { ); } + @Test + public void groupRevertingToSummaryDoesNotRetainStablePositionWithLegacyIndexLogic() { + when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(false); + + // GIVEN a notification group is on screen + mStabilityManager.setAllowEntryReordering(false); + + // WHEN the list is originally built with reordering disabled (and section changes allowed) + addNotif(0, PACKAGE_1).setRank(2); + addNotif(1, PACKAGE_1).setRank(3); + addGroupSummary(2, PACKAGE_1, "group").setRank(4); + addGroupChild(3, PACKAGE_1, "group").setRank(5); + addGroupChild(4, PACKAGE_1, "group").setRank(6); + dispatchBuild(); + + verifyBuiltList( + notif(0), + notif(1), + group( + summary(2), + child(3), + child(4) + ) + ); + + // WHEN the notification summary rank increases and children removed + setNewRank(notif(2).entry, 1); + mEntrySet.remove(4); + mEntrySet.remove(3); + dispatchBuild(); + + // VERIFY the summary (incorrectly) moves to the top of the section where it is ranked, + // despite visual stability being active + verifyBuiltList( + notif(2), + notif(0), + notif(1) + ); + } + + @Test + public void groupRevertingToSummaryRetainsStablePosition() { + when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true); + + // GIVEN a notification group is on screen + mStabilityManager.setAllowEntryReordering(false); + + // WHEN the list is originally built with reordering disabled (and section changes allowed) + addNotif(0, PACKAGE_1).setRank(2); + addNotif(1, PACKAGE_1).setRank(3); + addGroupSummary(2, PACKAGE_1, "group").setRank(4); + addGroupChild(3, PACKAGE_1, "group").setRank(5); + addGroupChild(4, PACKAGE_1, "group").setRank(6); + dispatchBuild(); + + verifyBuiltList( + notif(0), + notif(1), + group( + summary(2), + child(3), + child(4) + ) + ); + + // WHEN the notification summary rank increases and children removed + setNewRank(notif(2).entry, 1); + mEntrySet.remove(4); + mEntrySet.remove(3); + dispatchBuild(); + + // VERIFY the summary stays in the same location on rebuild + verifyBuiltList( + notif(0), + notif(1), + notif(2) + ); + } + private static void setNewRank(NotificationEntry entry, int rank) { entry.setRanking(new RankingBuilder(entry.getRanking()).setRank(rank).build()); } @@ -2255,26 +2450,35 @@ public class ShadeListBuilderTest extends SysuiTestCase { return addGroupChildWithTag(index, packageId, groupId, null); } - private void assertOrder(String visible, String active, String expected) { + private void assertOrder(String visible, String active, String expected, + boolean isOrderedCorrectly) { StringBuilder differenceSb = new StringBuilder(); + NotifSection section = new NotifSection(mock(NotifSectioner.class), 0); for (char c : active.toCharArray()) { if (visible.indexOf(c) < 0) differenceSb.append(c); } String difference = differenceSb.toString(); + int globalIndex = 0; for (int i = 0; i < visible.length(); i++) { - addNotif(i, String.valueOf(visible.charAt(i))) - .setRank(active.indexOf(visible.charAt(i))) + final char c = visible.charAt(i); + // Skip notifications which aren't active anymore + if (!active.contains(String.valueOf(c))) continue; + addNotif(globalIndex++, String.valueOf(c)) + .setRank(active.indexOf(c)) + .setSection(section) .setStableIndex(i); - } - for (int i = 0; i < difference.length(); i++) { - addNotif(i + visible.length(), String.valueOf(difference.charAt(i))) - .setRank(active.indexOf(difference.charAt(i))) + for (char c : difference.toCharArray()) { + addNotif(globalIndex++, String.valueOf(c)) + .setRank(active.indexOf(c)) + .setSection(section) .setStableIndex(-1); } + clearInvocations(mStabilityManager); + dispatchBuild(); StringBuilder resultSb = new StringBuilder(); for (int i = 0; i < expected.length(); i++) { @@ -2284,6 +2488,9 @@ public class ShadeListBuilderTest extends SysuiTestCase { assertEquals("visible [" + visible + "] active [" + active + "]", expected, resultSb.toString()); mEntrySet.clear(); + + verify(mStabilityManager, isOrderedCorrectly ? never() : times(1)) + .onEntryReorderSuppressed(); } private int nextId(String packageName) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 340bc96f80c2..3ff7639e9262 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -674,7 +674,9 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test fun testOnRankingApplied_newEntryShouldAlert() { // GIVEN that mEntry has never interrupted in the past, and now should + // and is new enough to do so assertFalse(mEntry.hasInterrupted()) + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis()) setShouldHeadsUp(mEntry) whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) @@ -690,8 +692,9 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test fun testOnRankingApplied_alreadyAlertedEntryShouldNotAlertAgain() { - // GIVEN that mEntry has alerted in the past + // GIVEN that mEntry has alerted in the past, even if it's new mEntry.setInterruption() + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis()) setShouldHeadsUp(mEntry) whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) @@ -725,6 +728,27 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager).showNotification(mEntry) } + @Test + fun testOnRankingApplied_entryUpdatedButTooOld() { + // GIVEN that mEntry is added in a state where it should not HUN + setShouldHeadsUp(mEntry, false) + mCollectionListener.onEntryAdded(mEntry) + + // and it was actually added 10s ago + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis() - 10000) + + // WHEN it is updated to HUN and then a ranking update occurs + setShouldHeadsUp(mEntry) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) + mCollectionListener.onRankingApplied() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is never bound or shown + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + verify(mHeadsUpManager, never()).showNotification(any()) + } + private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) { whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should) whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any())) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java index e1e5051751bb..590c902ba687 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java @@ -35,7 +35,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.SysuiTestCase; -import com.android.systemui.media.MediaFeatureFlag; +import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index dcf245525f10..b6b0b7738997 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java @@ -261,23 +261,15 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mNotifInflater.invokeInflateCallbackForEntry(mEntry); // WHEN notification is moved under a parent - NotificationEntry groupSummary = getNotificationEntryBuilder() - .setParent(ROOT_ENTRY) - .setGroupSummary(mContext, true) - .setGroup(mContext, TEST_GROUP_KEY) - .build(); - GroupEntry parent = mock(GroupEntry.class); - when(parent.getSummary()).thenReturn(groupSummary); - NotificationEntryBuilder.setNewParent(mEntry, parent); - mCollectionListener.onEntryInit(groupSummary); - mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry, groupSummary)); + NotificationEntryBuilder.setNewParent(mEntry, mock(GroupEntry.class)); + mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); // THEN we rebind it as not-minimized verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any()); assertFalse(mParamsCaptor.getValue().isLowPriority()); - // THEN we filter it because the parent summary is not yet inflated. - assertTrue(mUninflatedFilter.shouldFilterOut(mEntry, 0)); + // THEN we do not filter it because it's not the first inflation. + assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0)); } @Test @@ -401,6 +393,36 @@ public class PreparationCoordinatorTest extends SysuiTestCase { } @Test + public void testNullGroupSummary() { + // GIVEN a newly-posted group with a summary and two children + final GroupEntry group = new GroupEntryBuilder() + .setCreationTime(400) + .setSummary(getNotificationEntryBuilder().setId(1).build()) + .addChild(getNotificationEntryBuilder().setId(2).build()) + .addChild(getNotificationEntryBuilder().setId(3).build()) + .build(); + fireAddEvents(List.of(group)); + final NotificationEntry child0 = group.getChildren().get(0); + final NotificationEntry child1 = group.getChildren().get(1); + mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group)); + + // WHEN the summary is pruned + new GroupEntryBuilder() + .setCreationTime(400) + .addChild(child0) + .addChild(child1) + .build(); + + // WHEN all of the children (but not the summary) finish inflating + mNotifInflater.invokeInflateCallbackForEntry(child0); + mNotifInflater.invokeInflateCallbackForEntry(child1); + + // THEN the entire group is not filtered out + assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401)); + assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401)); + } + + @Test public void testPartiallyInflatedGroupsAreNotFilteredOutIfSummaryReinflate() { // GIVEN a newly-posted group with a summary and two children final String groupKey = "test_reinflate_group"; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt new file mode 100644 index 000000000000..1cdd023dd01c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt @@ -0,0 +1,210 @@ +/* + * 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.notification.collection.listbuilder + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.util.Log +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class SemiStableSortTest : SysuiTestCase() { + + var shuffleInput: Boolean = false + var testStabilizeTo: Boolean = false + var sorter: SemiStableSort? = null + + @Before + fun setUp() { + shuffleInput = false + sorter = null + } + + private fun stringStabilizeTo( + stableOrder: String, + activeOrder: String, + ): Pair<String, Boolean> { + val actives = activeOrder.toMutableList() + val result = mutableListOf<Char>() + return (sorter ?: SemiStableSort()) + .stabilizeTo( + actives, + { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } }, + result, + ) + .let { ordered -> result.joinToString("") to ordered } + } + + private fun stringSort( + stableOrder: String, + activeOrder: String, + ): Pair<String, Boolean> { + val actives = activeOrder.toMutableList() + if (shuffleInput) { + actives.shuffle() + } + return (sorter ?: SemiStableSort()) + .sort( + actives, + { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } }, + compareBy { activeOrder.indexOf(it) }, + ) + .let { ordered -> actives.joinToString("") to ordered } + } + + private fun testCase( + stableOrder: String, + activeOrder: String, + expected: String, + expectOrdered: Boolean, + ) { + val (mergeResult, ordered) = + if (testStabilizeTo) stringStabilizeTo(stableOrder, activeOrder) + else stringSort(stableOrder, activeOrder) + val resultPass = expected == mergeResult + val orderedPass = ordered == expectOrdered + val pass = resultPass && orderedPass + val resultSuffix = + if (resultPass) "result=$expected" else "expected=$expected got=$mergeResult" + val orderedSuffix = + if (orderedPass) "ordered=$ordered" else "expected ordered to be $expectOrdered" + val readableResult = "stable=$stableOrder active=$activeOrder $resultSuffix $orderedSuffix" + Log.d("SemiStableSortTest", "${if (pass) "PASS" else "FAIL"}: $readableResult") + if (!pass) { + throw AssertionError("Test case failed: $readableResult") + } + } + + private fun runAllTestCases() { + // No input or output + testCase("", "", "", true) + // Remove everything + testCase("ABCDEFG", "", "", true) + // Literally no changes + testCase("ABCDEFG", "ABCDEFG", "ABCDEFG", true) + + // No stable order + testCase("", "ABCDEFG", "ABCDEFG", true) + + // F moved after A, and... + testCase("ABCDEFG", "AFBCDEG", "ABCDEFG", false) // No other changes + testCase("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false) // Insert X before F + testCase("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false) // Insert X after F + testCase("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false) // Insert X where F was + + // B moved after F, and... + testCase("ABCDEFG", "ACDEFBG", "ABCDEFG", false) // No other changes + testCase("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false) // Insert X before B + testCase("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false) // Insert X after B + testCase("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false) // Insert X where B was + + // Swap F and B, and... + testCase("ABCDEFG", "AFCDEBG", "ABCDEFG", false) // No other changes + testCase("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false) // Insert X before F + testCase("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false) // Insert X after F + testCase("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false) // Insert X between CD (Alt: ABCXDEFG) + testCase("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false) // Insert X between DE (Alt: ABCDEFXG) + testCase("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false) // Insert X before B + testCase("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false) // Insert X after B + + // Remove a bunch of entries at once + testCase("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true) + + // Remove a bunch of entries and scramble + testCase("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false) + + // Add a bunch of entries at once + testCase("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true) + + // Add a bunch of entries and reverse originals + // NOTE: Some of these don't have obviously correct answers + testCase("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false) // appended + testCase("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false) // prepended + testCase("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false) // closer to back: append + testCase("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false) // closer to front: prepend + testCase("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false) // split new entries + + // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout + testCase("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false) + } + + @Test + fun testSort() { + testStabilizeTo = false + shuffleInput = false + sorter = null + runAllTestCases() + } + + @Test + fun testSortWithSingleInstance() { + testStabilizeTo = false + shuffleInput = false + sorter = SemiStableSort() + runAllTestCases() + } + + @Test + fun testSortWithShuffledInput() { + testStabilizeTo = false + shuffleInput = true + sorter = null + runAllTestCases() + } + + @Test + fun testStabilizeTo() { + testStabilizeTo = true + sorter = null + runAllTestCases() + } + + @Test + fun testStabilizeToWithSingleInstance() { + testStabilizeTo = true + sorter = SemiStableSort() + runAllTestCases() + } + + @Test + fun testIsSorted() { + val intCmp = Comparator<Int> { x, y -> Integer.compare(x, y) } + SemiStableSort.apply { + assertTrue(emptyList<Int>().isSorted(intCmp)) + assertTrue(listOf(1).isSorted(intCmp)) + assertTrue(listOf(1, 2).isSorted(intCmp)) + assertTrue(listOf(1, 2, 3).isSorted(intCmp)) + assertTrue(listOf(1, 2, 3, 4).isSorted(intCmp)) + assertTrue(listOf(1, 2, 3, 4, 5).isSorted(intCmp)) + assertTrue(listOf(1, 1, 1, 1, 1).isSorted(intCmp)) + assertTrue(listOf(1, 1, 2, 2, 3, 3).isSorted(intCmp)) + assertFalse(listOf(2, 1).isSorted(intCmp)) + assertFalse(listOf(2, 1, 2).isSorted(intCmp)) + assertFalse(listOf(1, 2, 1).isSorted(intCmp)) + assertFalse(listOf(1, 2, 3, 2, 5).isSorted(intCmp)) + assertFalse(listOf(5, 2, 3, 4, 5).isSorted(intCmp)) + assertFalse(listOf(1, 2, 3, 4, 1).isSorted(intCmp)) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt new file mode 100644 index 000000000000..20369546d68a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt @@ -0,0 +1,76 @@ +/* + * 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.notification.collection.listbuilder + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper.getContiguousSubLists +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class ShadeListBuilderHelperTest : SysuiTestCase() { + + @Test + fun testGetContiguousSubLists() { + assertThat(getContiguousSubLists("AAAAAA".toList()) { it }) + .containsExactly( + listOf('A', 'A', 'A', 'A', 'A', 'A'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABBB".toList()) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('B', 'B', 'B'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABAA".toList()) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('B'), + listOf('A', 'A'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABAA".toList(), minLength = 2) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('A', 'A'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList()) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('B', 'B', 'B', 'B'), + listOf('C', 'C'), + listOf('D'), + listOf('E', 'E', 'E'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList(), minLength = 2) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('B', 'B', 'B', 'B'), + listOf('C', 'C'), + listOf('E', 'E', 'E'), + ) + .inOrder() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java index 46f630b7db63..ea311da3e20b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java @@ -51,12 +51,14 @@ import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; +import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -97,6 +99,7 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { NotifPipelineFlags mFlags; @Mock KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider; + UiEventLoggerFake mUiEventLoggerFake; @Mock PendingIntent mPendingIntent; @@ -107,6 +110,8 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(false); + mUiEventLoggerFake = new UiEventLoggerFake(); + mNotifInterruptionStateProvider = new NotificationInterruptStateProviderImpl( mContext.getContentResolver(), @@ -120,7 +125,8 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { mLogger, mMockHandler, mFlags, - mKeyguardNotificationVisibilityProvider); + mKeyguardNotificationVisibilityProvider, + mUiEventLoggerFake); mNotifInterruptionStateProvider.mUseHeadsUp = true; } @@ -442,6 +448,13 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { verify(mLogger, never()).logNoFullscreen(any(), any()); verify(mLogger).logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN"); verify(mLogger, never()).logFullscreen(any(), any()); + + assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1); + UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0); + assertThat(fakeUiEvent.eventId).isEqualTo( + NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR.getId()); + assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid()); + assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName()); } @Test @@ -600,6 +613,13 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { verify(mLogger, never()).logNoFullscreen(any(), any()); verify(mLogger).logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard"); verify(mLogger, never()).logFullscreen(any(), any()); + + assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1); + UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0); + assertThat(fakeUiEvent.eventId).isEqualTo( + NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD.getId()); + assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid()); + assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName()); } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt index 16e2441c556b..f69839b7087c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt @@ -28,30 +28,21 @@ import android.widget.RemoteViews import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.NotificationUtils -import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) -class NotificationMemoryMonitorTest : SysuiTestCase() { - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - } +class NotificationMemoryMeterTest : SysuiTestCase() { @Test fun currentNotificationMemoryUse_plainNotification() { val notification = createBasicNotification().build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -69,8 +60,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() { val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)) val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -92,8 +83,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { RemoteViews(context.packageName, android.R.layout.list_content) ) .build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -112,8 +103,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444) val notification = createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = 444444, @@ -141,8 +132,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { .bigLargeIcon(bigPictureIcon) ) .build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -167,8 +158,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { createBasicNotification() .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent)) .build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -203,8 +194,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { .addHistoricMessage(historicMessage) ) .build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -225,8 +216,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888) val extender = Notification.CarExtender().setLargeIcon(carIcon) val notification = createBasicNotification().extend(extender).build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -246,8 +237,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888) val wearExtender = Notification.WearableExtender().setBackground(wearBackground) val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -283,10 +274,10 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { extender: Int, style: String?, styleIcon: Int, - hasCustomView: Boolean + hasCustomView: Boolean, ) { assertThat(memoryUse.packageName).isEqualTo("test_pkg") - assertThat(memoryUse.notificationId) + assertThat(memoryUse.notificationKey) .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0")) assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon) assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon) @@ -301,21 +292,14 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { } private fun getUseObject( - singleItemUseList: List<NotificationMemoryUsage> + singleItemUseList: List<NotificationMemoryUsage>, ): NotificationMemoryUsage { assertThat(singleItemUseList).hasSize(1) return singleItemUseList[0] } - private fun createNMMWithNotifications( - notifications: List<Notification> - ): NotificationMemoryMonitor { - val notifPipeline: NotifPipeline = mock() - val notificationEntries = - notifications.map { n -> - NotificationEntryBuilder().setTag("test").setNotification(n).build() - } - whenever(notifPipeline.allNotifs).thenReturn(notificationEntries) - return NotificationMemoryMonitor(notifPipeline, mock()) - } + private fun createNotificationEntry( + notification: Notification, + ): NotificationEntry = + NotificationEntryBuilder().setTag("test").setNotification(notification).build() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt new file mode 100644 index 000000000000..3a16fb33388b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt @@ -0,0 +1,148 @@ +package com.android.systemui.statusbar.notification.logging + +import android.app.Notification +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.widget.RemoteViews +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.android.systemui.tests.R +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class NotificationMemoryViewWalkerTest : SysuiTestCase() { + + private lateinit var testHelper: NotificationTestHelper + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + testHelper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + } + + @Test + fun testViewWalker_nullRow_returnsEmptyView() { + val result = NotificationMemoryViewWalker.getViewUsage(null) + assertThat(result).isNotNull() + assertThat(result).isEmpty() + } + + @Test + fun testViewWalker_plainNotification() { + val row = testHelper.createRow() + val result = NotificationMemoryViewWalker.getViewUsage(row) + assertThat(result).hasSize(5) + assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains(NotificationViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains(NotificationViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0)) + } + + @Test + fun testViewWalker_bigPictureNotification() { + val bigPicture = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888) + val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888)) + val largeIcon = Icon.createWithBitmap(Bitmap.createBitmap(60, 60, Bitmap.Config.ARGB_8888)) + val row = + testHelper.createRow( + Notification.Builder(mContext) + .setContentText("Test") + .setContentTitle("title") + .setSmallIcon(icon) + .setLargeIcon(largeIcon) + .setStyle(Notification.BigPictureStyle().bigPicture(bigPicture)) + .build() + ) + val result = NotificationMemoryViewWalker.getViewUsage(row) + assertThat(result).hasSize(5) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_EXPANDED_VIEW, + icon.bitmap.allocationByteCount, + largeIcon.bitmap.allocationByteCount, + 0, + bigPicture.allocationByteCount, + 0, + bigPicture.allocationByteCount + + icon.bitmap.allocationByteCount + + largeIcon.bitmap.allocationByteCount + ) + ) + + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_CONTRACTED_VIEW, + icon.bitmap.allocationByteCount, + largeIcon.bitmap.allocationByteCount, + 0, + 0, + 0, + icon.bitmap.allocationByteCount + largeIcon.bitmap.allocationByteCount + ) + ) + // Due to deduplication, this should all be 0. + assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + } + + @Test + fun testViewWalker_customView() { + val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888)) + val bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888) + + val views = RemoteViews(mContext.packageName, R.layout.custom_view_dark) + views.setImageViewBitmap(R.id.custom_view_dark_image, bitmap) + val row = + testHelper.createRow( + Notification.Builder(mContext) + .setContentText("Test") + .setContentTitle("title") + .setSmallIcon(icon) + .setCustomContentView(views) + .setCustomBigContentView(views) + .build() + ) + val result = NotificationMemoryViewWalker.getViewUsage(row) + assertThat(result).hasSize(5) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_CONTRACTED_VIEW, + icon.bitmap.allocationByteCount, + 0, + 0, + 0, + bitmap.allocationByteCount, + bitmap.allocationByteCount + icon.bitmap.allocationByteCount + ) + ) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_EXPANDED_VIEW, + icon.bitmap.allocationByteCount, + 0, + 0, + 0, + bitmap.allocationByteCount, + bitmap.allocationByteCount + icon.bitmap.allocationByteCount + ) + ) + // Due to deduplication, this should all be 0. + assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index 8375e7cceb28..5394d88ad103 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -51,7 +51,7 @@ import androidx.test.filters.SmallTest; import androidx.test.filters.Suppress; import com.android.systemui.SysuiTestCase; -import com.android.systemui.media.MediaFeatureFlag; +import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; import com.android.systemui.statusbar.notification.collection.NotificationEntry; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java deleted file mode 100644 index 81b8e98029ce..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * 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.statusbar.notification.row; - -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -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 android.view.NotificationHeaderView; -import android.view.View; -import android.view.ViewPropertyAnimator; - -import androidx.test.annotation.UiThreadTest; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.internal.R; -import com.android.internal.widget.NotificationActionListLayout; -import com.android.internal.widget.NotificationExpandButton; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.media.dialog.MediaOutputDialogFactory; -import com.android.systemui.statusbar.notification.FeedbackIcon; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class NotificationContentViewTest extends SysuiTestCase { - - NotificationContentView mView; - - @Before - @UiThreadTest - public void setup() { - mDependency.injectMockDependency(MediaOutputDialogFactory.class); - - mView = new NotificationContentView(mContext, null); - ExpandableNotificationRow row = new ExpandableNotificationRow(mContext, null); - ExpandableNotificationRow mockRow = spy(row); - doReturn(10).when(mockRow).getIntrinsicHeight(); - - mView.setContainingNotification(mockRow); - mView.setHeights(10, 20, 30); - - mView.setContractedChild(createViewWithHeight(10)); - mView.setExpandedChild(createViewWithHeight(20)); - mView.setHeadsUpChild(createViewWithHeight(30)); - - mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight()); - } - - private View createViewWithHeight(int height) { - View view = new View(mContext, null); - view.setMinimumHeight(height); - return view; - } - - @Test - @UiThreadTest - public void testSetFeedbackIcon() { - View mockContracted = mock(NotificationHeaderView.class); - when(mockContracted.findViewById(com.android.internal.R.id.feedback)) - .thenReturn(mockContracted); - when(mockContracted.getContext()).thenReturn(mContext); - View mockExpanded = mock(NotificationHeaderView.class); - when(mockExpanded.findViewById(com.android.internal.R.id.feedback)) - .thenReturn(mockExpanded); - when(mockExpanded.getContext()).thenReturn(mContext); - View mockHeadsUp = mock(NotificationHeaderView.class); - when(mockHeadsUp.findViewById(com.android.internal.R.id.feedback)) - .thenReturn(mockHeadsUp); - when(mockHeadsUp.getContext()).thenReturn(mContext); - - mView.setContractedChild(mockContracted); - mView.setExpandedChild(mockExpanded); - mView.setHeadsUpChild(mockHeadsUp); - - mView.setFeedbackIcon(new FeedbackIcon(R.drawable.ic_feedback_alerted, - R.string.notification_feedback_indicator_alerted)); - - verify(mockContracted, times(1)).setVisibility(View.VISIBLE); - verify(mockExpanded, times(1)).setVisibility(View.VISIBLE); - verify(mockHeadsUp, times(1)).setVisibility(View.VISIBLE); - } - - @Test - @UiThreadTest - public void testExpandButtonFocusIsCalled() { - View mockContractedEB = mock(NotificationExpandButton.class); - View mockContracted = mock(NotificationHeaderView.class); - when(mockContracted.animate()).thenReturn(mock(ViewPropertyAnimator.class)); - when(mockContracted.findViewById(com.android.internal.R.id.expand_button)).thenReturn( - mockContractedEB); - when(mockContracted.getContext()).thenReturn(mContext); - - View mockExpandedEB = mock(NotificationExpandButton.class); - View mockExpanded = mock(NotificationHeaderView.class); - when(mockExpanded.animate()).thenReturn(mock(ViewPropertyAnimator.class)); - when(mockExpanded.findViewById(com.android.internal.R.id.expand_button)).thenReturn( - mockExpandedEB); - when(mockExpanded.getContext()).thenReturn(mContext); - - View mockHeadsUpEB = mock(NotificationExpandButton.class); - View mockHeadsUp = mock(NotificationHeaderView.class); - when(mockHeadsUp.animate()).thenReturn(mock(ViewPropertyAnimator.class)); - when(mockHeadsUp.findViewById(com.android.internal.R.id.expand_button)).thenReturn( - mockHeadsUpEB); - when(mockHeadsUp.getContext()).thenReturn(mContext); - - // Set up all 3 child forms - mView.setContractedChild(mockContracted); - mView.setExpandedChild(mockExpanded); - mView.setHeadsUpChild(mockHeadsUp); - - // This is required to call requestAccessibilityFocus() - mView.setFocusOnVisibilityChange(); - - // The following will initialize the view and switch from not visible to expanded. - // (heads-up is actually an alternate form of contracted, hence this enters expanded state) - mView.setHeadsUp(true); - - verify(mockContractedEB, times(0)).requestAccessibilityFocus(); - verify(mockExpandedEB, times(1)).requestAccessibilityFocus(); - verify(mockHeadsUpEB, times(0)).requestAccessibilityFocus(); - } - - @Test - @UiThreadTest - public void testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() { - View mockContracted = mock(NotificationHeaderView.class); - - View mockExpandedActions = mock(NotificationActionListLayout.class); - View mockExpanded = mock(NotificationHeaderView.class); - when(mockExpanded.findViewById(com.android.internal.R.id.actions)).thenReturn( - mockExpandedActions); - - View mockHeadsUpActions = mock(NotificationActionListLayout.class); - View mockHeadsUp = mock(NotificationHeaderView.class); - when(mockHeadsUp.findViewById(com.android.internal.R.id.actions)).thenReturn( - mockHeadsUpActions); - - mView.setContractedChild(mockContracted); - mView.setExpandedChild(mockExpanded); - mView.setHeadsUpChild(mockHeadsUp); - - mView.setRemoteInputVisible(true); - - verify(mockContracted, times(0)).findViewById(0); - verify(mockExpandedActions, times(1)).setImportantForAccessibility( - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - verify(mockHeadsUpActions, times(1)).setImportantForAccessibility( - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - } - - @Test - @UiThreadTest - public void testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() { - View mockContracted = mock(NotificationHeaderView.class); - - View mockExpandedActions = mock(NotificationActionListLayout.class); - View mockExpanded = mock(NotificationHeaderView.class); - when(mockExpanded.findViewById(com.android.internal.R.id.actions)).thenReturn( - mockExpandedActions); - - View mockHeadsUpActions = mock(NotificationActionListLayout.class); - View mockHeadsUp = mock(NotificationHeaderView.class); - when(mockHeadsUp.findViewById(com.android.internal.R.id.actions)).thenReturn( - mockHeadsUpActions); - - mView.setContractedChild(mockContracted); - mView.setExpandedChild(mockExpanded); - mView.setHeadsUpChild(mockHeadsUp); - - mView.setRemoteInputVisible(false); - - verify(mockContracted, times(0)).findViewById(0); - verify(mockExpandedActions, times(1)).setImportantForAccessibility( - View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); - verify(mockHeadsUpActions, times(1)).setImportantForAccessibility( - View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt new file mode 100644 index 000000000000..562b4dfb35ef --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt @@ -0,0 +1,350 @@ +/* + * 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.notification.row + +import android.content.res.Resources +import android.os.UserHandle +import android.service.notification.StatusBarNotification +import android.testing.AndroidTestingRunner +import android.view.NotificationHeaderView +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.internal.widget.NotificationActionListLayout +import com.android.internal.widget.NotificationExpandButton +import com.android.systemui.SysuiTestCase +import com.android.systemui.media.dialog.MediaOutputDialogFactory +import com.android.systemui.statusbar.notification.FeedbackIcon +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations.initMocks + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class NotificationContentViewTest : SysuiTestCase() { + private lateinit var view: NotificationContentView + + @Mock private lateinit var mPeopleNotificationIdentifier: PeopleNotificationIdentifier + + private val notificationContentMargin = + mContext.resources.getDimensionPixelSize(R.dimen.notification_content_margin) + + @Before + fun setup() { + initMocks(this) + + mDependency.injectMockDependency(MediaOutputDialogFactory::class.java) + + view = spy(NotificationContentView(mContext, /* attrs= */ null)) + val row = ExpandableNotificationRow(mContext, /* attrs= */ null) + row.entry = createMockNotificationEntry(false) + val spyRow = spy(row) + doReturn(10).whenever(spyRow).intrinsicHeight + + with(view) { + initialize(mPeopleNotificationIdentifier, mock(), mock(), mock()) + setContainingNotification(spyRow) + setHeights(/* smallHeight= */ 10, /* headsUpMaxHeight= */ 20, /* maxHeight= */ 30) + contractedChild = createViewWithHeight(10) + expandedChild = createViewWithHeight(20) + headsUpChild = createViewWithHeight(30) + measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + layout(0, 0, view.measuredWidth, view.measuredHeight) + } + } + + private fun createViewWithHeight(height: Int) = + View(mContext, /* attrs= */ null).apply { minimumHeight = height } + + @Test + fun testSetFeedbackIcon() { + // Given: contractedChild, enpandedChild, and headsUpChild being set + val mockContracted = createMockNotificationHeaderView() + val mockExpanded = createMockNotificationHeaderView() + val mockHeadsUp = createMockNotificationHeaderView() + + with(view) { + contractedChild = mockContracted + expandedChild = mockExpanded + headsUpChild = mockHeadsUp + } + + // When: FeedBackIcon is set + view.setFeedbackIcon( + FeedbackIcon( + R.drawable.ic_feedback_alerted, + R.string.notification_feedback_indicator_alerted + ) + ) + + // Then: contractedChild, enpandedChild, and headsUpChild should be set to be visible + verify(mockContracted).visibility = View.VISIBLE + verify(mockExpanded).visibility = View.VISIBLE + verify(mockHeadsUp).visibility = View.VISIBLE + } + + private fun createMockNotificationHeaderView() = + mock<NotificationHeaderView>().apply { + whenever(this.findViewById<View>(R.id.feedback)).thenReturn(this) + whenever(this.context).thenReturn(mContext) + } + + @Test + fun testExpandButtonFocusIsCalled() { + val mockContractedEB = mock<NotificationExpandButton>() + val mockContracted = createMockNotificationHeaderView(mockContractedEB) + + val mockExpandedEB = mock<NotificationExpandButton>() + val mockExpanded = createMockNotificationHeaderView(mockExpandedEB) + + val mockHeadsUpEB = mock<NotificationExpandButton>() + val mockHeadsUp = createMockNotificationHeaderView(mockHeadsUpEB) + + // Set up all 3 child forms + view.contractedChild = mockContracted + view.expandedChild = mockExpanded + view.headsUpChild = mockHeadsUp + + // This is required to call requestAccessibilityFocus() + view.setFocusOnVisibilityChange() + + // The following will initialize the view and switch from not visible to expanded. + // (heads-up is actually an alternate form of contracted, hence this enters expanded state) + view.setHeadsUp(true) + verify(mockContractedEB, never()).requestAccessibilityFocus() + verify(mockExpandedEB).requestAccessibilityFocus() + verify(mockHeadsUpEB, never()).requestAccessibilityFocus() + } + + private fun createMockNotificationHeaderView(mockExpandedEB: NotificationExpandButton) = + mock<NotificationHeaderView>().apply { + whenever(this.animate()).thenReturn(mock()) + whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB) + whenever(this.context).thenReturn(mContext) + } + + @Test + fun testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() { + val mockContracted = mock<NotificationHeaderView>() + + val mockExpandedActions = mock<NotificationActionListLayout>() + val mockExpanded = mock<NotificationHeaderView>() + whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions) + + val mockHeadsUpActions = mock<NotificationActionListLayout>() + val mockHeadsUp = mock<NotificationHeaderView>() + whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions) + + with(view) { + contractedChild = mockContracted + expandedChild = mockExpanded + headsUpChild = mockHeadsUp + } + + view.setRemoteInputVisible(true) + + verify(mockContracted, never()).findViewById<View>(0) + verify(mockExpandedActions).importantForAccessibility = + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + verify(mockHeadsUpActions).importantForAccessibility = + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } + + @Test + fun testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() { + val mockContracted = mock<NotificationHeaderView>() + + val mockExpandedActions = mock<NotificationActionListLayout>() + val mockExpanded = mock<NotificationHeaderView>() + whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions) + + val mockHeadsUpActions = mock<NotificationActionListLayout>() + val mockHeadsUp = mock<NotificationHeaderView>() + whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions) + + with(view) { + contractedChild = mockContracted + expandedChild = mockExpanded + headsUpChild = mockHeadsUp + } + + view.setRemoteInputVisible(false) + + verify(mockContracted, never()).findViewById<View>(0) + verify(mockExpandedActions).importantForAccessibility = + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + verify(mockHeadsUpActions).importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + + @Test + fun setExpandedChild_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() { + // Given: bottom margin of actionListMarginTarget is notificationContentMargin + // Bubble button should not be shown for the given NotificationEntry + val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false) + val mockContainingNotification = createMockContainingNotification(mockNotificationEntry) + val actionListMarginTarget = + spy(createLinearLayoutWithBottomMargin(notificationContentMargin)) + val mockExpandedChild = createMockExpandedChild(mockNotificationEntry) + whenever( + mockExpandedChild.findViewById<LinearLayout>( + R.id.notification_action_list_margin_target + ) + ) + .thenReturn(actionListMarginTarget) + view.setContainingNotification(mockContainingNotification) + + // When: call NotificationContentView.setExpandedChild() to set the expandedChild + view.expandedChild = mockExpandedChild + + // Then: bottom margin of actionListMarginTarget should not change, + // still be notificationContentMargin + assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) + } + + @Test + fun setExpandedChild_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() { + // Given: bottom margin of actionListMarginTarget is notificationContentMargin + // Bubble button should be shown for the given NotificationEntry + val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ true) + val mockContainingNotification = createMockContainingNotification(mockNotificationEntry) + val actionListMarginTarget = + spy(createLinearLayoutWithBottomMargin(notificationContentMargin)) + val mockExpandedChild = createMockExpandedChild(mockNotificationEntry) + whenever( + mockExpandedChild.findViewById<LinearLayout>( + R.id.notification_action_list_margin_target + ) + ) + .thenReturn(actionListMarginTarget) + view.setContainingNotification(mockContainingNotification) + + // When: call NotificationContentView.setExpandedChild() to set the expandedChild + view.expandedChild = mockExpandedChild + + // Then: bottom margin of actionListMarginTarget should be set to 0 + assertEquals(0, getMarginBottom(actionListMarginTarget)) + } + + @Test + fun onNotificationUpdated_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() { + // Given: bottom margin of actionListMarginTarget is notificationContentMargin + val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false) + val mockContainingNotification = createMockContainingNotification(mockNotificationEntry) + val actionListMarginTarget = + spy(createLinearLayoutWithBottomMargin(notificationContentMargin)) + val mockExpandedChild = createMockExpandedChild(mockNotificationEntry) + whenever( + mockExpandedChild.findViewById<LinearLayout>( + R.id.notification_action_list_margin_target + ) + ) + .thenReturn(actionListMarginTarget) + view.setContainingNotification(mockContainingNotification) + view.expandedChild = mockExpandedChild + assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) + + // When: call NotificationContentView.onNotificationUpdated() to update the + // NotificationEntry, which should not show bubble button + view.onNotificationUpdated(createMockNotificationEntry(/* showButton= */ false)) + + // Then: bottom margin of actionListMarginTarget should not change, still be 20 + assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) + } + + @Test + fun onNotificationUpdated_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() { + // Given: bottom margin of actionListMarginTarget is notificationContentMargin + val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false) + val mockContainingNotification = createMockContainingNotification(mockNotificationEntry) + val actionListMarginTarget = + spy(createLinearLayoutWithBottomMargin(notificationContentMargin)) + val mockExpandedChild = createMockExpandedChild(mockNotificationEntry) + whenever( + mockExpandedChild.findViewById<LinearLayout>( + R.id.notification_action_list_margin_target + ) + ) + .thenReturn(actionListMarginTarget) + view.setContainingNotification(mockContainingNotification) + view.expandedChild = mockExpandedChild + assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) + + // When: call NotificationContentView.onNotificationUpdated() to update the + // NotificationEntry, which should show bubble button + view.onNotificationUpdated(createMockNotificationEntry(true)) + + // Then: bottom margin of actionListMarginTarget should not change, still be 20 + assertEquals(0, getMarginBottom(actionListMarginTarget)) + } + + private fun createMockContainingNotification(notificationEntry: NotificationEntry) = + mock<ExpandableNotificationRow>().apply { + whenever(this.entry).thenReturn(notificationEntry) + whenever(this.context).thenReturn(mContext) + whenever(this.bubbleClickListener).thenReturn(View.OnClickListener {}) + } + + private fun createMockNotificationEntry(showButton: Boolean) = + mock<NotificationEntry>().apply { + whenever(mPeopleNotificationIdentifier.getPeopleNotificationType(this)) + .thenReturn(PeopleNotificationIdentifier.TYPE_FULL_PERSON) + whenever(this.bubbleMetadata).thenReturn(mock()) + val sbnMock: StatusBarNotification = mock() + val userMock: UserHandle = mock() + whenever(this.sbn).thenReturn(sbnMock) + whenever(sbnMock.user).thenReturn(userMock) + doReturn(showButton).whenever(view).shouldShowBubbleButton(this) + } + + private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout { + val outerLayout = LinearLayout(mContext) + val innerLayout = LinearLayout(mContext) + outerLayout.addView(innerLayout) + val mlp = innerLayout.layoutParams as ViewGroup.MarginLayoutParams + mlp.setMargins(0, 0, 0, bottomMargin) + return innerLayout + } + + private fun createMockExpandedChild(notificationEntry: NotificationEntry) = + mock<ExpandableNotificationRow>().apply { + whenever(this.findViewById<ImageView>(R.id.bubble_button)).thenReturn(mock()) + whenever(this.findViewById<View>(R.id.actions_container)).thenReturn(mock()) + whenever(this.entry).thenReturn(notificationEntry) + whenever(this.context).thenReturn(mContext) + + val resourcesMock: Resources = mock() + whenever(resourcesMock.configuration).thenReturn(mock()) + whenever(this.resources).thenReturn(resourcesMock) + } + + private fun getMarginBottom(layout: LinearLayout): Int = + (layout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 9abdeb900c67..421f918a135e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -52,7 +52,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.TestableDependency; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; -import com.android.systemui.media.MediaFeatureFlag; +import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.NotificationMediaManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt index 11798a7a4f96..87f4c323b7cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt @@ -361,6 +361,22 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.isOnKeyguard).isFalse() } // endregion + + // region mIsClosing + @Test + fun isClosing_whenShadeClosing_shouldReturnTrue() { + sut.setIsClosing(true) + + assertThat(sut.isClosing).isTrue() + } + + @Test + fun isClosing_whenShadeFinishClosing_shouldReturnFalse() { + sut.setIsClosing(false) + + assertThat(sut.isClosing).isFalse() + } + // endregion } // region Arrange helper methods. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java index a95a49c31adf..8c8b64424814 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java @@ -147,8 +147,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mFirst, mSecond), createSection(null, null) }); - Assert.assertEquals(1.0f, mSecond.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mSecond.getBottomRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mSecond.getTopRoundness(), 0.0f); } @Test @@ -170,13 +170,13 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { when(testHelper.getStatusBarStateController().isDozing()).thenReturn(true); row.setHeadsUp(true); mRoundnessManager.updateView(entry.getRow(), false); - Assert.assertEquals(1f, row.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1f, row.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1f, row.getBottomRoundness(), 0.0f); + Assert.assertEquals(1f, row.getTopRoundness(), 0.0f); row.setHeadsUp(false); mRoundnessManager.updateView(entry.getRow(), false); - Assert.assertEquals(mSmallRadiusRatio, row.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(mSmallRadiusRatio, row.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, row.getBottomRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, row.getTopRoundness(), 0.0f); } @Test @@ -185,8 +185,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mFirst, mFirst), createSection(null, mSecond) }); - Assert.assertEquals(1.0f, mSecond.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mSecond.getBottomRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mSecond.getTopRoundness(), 0.0f); } @Test @@ -195,8 +195,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mFirst, mFirst), createSection(mSecond, null) }); - Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mSecond.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mSecond.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mSecond.getTopRoundness(), 0.0f); } @Test @@ -205,8 +205,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mFirst, null), createSection(null, null) }); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f); } @Test @@ -215,8 +215,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f); } @Test @@ -226,8 +226,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f); } @Test @@ -238,8 +238,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f); } @Test @@ -250,8 +250,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f); } @Test @@ -262,8 +262,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f); } @Test @@ -274,8 +274,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f); } @Test @@ -286,8 +286,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(0.5f, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(0.5f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(0.5f, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(0.5f, mFirst.getTopRoundness(), 0.0f); } @Test @@ -298,8 +298,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(null, null) }); mFirst.setHeadsUpAnimatingAway(true); - Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f); } @@ -312,8 +312,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { }); mFirst.setHeadsUpAnimatingAway(true); mFirst.setHeadsUpAnimatingAway(false); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java index 9d848e87b0a0..ecc02246ea1d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java @@ -30,7 +30,7 @@ import android.view.ViewGroup; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.media.KeyguardMediaController; +import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 1c9b0be0185a..90061b078afe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -46,7 +46,7 @@ import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.media.KeyguardMediaController; +import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -129,6 +129,7 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator; @Mock private ShadeTransitionController mShadeTransitionController; @Mock private FeatureFlags mFeatureFlags; + @Mock private NotificationTargetsHelper mNotificationTargetsHelper; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor; @@ -177,7 +178,8 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mStackLogger, mLogger, mNotificationStackSizeCalculator, - mFeatureFlags + mFeatureFlags, + mNotificationTargetsHelper ); when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 43530365360b..91aecd8cf753 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -163,7 +163,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mStackScroller.setCentralSurfaces(mCentralSurfaces); mStackScroller.setEmptyShadeView(mEmptyShadeView); when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true); - when(mStackScrollLayoutController.getNoticationRoundessManager()) + when(mStackScrollLayoutController.getNotificationRoundnessManager()) .thenReturn(mNotificationRoundnessManager); mStackScroller.setController(mStackScrollLayoutController); @@ -728,6 +728,57 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { verify(mNotificationStackSizeCalculator).computeHeight(any(), anyInt(), anyFloat()); } + @Test + public void testSetOwnScrollY_shadeNotClosing_scrollYChanges() { + // Given: shade is not closing, scrollY is 0 + mAmbientState.setScrollY(0); + assertEquals(0, mAmbientState.getScrollY()); + mAmbientState.setIsClosing(false); + + // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1 + mStackScroller.setOwnScrollY(1); + + // Then: scrollY should be set to 1 + assertEquals(1, mAmbientState.getScrollY()); + + // Reset scrollY back to 0 to avoid interfering with other tests + mStackScroller.setOwnScrollY(0); + assertEquals(0, mAmbientState.getScrollY()); + } + + @Test + public void testSetOwnScrollY_shadeClosing_scrollYDoesNotChange() { + // Given: shade is closing, scrollY is 0 + mAmbientState.setScrollY(0); + assertEquals(0, mAmbientState.getScrollY()); + mAmbientState.setIsClosing(true); + + // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1 + mStackScroller.setOwnScrollY(1); + + // Then: scrollY should not change, it should still be 0 + assertEquals(0, mAmbientState.getScrollY()); + + // Reset scrollY and mAmbientState.mIsClosing to avoid interfering with other tests + mAmbientState.setIsClosing(false); + mStackScroller.setOwnScrollY(0); + assertEquals(0, mAmbientState.getScrollY()); + } + + @Test + public void onShadeFlingClosingEnd_scrollYShouldBeSetToZero() { + // Given: mAmbientState.mIsClosing is set to be true + // mIsExpanded is set to be false + mAmbientState.setIsClosing(true); + mStackScroller.setIsExpanded(false); + + // When: onExpansionStopped is called + mStackScroller.onExpansionStopped(); + + // Then: mAmbientState.scrollY should be set to be 0 + assertEquals(mAmbientState.getScrollY(), 0); + } + private void setBarStateForTest(int state) { // Can't inject this through the listener or we end up on the actual implementation // rather than the mock because the spy just coppied the anonymous inner /shruggie. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt new file mode 100644 index 000000000000..a2e92305bf27 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt @@ -0,0 +1,107 @@ +package com.android.systemui.statusbar.notification.stack + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.android.systemui.util.mockito.mock +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for {@link NotificationTargetsHelper}. */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class NotificationTargetsHelperTest : SysuiTestCase() { + lateinit var notificationTestHelper: NotificationTestHelper + private val sectionsManager: NotificationSectionsManager = mock() + private val stackScrollLayout: NotificationStackScrollLayout = mock() + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + notificationTestHelper = + NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + } + + private fun notificationTargetsHelper( + notificationGroupCorner: Boolean = true, + ) = + NotificationTargetsHelper( + FakeFeatureFlags().apply { + set(Flags.NOTIFICATION_GROUP_CORNER, notificationGroupCorner) + } + ) + + @Test + fun targetsForFirstNotificationInGroup() { + val children = notificationTestHelper.createGroup(3).childrenContainer + val swiped = children.attachedChildren[0] + + val actual = + notificationTargetsHelper() + .findRoundableTargets( + viewSwiped = swiped, + stackScrollLayout = stackScrollLayout, + sectionsManager = sectionsManager, + ) + + val expected = + RoundableTargets( + before = children.notificationHeaderWrapper, // group header + swiped = swiped, + after = children.attachedChildren[1], + ) + assertEquals(expected, actual) + } + + @Test + fun targetsForMiddleNotificationInGroup() { + val children = notificationTestHelper.createGroup(3).childrenContainer + val swiped = children.attachedChildren[1] + + val actual = + notificationTargetsHelper() + .findRoundableTargets( + viewSwiped = swiped, + stackScrollLayout = stackScrollLayout, + sectionsManager = sectionsManager, + ) + + val expected = + RoundableTargets( + before = children.attachedChildren[0], + swiped = swiped, + after = children.attachedChildren[2], + ) + assertEquals(expected, actual) + } + + @Test + fun targetsForLastNotificationInGroup() { + val children = notificationTestHelper.createGroup(3).childrenContainer + val swiped = children.attachedChildren[2] + + val actual = + notificationTargetsHelper() + .findRoundableTargets( + viewSwiped = swiped, + stackScrollLayout = stackScrollLayout, + sectionsManager = sectionsManager, + ) + + val expected = + RoundableTargets( + before = children.attachedChildren[1], + swiped = swiped, + after = null, + ) + assertEquals(expected, actual) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index ad497a2ec1e1..57557821cca6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -80,6 +80,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; import com.android.internal.jank.InteractionJankMonitor; +import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.logging.testing.FakeMetricsLogger; import com.android.internal.statusbar.IStatusBarService; @@ -98,7 +99,8 @@ import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; @@ -233,6 +235,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private NavigationBarController mNavigationBarController; @Mock private AccessibilityFloatingMenuController mAccessibilityFloatingMenuController; @Mock private SysuiColorExtractor mColorExtractor; + private WakefulnessLifecycle mWakefulnessLifecycle; @Mock private ColorExtractor.GradientColors mGradientColors; @Mock private PulseExpansionHandler mPulseExpansionHandler; @Mock private NotificationWakeUpCoordinator mNotificationWakeUpCoordinator; @@ -271,7 +274,6 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private OngoingCallController mOngoingCallController; @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; @Mock private LockscreenShadeTransitionController mLockscreenTransitionController; - @Mock private FeatureFlags mFeatureFlags; @Mock private NotificationVisibilityProvider mVisibilityProvider; @Mock private WallpaperManager mWallpaperManager; @Mock private IWallpaperManager mIWallpaperManager; @@ -296,9 +298,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); - private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); - private FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock); - private InitController mInitController = new InitController(); + private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); + private final FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock); + private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); + private final InitController mInitController = new InitController(); private final DumpManager mDumpManager = new DumpManager(); @Before @@ -322,7 +325,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mock(NotificationInterruptLogger.class), new Handler(TestableLooper.get(this).getLooper()), mock(NotifPipelineFlags.class), - mock(KeyguardNotificationVisibilityProvider.class)); + mock(KeyguardNotificationVisibilityProvider.class), + mock(UiEventLogger.class)); mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class)); mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class)); @@ -363,10 +367,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { return null; }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any()); - WakefulnessLifecycle wakefulnessLifecycle = + mWakefulnessLifecycle = new WakefulnessLifecycle(mContext, mIWallpaperManager, mDumpManager); - wakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN); - wakefulnessLifecycle.dispatchFinishedWakingUp(); + mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN); + mWakefulnessLifecycle.dispatchFinishedWakingUp(); when(mGradientColors.supportsDarkText()).thenReturn(true); when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors); @@ -425,7 +429,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mBatteryController, mColorExtractor, new ScreenLifecycle(mDumpManager), - wakefulnessLifecycle, + mWakefulnessLifecycle, mStatusBarStateController, Optional.of(mBubbles), mDeviceProvisionedController, @@ -504,6 +508,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mCentralSurfaces.mKeyguardIndicationController = mKeyguardIndicationController; mCentralSurfaces.mBarService = mBarService; mCentralSurfaces.mStackScroller = mStackScroller; + mCentralSurfaces.mGestureWakeLock = mPowerManager.newWakeLock( + PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "sysui:GestureWakeLock"); mCentralSurfaces.startKeyguard(); mInitController.executePostInitTasks(); notificationLogger.setUpWithContainer(mNotificationListContainer); @@ -1017,6 +1023,60 @@ public class CentralSurfacesImplTest extends SysuiTestCase { } @Test + public void collapseShade_callsAnimateCollapsePanels_whenExpanded() { + // GIVEN the shade is expanded + mCentralSurfaces.setPanelExpanded(true); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + + // WHEN collapseShade is called + mCentralSurfaces.collapseShade(); + + // VERIFY that animateCollapsePanels is called + verify(mShadeController).animateCollapsePanels(); + } + + @Test + public void collapseShade_doesNotCallAnimateCollapsePanels_whenCollapsed() { + // GIVEN the shade is collapsed + mCentralSurfaces.setPanelExpanded(false); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + + // WHEN collapseShade is called + mCentralSurfaces.collapseShade(); + + // VERIFY that animateCollapsePanels is NOT called + verify(mShadeController, never()).animateCollapsePanels(); + } + + @Test + public void collapseShadeForBugReport_callsAnimateCollapsePanels_whenFlagDisabled() { + // GIVEN the shade is expanded & flag enabled + mCentralSurfaces.setPanelExpanded(true); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, false); + + // WHEN collapseShadeForBugreport is called + mCentralSurfaces.collapseShadeForBugreport(); + + // VERIFY that animateCollapsePanels is called + verify(mShadeController).animateCollapsePanels(); + } + + @Test + public void collapseShadeForBugReport_doesNotCallAnimateCollapsePanels_whenFlagEnabled() { + // GIVEN the shade is expanded & flag enabled + mCentralSurfaces.setPanelExpanded(true); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, true); + + // WHEN collapseShadeForBugreport is called + mCentralSurfaces.collapseShadeForBugreport(); + + // VERIFY that animateCollapsePanels is called + verify(mShadeController, never()).animateCollapsePanels(); + } + + @Test public void deviceStateChange_unfolded_shadeOpen_setsLeaveOpenOnKeyguardHide() { when(mKeyguardStateController.isShowing()).thenReturn(false); setFoldedStates(FOLD_STATE_FOLDED); @@ -1068,6 +1128,55 @@ public class CentralSurfacesImplTest extends SysuiTestCase { assertThat(onDismissActionCaptor.getValue().onDismiss()).isFalse(); } + @Test + public void testKeyguardHideDelayedIfOcclusionAnimationRunning() { + // Show the keyguard and verify we've done so. + setKeyguardShowingAndOccluded(true /* showing */, false /* occluded */); + verify(mStatusBarStateController).setState(StatusBarState.KEYGUARD); + + // Request to hide the keyguard, but while the occlude animation is playing. We should delay + // this hide call, since we're playing the occlude animation over the keyguard and thus want + // it to remain visible. + when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true); + setKeyguardShowingAndOccluded(false /* showing */, true /* occluded */); + verify(mStatusBarStateController, never()).setState(StatusBarState.SHADE); + + // Once the animation ends, verify that the keyguard is actually hidden. + when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(false); + setKeyguardShowingAndOccluded(false /* showing */, true /* occluded */); + verify(mStatusBarStateController).setState(StatusBarState.SHADE); + } + + @Test + public void testKeyguardHideNotDelayedIfOcclusionAnimationNotRunning() { + // Show the keyguard and verify we've done so. + setKeyguardShowingAndOccluded(true /* showing */, false /* occluded */); + verify(mStatusBarStateController).setState(StatusBarState.KEYGUARD); + + // Hide the keyguard while the occlusion animation is not running. Verify that we + // immediately hide the keyguard. + when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(false); + setKeyguardShowingAndOccluded(false /* showing */, true /* occluded */); + verify(mStatusBarStateController).setState(StatusBarState.SHADE); + } + + /** + * Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard} + * to reconfigure the keyguard to reflect the requested showing/occluded states. + */ + private void setKeyguardShowingAndOccluded(boolean showing, boolean occluded) { + when(mStatusBarStateController.isKeyguardRequested()).thenReturn(showing); + when(mKeyguardStateController.isOccluded()).thenReturn(occluded); + + // If we want to show the keyguard, make sure that we think we're awake and not unlocking. + if (showing) { + when(mBiometricUnlockController.isWakeAndUnlock()).thenReturn(false); + mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN); + } + + mCentralSurfaces.updateIsKeyguard(false /* forceStateChange */); + } + private void setDeviceState(int state) { ArgumentCaptor<DeviceStateManager.DeviceStateCallback> callbackCaptor = ArgumentCaptor.forClass(DeviceStateManager.DeviceStateCallback.class); @@ -1102,7 +1211,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { NotificationInterruptLogger logger, Handler mainHandler, NotifPipelineFlags flags, - KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) { + KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider, + UiEventLogger uiEventLogger) { super( contentResolver, powerManager, @@ -1115,7 +1225,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { logger, mainHandler, flags, - keyguardNotificationVisibilityProvider + keyguardNotificationVisibilityProvider, + uiEventLogger ); mUseHeadsUp = true; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 4d1a52c494ab..a5deaa45bf0f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.statusbar.phone.ScrimController.KEYGUARD_SCRIM_ALPHA; import static com.android.systemui.statusbar.phone.ScrimController.OPAQUE; import static com.android.systemui.statusbar.phone.ScrimController.SEMI_TRANSPARENT; import static com.android.systemui.statusbar.phone.ScrimController.TRANSPARENT; @@ -58,6 +59,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.scrim.ScrimView; import com.android.systemui.statusbar.policy.FakeConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -117,6 +119,7 @@ public class ScrimControllerTest extends SysuiTestCase { // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The // event-dispatch-on-registration pattern caused some of these unit tests to fail.) @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + @Mock private KeyguardViewMediator mKeyguardViewMediator; private static class AnimatorListener implements Animator.AnimatorListener { private int mNumStarts; @@ -230,7 +233,8 @@ public class ScrimControllerTest extends SysuiTestCase { mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()), mScreenOffAnimationController, mKeyguardUnlockAnimationController, - mStatusBarKeyguardViewManager); + mStatusBarKeyguardViewManager, + mKeyguardViewMediator); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -239,6 +243,8 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.setWallpaperSupportsAmbientMode(false); mScrimController.transitionTo(ScrimState.KEYGUARD); finishAnimationsImmediately(); + + mScrimController.setLaunchingAffordanceWithPreview(false); } @After @@ -852,7 +858,8 @@ public class ScrimControllerTest extends SysuiTestCase { mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()), mScreenOffAnimationController, mKeyguardUnlockAnimationController, - mStatusBarKeyguardViewManager); + mStatusBarKeyguardViewManager, + mKeyguardViewMediator); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -1592,6 +1599,30 @@ public class ScrimControllerTest extends SysuiTestCase { assertScrimAlpha(mScrimBehind, 0); } + @Test + public void keyguardAlpha_whenUnlockedForOcclusion_ifPlayingOcclusionAnimation() { + mScrimController.transitionTo(ScrimState.KEYGUARD); + + when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true); + + mScrimController.transitionTo(ScrimState.UNLOCKED); + finishAnimationsImmediately(); + + assertScrimAlpha(mNotificationsScrim, (int) (KEYGUARD_SCRIM_ALPHA * 255f)); + } + + @Test + public void keyguardAlpha_whenUnlockedForLaunch_ifLaunchingAffordance() { + mScrimController.transitionTo(ScrimState.KEYGUARD); + when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true); + mScrimController.setLaunchingAffordanceWithPreview(true); + + mScrimController.transitionTo(ScrimState.UNLOCKED); + finishAnimationsImmediately(); + + assertScrimAlpha(mNotificationsScrim, (int) (KEYGUARD_SCRIM_ALPHA * 255f)); + } + private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) { mScrimController.setRawPanelExpansionFraction(expansion); finishAnimationsImmediately(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 8da8d049516e..0c35659b458a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -117,7 +117,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private BouncerCallbackInteractor mBouncerCallbackInteractor; @Mock private BouncerInteractor mBouncerInteractor; @Mock private BouncerView mBouncerView; -// @Mock private WeakReference<BouncerViewDelegate> mBouncerViewDelegateWeakReference; @Mock private BouncerViewDelegate mBouncerViewDelegate; private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt index 1ee8875eada8..78a4db1224cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt @@ -20,7 +20,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogcatEchoTracker import com.android.systemui.statusbar.DisableFlagsLogger import com.google.common.truth.Truth.assertThat import org.junit.Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 63467e7039d4..438271c489e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -49,9 +49,9 @@ import com.android.systemui.R; import com.android.systemui.SysuiBaseFragmentTest; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.log.LogBuffer; -import com.android.systemui.log.LogcatEchoTracker; import com.android.systemui.plugins.DarkIconDispatcher; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogcatEchoTracker; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.ShadeExpansionStateManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt index bf432388ad28..eba3b04f3472 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt @@ -20,7 +20,6 @@ import android.content.Intent import android.os.UserHandle import android.testing.AndroidTestingRunner import android.testing.TestableLooper -import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags @@ -34,8 +33,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -91,7 +90,7 @@ class StatusBarUserSwitcherControllerOldImplTest : SysuiTestCase() { fun testStartActivity() { `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(false) statusBarUserSwitcherContainer.callOnClick() - verify(userSwitcherDialogController).showDialog(any(View::class.java)) + verify(userSwitcherDialogController).showDialog(any(), any()) `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(true) statusBarUserSwitcherContainer.callOnClick() verify(activityStarter).startActivity(any(Intent::class.java), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt new file mode 100644 index 000000000000..b7a6c0125cfa --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt @@ -0,0 +1,116 @@ +/* + * 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.pipeline.airplane.data.repository + +import android.os.Handler +import android.os.Looper +import android.os.UserHandle +import android.provider.Settings.Global +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class AirplaneModeRepositoryImplTest : SysuiTestCase() { + + private lateinit var underTest: AirplaneModeRepositoryImpl + + @Mock private lateinit var logger: ConnectivityPipelineLogger + private lateinit var bgHandler: Handler + private lateinit var scope: CoroutineScope + private lateinit var settings: FakeSettings + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + bgHandler = Handler(Looper.getMainLooper()) + scope = CoroutineScope(IMMEDIATE) + settings = FakeSettings() + settings.userId = UserHandle.USER_ALL + + underTest = + AirplaneModeRepositoryImpl( + bgHandler, + settings, + logger, + scope, + ) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun isAirplaneMode_initiallyGetsSettingsValue() = + runBlocking(IMMEDIATE) { + settings.putInt(Global.AIRPLANE_MODE_ON, 1) + + underTest = + AirplaneModeRepositoryImpl( + bgHandler, + settings, + logger, + scope, + ) + + val job = underTest.isAirplaneMode.launchIn(this) + + assertThat(underTest.isAirplaneMode.value).isTrue() + + job.cancel() + } + + @Test + fun isAirplaneMode_settingUpdated_valueUpdated() = + runBlocking(IMMEDIATE) { + val job = underTest.isAirplaneMode.launchIn(this) + + settings.putInt(Global.AIRPLANE_MODE_ON, 0) + yield() + assertThat(underTest.isAirplaneMode.value).isFalse() + + settings.putInt(Global.AIRPLANE_MODE_ON, 1) + yield() + assertThat(underTest.isAirplaneMode.value).isTrue() + + settings.putInt(Global.AIRPLANE_MODE_ON, 0) + yield() + assertThat(underTest.isAirplaneMode.value).isFalse() + + job.cancel() + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt new file mode 100644 index 000000000000..63bbdfca0071 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.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.statusbar.pipeline.airplane.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class FakeAirplaneModeRepository : AirplaneModeRepository { + private val _isAirplaneMode = MutableStateFlow(false) + override val isAirplaneMode: StateFlow<Boolean> = _isAirplaneMode + + fun setIsAirplaneMode(isAirplaneMode: Boolean) { + _isAirplaneMode.value = isAirplaneMode + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt new file mode 100644 index 000000000000..33a80e1a3dd6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt @@ -0,0 +1,99 @@ +/* + * 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.pipeline.airplane.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.Before +import org.junit.Test + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +class AirplaneModeInteractorTest : SysuiTestCase() { + + private lateinit var underTest: AirplaneModeInteractor + + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository + private lateinit var connectivityRepository: FakeConnectivityRepository + + @Before + fun setUp() { + airplaneModeRepository = FakeAirplaneModeRepository() + connectivityRepository = FakeConnectivityRepository() + underTest = AirplaneModeInteractor(airplaneModeRepository, connectivityRepository) + } + + @Test + fun isAirplaneMode_matchesRepo() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isAirplaneMode.onEach { latest = it }.launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + yield() + assertThat(latest).isTrue() + + airplaneModeRepository.setIsAirplaneMode(false) + yield() + assertThat(latest).isFalse() + + airplaneModeRepository.setIsAirplaneMode(true) + yield() + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isForceHidden_repoHasWifiHidden_outputsTrue() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE)) + + var latest: Boolean? = null + val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isForceHidden_repoDoesNotHaveWifiHidden_outputsFalse() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf()) + + var latest: Boolean? = null + val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt new file mode 100644 index 000000000000..76016a121e68 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt @@ -0,0 +1,110 @@ +/* + * 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.pipeline.airplane.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +class AirplaneModeViewModelTest : SysuiTestCase() { + + private lateinit var underTest: AirplaneModeViewModel + + @Mock private lateinit var logger: ConnectivityPipelineLogger + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository + private lateinit var connectivityRepository: FakeConnectivityRepository + private lateinit var interactor: AirplaneModeInteractor + private lateinit var scope: CoroutineScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() + connectivityRepository = FakeConnectivityRepository() + interactor = AirplaneModeInteractor(airplaneModeRepository, connectivityRepository) + scope = CoroutineScope(IMMEDIATE) + + underTest = + AirplaneModeViewModel( + interactor, + logger, + scope, + ) + } + + @Test + fun isAirplaneModeIconVisible_notAirplaneMode_outputsFalse() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf()) + airplaneModeRepository.setIsAirplaneMode(false) + + var latest: Boolean? = null + val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isAirplaneModeIconVisible_forceHidden_outputsFalse() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE)) + airplaneModeRepository.setIsAirplaneMode(true) + + var latest: Boolean? = null + val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isAirplaneModeIconVisible_isAirplaneModeAndNotForceHidden_outputsTrue() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf()) + airplaneModeRepository.setIsAirplaneMode(true) + + var latest: Boolean? = null + val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt new file mode 100644 index 000000000000..6ff7b7ccd5e3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.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.statusbar.pipeline.mobile.data.repository + +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeMobileConnectionRepository : MobileConnectionRepository { + private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel()) + override val subscriptionModelFlow: Flow<MobileSubscriptionModel> = _subscriptionsModelFlow + + fun setMobileSubscriptionModel(model: MobileSubscriptionModel) { + _subscriptionsModelFlow.value = model + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index 0d1526883023..c88d468f1755 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -18,11 +18,11 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.settingslib.mobile.MobileMappings.Config import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -class FakeMobileSubscriptionRepository : MobileSubscriptionRepository { +class FakeMobileConnectionsRepository : MobileConnectionsRepository { private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf()) override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow @@ -30,22 +30,27 @@ class FakeMobileSubscriptionRepository : MobileSubscriptionRepository { MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID) override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId - private val subIdFlows = mutableMapOf<Int, MutableStateFlow<MobileSubscriptionModel>>() - override fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> { - return subIdFlows[subId] - ?: MutableStateFlow(MobileSubscriptionModel()).also { subIdFlows[subId] = it } + private val _defaultDataSubRatConfig = MutableStateFlow(Config()) + override val defaultDataSubRatConfig = _defaultDataSubRatConfig + + private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>() + override fun getRepoForSubId(subId: Int): MobileConnectionRepository { + return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it } } fun setSubscriptions(subs: List<SubscriptionInfo>) { _subscriptionsFlow.value = subs } + fun setDefaultDataSubRatConfig(config: Config) { + _defaultDataSubRatConfig.value = config + } + fun setActiveMobileDataSubscriptionId(subId: Int) { _activeMobileDataSubscriptionId.value = subId } - fun setMobileSubscriptionModel(model: MobileSubscriptionModel, subId: Int) { - val subscription = subIdFlows[subId] ?: throw Exception("no flow exists for this subId yet") - subscription.value = model + fun setMobileConnectionRepositoryForId(subId: Int, repo: MobileConnectionRepository) { + subIdRepos[subId] = repo } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt index 316b795ac949..775e6dbb5e19 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt @@ -22,18 +22,18 @@ import android.telephony.SignalStrength import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback -import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener -import android.telephony.TelephonyCallback.CarrierNetworkListener -import android.telephony.TelephonyCallback.DataActivityListener -import android.telephony.TelephonyCallback.DataConnectionStateListener -import android.telephony.TelephonyCallback.DisplayInfoListener import android.telephony.TelephonyCallback.ServiceStateListener -import android.telephony.TelephonyCallback.SignalStrengthsListener import android.telephony.TelephonyDisplayInfo +import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA import android.telephony.TelephonyManager +import android.telephony.TelephonyManager.NETWORK_TYPE_LTE +import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock @@ -50,28 +50,32 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.mockito.Mock -import org.mockito.Mockito.verify +import org.mockito.Mockito import org.mockito.MockitoAnnotations @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) @SmallTest -class MobileSubscriptionRepositoryTest : SysuiTestCase() { - private lateinit var underTest: MobileSubscriptionRepositoryImpl +class MobileConnectionRepositoryTest : SysuiTestCase() { + private lateinit var underTest: MobileConnectionRepositoryImpl @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager + @Mock private lateinit var logger: ConnectivityPipelineLogger + private val scope = CoroutineScope(IMMEDIATE) @Before fun setUp() { MockitoAnnotations.initMocks(this) + whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) underTest = - MobileSubscriptionRepositoryImpl( - subscriptionManager, + MobileConnectionRepositoryImpl( + SUB_1_ID, telephonyManager, IMMEDIATE, + logger, scope, ) } @@ -82,78 +86,10 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { } @Test - fun testSubscriptions_initiallyEmpty() = - runBlocking(IMMEDIATE) { - assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>()) - } - - @Test - fun testSubscriptions_listUpdates() = - runBlocking(IMMEDIATE) { - var latest: List<SubscriptionInfo>? = null - - val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) - - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_2)) - getSubscriptionCallback().onSubscriptionsChanged() - - assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2)) - - job.cancel() - } - - @Test - fun testSubscriptions_removingSub_updatesList() = - runBlocking(IMMEDIATE) { - var latest: List<SubscriptionInfo>? = null - - val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) - - // WHEN 2 networks show up - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_2)) - getSubscriptionCallback().onSubscriptionsChanged() - - // WHEN one network is removed - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_2)) - getSubscriptionCallback().onSubscriptionsChanged() - - // THEN the subscriptions list represents the newest change - assertThat(latest).isEqualTo(listOf(SUB_2)) - - job.cancel() - } - - @Test - fun testActiveDataSubscriptionId_initialValueIsInvalidId() = - runBlocking(IMMEDIATE) { - assertThat(underTest.activeMobileDataSubscriptionId.value) - .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID) - } - - @Test - fun testActiveDataSubscriptionId_updates() = - runBlocking(IMMEDIATE) { - var active: Int? = null - - val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this) - - getActiveDataSubscriptionCallback().onActiveDataSubscriptionIdChanged(SUB_2_ID) - - assertThat(active).isEqualTo(SUB_2_ID) - - job.cancel() - } - - @Test fun testFlowForSubId_default() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) assertThat(latest).isEqualTo(MobileSubscriptionModel()) @@ -163,10 +99,8 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { @Test fun testFlowForSubId_emergencyOnly() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) val serviceState = ServiceState() serviceState.isEmergencyOnly = true @@ -181,10 +115,8 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { @Test fun testFlowForSubId_emergencyOnly_toggles() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<ServiceStateListener>() val serviceState = ServiceState() @@ -201,13 +133,11 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { @Test fun testFlowForSubId_signalStrengths_levelsUpdate() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val callback = getTelephonyCallbackForType<SignalStrengthsListener>() - val strength = signalStrength(1, 2, true) + val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() + val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) callback.onSignalStrengthsChanged(strength) assertThat(latest?.isGsm).isEqualTo(true) @@ -220,12 +150,11 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { @Test fun testFlowForSubId_dataConnectionState() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val callback = getTelephonyCallbackForType<DataConnectionStateListener>() + val callback = + getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(100, 200 /* unused */) assertThat(latest?.dataConnectionState).isEqualTo(100) @@ -236,12 +165,10 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { @Test fun testFlowForSubId_dataActivity() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val callback = getTelephonyCallbackForType<DataActivityListener>() + val callback = getTelephonyCallbackForType<TelephonyCallback.DataActivityListener>() callback.onDataActivity(3) assertThat(latest?.dataActivityDirection).isEqualTo(3) @@ -252,12 +179,10 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { @Test fun testFlowForSubId_carrierNetworkChange() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val callback = getTelephonyCallbackForType<CarrierNetworkListener>() + val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>() callback.onCarrierNetworkChange(true) assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true) @@ -266,65 +191,59 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { } @Test - fun testFlowForSubId_displayInfo() = + fun subscriptionFlow_networkType_default() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val callback = getTelephonyCallbackForType<DisplayInfoListener>() - val ti = mock<TelephonyDisplayInfo>() - callback.onDisplayInfoChanged(ti) + val type = NETWORK_TYPE_UNKNOWN + val expected = DefaultNetworkType(type) - assertThat(latest?.displayInfo).isEqualTo(ti) + assertThat(latest?.resolvedNetworkType).isEqualTo(expected) job.cancel() } @Test - fun testFlowForSubId_isCached() = + fun subscriptionFlow_networkType_updatesUsingDefault() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) - val state1 = underTest.getFlowForSubId(SUB_1_ID) - val state2 = underTest.getFlowForSubId(SUB_1_ID) + val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() + val type = NETWORK_TYPE_LTE + val expected = DefaultNetworkType(type) + val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) } + callback.onDisplayInfoChanged(ti) - assertThat(state1).isEqualTo(state2) + assertThat(latest?.resolvedNetworkType).isEqualTo(expected) + + job.cancel() } @Test - fun testFlowForSubId_isRemovedAfterFinish() = + fun subscriptionFlow_networkType_updatesUsingOverride() = runBlocking(IMMEDIATE) { - whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) - var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() + val type = OVERRIDE_NETWORK_TYPE_LTE_CA + val expected = OverrideNetworkType(type) + val ti = + mock<TelephonyDisplayInfo>().also { + whenever(it.overrideNetworkType).thenReturn(type) + } + callback.onDisplayInfoChanged(ti) - // Start collecting on some flow - val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) - - // There should be once cached flow now - assertThat(underTest.getSubIdFlowCache().size).isEqualTo(1) + assertThat(latest?.resolvedNetworkType).isEqualTo(expected) - // When the job is canceled, the cache should be cleared job.cancel() - - assertThat(underTest.getSubIdFlowCache().size).isEqualTo(0) } - private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { - val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>() - verify(subscriptionManager) - .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture()) - return callbackCaptor.value!! - } - - private fun getActiveDataSubscriptionCallback(): ActiveDataSubscriptionIdListener = - getTelephonyCallbackForType() - private fun getTelephonyCallbacks(): List<TelephonyCallback> { val callbackCaptor = argumentCaptor<TelephonyCallback>() - verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) + Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) return callbackCaptor.allValues } @@ -352,9 +271,5 @@ class MobileSubscriptionRepositoryTest : SysuiTestCase() { private const val SUB_1_ID = 1 private val SUB_1 = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } - - private const val SUB_2_ID = 2 - private val SUB_2 = - mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt new file mode 100644 index 000000000000..326e0d28166f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt @@ -0,0 +1,246 @@ +/* + * 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.pipeline.mobile.data.repository + +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyCallback +import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener +import android.telephony.TelephonyManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.nullable +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class MobileConnectionsRepositoryTest : SysuiTestCase() { + private lateinit var underTest: MobileConnectionsRepositoryImpl + + @Mock private lateinit var subscriptionManager: SubscriptionManager + @Mock private lateinit var telephonyManager: TelephonyManager + @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + + private val scope = CoroutineScope(IMMEDIATE) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever( + broadcastDispatcher.broadcastFlow( + any(), + nullable(), + ArgumentMatchers.anyInt(), + nullable(), + ) + ) + .thenReturn(flowOf(Unit)) + + underTest = + MobileConnectionsRepositoryImpl( + subscriptionManager, + telephonyManager, + logger, + broadcastDispatcher, + context, + IMMEDIATE, + scope, + mock(), + ) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun testSubscriptions_initiallyEmpty() = + runBlocking(IMMEDIATE) { + assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>()) + } + + @Test + fun testSubscriptions_listUpdates() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionInfo>? = null + + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2)) + + job.cancel() + } + + @Test + fun testSubscriptions_removingSub_updatesList() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionInfo>? = null + + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + // WHEN 2 networks show up + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // WHEN one network is removed + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // THEN the subscriptions list represents the newest change + assertThat(latest).isEqualTo(listOf(SUB_2)) + + job.cancel() + } + + @Test + fun testActiveDataSubscriptionId_initialValueIsInvalidId() = + runBlocking(IMMEDIATE) { + assertThat(underTest.activeMobileDataSubscriptionId.value) + .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + } + + @Test + fun testActiveDataSubscriptionId_updates() = + runBlocking(IMMEDIATE) { + var active: Int? = null + + val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this) + + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() + .onActiveDataSubscriptionIdChanged(SUB_2_ID) + + assertThat(active).isEqualTo(SUB_2_ID) + + job.cancel() + } + + @Test + fun testConnectionRepository_validSubId_isCached() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptionsFlow.launchIn(this) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repo2 = underTest.getRepoForSubId(SUB_1_ID) + + assertThat(repo1).isSameInstanceAs(repo2) + + job.cancel() + } + + @Test + fun testConnectionCache_clearsInvalidSubscriptions() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptionsFlow.launchIn(this) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Get repos to trigger caching + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repo2 = underTest.getRepoForSubId(SUB_2_ID) + + assertThat(underTest.getSubIdRepoCache()) + .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2) + + // SUB_2 disappears + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) + + job.cancel() + } + + @Test + fun testConnectionRepository_invalidSubId_throws() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptionsFlow.launchIn(this) + + assertThrows(IllegalArgumentException::class.java) { + underTest.getRepoForSubId(SUB_1_ID) + } + + job.cancel() + } + + private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { + val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>() + verify(subscriptionManager) + .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture()) + return callbackCaptor.value!! + } + + private fun getTelephonyCallbacks(): List<TelephonyCallback> { + val callbackCaptor = argumentCaptor<TelephonyCallback>() + verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) + return callbackCaptor.allValues + } + + private inline fun <reified T> getTelephonyCallbackForType(): T { + val cbs = getTelephonyCallbacks().filterIsInstance<T>() + assertThat(cbs.size).isEqualTo(1) + return cbs[0] + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private const val SUB_1_ID = 1 + private val SUB_1 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + + private const val SUB_2_ID = 2 + private val SUB_2 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index 8ec68f36a837..cd4dbebcc35c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeMobileIconInteractor : MobileIconInteractor { private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN) - override val iconGroup = _iconGroup + override val networkTypeIconGroup = _iconGroup private val _isEmergencyOnly = MutableStateFlow<Boolean>(false) override val isEmergencyOnly = _isEmergencyOnly diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt new file mode 100644 index 000000000000..2bd228603cb0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -0,0 +1,75 @@ +/* + * 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.pipeline.mobile.domain.interactor + +import android.telephony.SubscriptionInfo +import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO +import android.telephony.TelephonyManager.NETWORK_TYPE_GSM +import android.telephony.TelephonyManager.NETWORK_TYPE_LTE +import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) : + MobileIconsInteractor { + val THREE_G_KEY = mobileMappings.toIconKey(THREE_G) + val LTE_KEY = mobileMappings.toIconKey(LTE) + val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G) + val FIVE_G_OVERRIDE_KEY = mobileMappings.toIconKeyOverride(FIVE_G_OVERRIDE) + + /** + * To avoid a reliance on [MobileMappings], we'll build a simpler map from network type to + * mobile icon. See TelephonyManager.NETWORK_TYPES for a list of types and [TelephonyIcons] for + * the exhaustive set of icons + */ + val TEST_MAPPING: Map<String, MobileIconGroup> = + mapOf( + THREE_G_KEY to TelephonyIcons.THREE_G, + LTE_KEY to TelephonyIcons.LTE, + FOUR_G_KEY to TelephonyIcons.FOUR_G, + FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G, + ) + + private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf()) + override val filteredSubscriptions = _filteredSubscriptions + + private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) + override val defaultMobileIconMapping = _defaultMobileIconMapping + + private val _defaultMobileIconGroup = MutableStateFlow(DEFAULT_ICON) + override val defaultMobileIconGroup = _defaultMobileIconGroup + + private val _isUserSetup = MutableStateFlow(true) + override val isUserSetup = _isUserSetup + + /** Always returns a new fake interactor */ + override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor { + return FakeMobileIconInteractor() + } + + companion object { + val DEFAULT_ICON = TelephonyIcons.G + + // Use [MobileMappings] to define some simple definitions + const val THREE_G = NETWORK_TYPE_GSM + const val LTE = NETWORK_TYPE_LTE + const val FOUR_G = NETWORK_TYPE_UMTS + const val FIVE_G_OVERRIDE = OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 2f07d9cb3831..ff44af4c9204 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -18,10 +18,19 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CellSignalStrength import android.telephony.SubscriptionInfo +import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.filters.SmallTest +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel -import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.THREE_G +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -29,26 +38,33 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.Before import org.junit.Test @SmallTest class MobileIconInteractorTest : SysuiTestCase() { private lateinit var underTest: MobileIconInteractor - private val mobileSubscriptionRepository = FakeMobileSubscriptionRepository() - private val sub1Flow = mobileSubscriptionRepository.getFlowForSubId(SUB_1_ID) + private val mobileMappingsProxy = FakeMobileMappingsProxy() + private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy) + private val connectionRepository = FakeMobileConnectionRepository() @Before fun setUp() { - underTest = MobileIconInteractorImpl(sub1Flow) + underTest = + MobileIconInteractorImpl( + mobileIconsInteractor.defaultMobileIconMapping, + mobileIconsInteractor.defaultMobileIconGroup, + mobileMappingsProxy, + connectionRepository, + ) } @Test fun gsm_level_default_unknown() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel(isGsm = true), - SUB_1_ID ) var latest: Int? = null @@ -62,13 +78,12 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun gsm_usesGsmLevel() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel( isGsm = true, primaryLevel = GSM_LEVEL, cdmaLevel = CDMA_LEVEL ), - SUB_1_ID ) var latest: Int? = null @@ -82,9 +97,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun cdma_level_default_unknown() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel(isGsm = false), - SUB_1_ID ) var latest: Int? = null @@ -97,13 +111,12 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun cdma_usesCdmaLevel() = runBlocking(IMMEDIATE) { - mobileSubscriptionRepository.setMobileSubscriptionModel( + connectionRepository.setMobileSubscriptionModel( MobileSubscriptionModel( isGsm = false, primaryLevel = GSM_LEVEL, cdmaLevel = CDMA_LEVEL ), - SUB_1_ID ) var latest: Int? = null @@ -114,6 +127,75 @@ class MobileIconInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun iconGroup_three_g() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(TelephonyIcons.THREE_G) + + job.cancel() + } + + @Test + fun iconGroup_updates_on_change() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel( + resolvedNetworkType = DefaultNetworkType(FOUR_G), + ), + ) + yield() + + assertThat(latest).isEqualTo(TelephonyIcons.FOUR_G) + + job.cancel() + } + + @Test + fun iconGroup_5g_override_type() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(resolvedNetworkType = OverrideNetworkType(FIVE_G_OVERRIDE)), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(TelephonyIcons.NR_5G) + + job.cancel() + } + + @Test + fun iconGroup_default_if_no_lookup() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel( + resolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN), + ), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(FakeMobileIconsInteractor.DEFAULT_ICON) + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate @@ -123,9 +205,5 @@ class MobileIconInteractorTest : SysuiTestCase() { private const val SUB_1_ID = 1 private val SUB_1 = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } - - private const val SUB_2_ID = 2 - private val SUB_2 = - mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index 89ad9cb9e51e..b01efd18971f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -19,12 +19,14 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.SubscriptionInfo import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -39,7 +41,9 @@ import org.mockito.MockitoAnnotations class MobileIconsInteractorTest : SysuiTestCase() { private lateinit var underTest: MobileIconsInteractor private val userSetupRepository = FakeUserSetupRepository() - private val subscriptionsRepository = FakeMobileSubscriptionRepository() + private val subscriptionsRepository = FakeMobileConnectionsRepository() + private val mobileMappingsProxy = FakeMobileMappingsProxy() + private val scope = CoroutineScope(IMMEDIATE) @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker @@ -47,10 +51,12 @@ class MobileIconsInteractorTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) underTest = - MobileIconsInteractor( + MobileIconsInteractorImpl( subscriptionsRepository, carrierConfigTracker, + mobileMappingsProxy, userSetupRepository, + scope ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt new file mode 100644 index 000000000000..6d8d902615de --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt @@ -0,0 +1,46 @@ +/* + * 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.pipeline.mobile.util + +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.MobileMappings.Config +import com.android.settingslib.mobile.TelephonyIcons + +class FakeMobileMappingsProxy : MobileMappingsProxy { + private var iconMap = mapOf<String, MobileIconGroup>() + private var defaultIcons = TelephonyIcons.THREE_G + + fun setIconMap(map: Map<String, MobileIconGroup>) { + iconMap = map + } + override fun mapIconSets(config: Config): Map<String, MobileIconGroup> = iconMap + fun getIconMap() = iconMap + + fun setDefaultIcons(group: MobileIconGroup) { + defaultIcons = group + } + override fun getDefaultIcons(config: Config): MobileIconGroup = defaultIcons + fun getDefaultIcons(): MobileIconGroup = defaultIcons + + override fun toIconKey(networkType: Int): String { + return networkType.toString() + } + + override fun toIconKeyOverride(networkType: Int): String { + return toIconKey(networkType) + "_override" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt index 0e75c74ef6f5..b32058fca109 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt @@ -22,7 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogcatEchoTracker import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.google.common.truth.Truth.assertThat diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt index f751afc195b2..2f18ce31217e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt @@ -27,6 +27,9 @@ class FakeWifiRepository : WifiRepository { private val _isWifiEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false) override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled + private val _isWifiDefault: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault + private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> = MutableStateFlow(WifiNetworkModel.Inactive) override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork @@ -38,6 +41,10 @@ class FakeWifiRepository : WifiRepository { _isWifiEnabled.value = enabled } + fun setIsWifiDefault(default: Boolean) { + _isWifiDefault.value = default + } + fun setWifiNetwork(wifiNetworkModel: WifiNetworkModel) { _wifiNetwork.value = wifiNetworkModel } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt index 0ba0bd623c39..a64a4bd2e57a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt @@ -222,6 +222,83 @@ class WifiRepositoryImplTest : SysuiTestCase() { } @Test + fun isWifiDefault_initiallyGetsDefault() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + assertThat(underTest.isWifiDefault.value).isFalse() + + job.cancel() + } + + @Test + fun isWifiDefault_wifiNetwork_isTrue() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + val wifiInfo = mock<WifiInfo>().apply { + whenever(this.ssid).thenReturn(SSID) + } + + getDefaultNetworkCallback().onCapabilitiesChanged( + NETWORK, + createWifiNetworkCapabilities(wifiInfo) + ) + + assertThat(underTest.isWifiDefault.value).isTrue() + + job.cancel() + } + + @Test + fun isWifiDefault_cellularVcnNetwork_isTrue() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + val capabilities = mock<NetworkCapabilities>().apply { + whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO)) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(underTest.isWifiDefault.value).isTrue() + + job.cancel() + } + + @Test + fun isWifiDefault_cellularNotVcnNetwork_isFalse() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + val capabilities = mock<NetworkCapabilities>().apply { + whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(this.transportInfo).thenReturn(mock()) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(underTest.isWifiDefault.value).isFalse() + + job.cancel() + } + + @Test + fun isWifiDefault_wifiNetworkLost_isFalse() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + // First, add a network + getDefaultNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) + assertThat(underTest.isWifiDefault.value).isTrue() + + // WHEN the network is lost + getDefaultNetworkCallback().onLost(NETWORK) + + // THEN we update to false + assertThat(underTest.isWifiDefault.value).isFalse() + + job.cancel() + } + + @Test fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) { var latest: WifiNetworkModel? = null val job = underTest @@ -745,6 +822,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { return callbackCaptor.value!! } + private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { + val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>() + verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) + return callbackCaptor.value!! + } + private fun createWifiNetworkCapabilities( wifiInfo: WifiInfo, isValidated: Boolean = true, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt index 39b886af1cb8..71b8bab87d19 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt @@ -178,6 +178,29 @@ class WifiInteractorTest : SysuiTestCase() { } @Test + fun isDefault_matchesRepoIsDefault() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .isDefault + .onEach { latest = it } + .launchIn(this) + + wifiRepository.setIsWifiDefault(true) + yield() + assertThat(latest).isTrue() + + wifiRepository.setIsWifiDefault(false) + yield() + assertThat(latest).isFalse() + + wifiRepository.setIsWifiDefault(true) + yield() + assertThat(latest).isTrue() + + job.cancel() + } + + @Test fun wifiNetwork_matchesRepoWifiNetwork() = runBlocking(IMMEDIATE) { val wifiNetwork = WifiNetworkModel.Active( networkId = 45, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt index 4efb13520ebf..c5841098010a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt @@ -30,6 +30,9 @@ import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository @@ -63,11 +66,13 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor private lateinit var viewModel: WifiViewModel private lateinit var scope: CoroutineScope + private lateinit var airplaneModeViewModel: AirplaneModeViewModel @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule() @@ -77,12 +82,22 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) + airplaneModeRepository = FakeAirplaneModeRepository() connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) scope = CoroutineScope(Dispatchers.Unconfined) + airplaneModeViewModel = AirplaneModeViewModel( + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + ), + logger, + scope, + ) viewModel = WifiViewModel( + airplaneModeViewModel, connectivityConstants, context, logger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt index a3ad028519bb..a1afcd71e3c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt @@ -22,11 +22,14 @@ import androidx.test.filters.SmallTest import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot @@ -64,19 +67,31 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor + private lateinit var airplaneModeViewModel: AirplaneModeViewModel private lateinit var scope: CoroutineScope @Before fun setUp() { MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) scope = CoroutineScope(IMMEDIATE) + airplaneModeViewModel = + AirplaneModeViewModel( + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + ), + logger, + scope, + ) } @After @@ -88,6 +103,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase fun wifiIcon() = runBlocking(IMMEDIATE) { wifiRepository.setIsWifiEnabled(testCase.enabled) + wifiRepository.setIsWifiDefault(testCase.isDefault) connectivityRepository.setForceHiddenIcons( if (testCase.forceHidden) { setOf(ConnectivitySlot.WIFI) @@ -101,6 +117,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase .thenReturn(testCase.hasDataCapabilities) underTest = WifiViewModel( + airplaneModeViewModel, connectivityConstants, context, logger, @@ -125,19 +142,12 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase } else { testCase.expected.contentDescription.invoke(context) } - assertThat(iconFlow.value?.contentDescription?.getAsString()) + assertThat(iconFlow.value?.contentDescription?.loadContentDescription(context)) .isEqualTo(expectedContentDescription) job.cancel() } - private fun ContentDescription.getAsString(): String? { - return when (this) { - is ContentDescription.Loaded -> this.description - is ContentDescription.Resource -> context.getString(this.res) - } - } - internal data class Expected( /** The resource that should be used for the icon. */ @DrawableRes val iconResource: Int, @@ -159,6 +169,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase val forceHidden: Boolean = false, val alwaysShowIconWhenEnabled: Boolean = false, val hasDataCapabilities: Boolean = true, + val isDefault: Boolean = false, val network: WifiNetworkModel, /** The expected output. Null if we expect the output to be null. */ @@ -169,6 +180,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase "forceHidden=$forceHidden, " + "showWhenEnabled=$alwaysShowIconWhenEnabled, " + "hasDataCaps=$hasDataCapabilities, " + + "isDefault=$isDefault, " + "network=$network) then " + "EXPECTED($expected)" } @@ -303,6 +315,46 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase ), ), + // isDefault = true => all Inactive and Active networks shown + TestCase( + isDefault = true, + network = WifiNetworkModel.Inactive, + expected = + Expected( + iconResource = WIFI_NO_NETWORK, + contentDescription = { context -> + "${context.getString(WIFI_NO_CONNECTION)}," + + context.getString(NO_INTERNET) + }, + description = "No network icon", + ), + ), + TestCase( + isDefault = true, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 3), + expected = + Expected( + iconResource = WIFI_NO_INTERNET_ICONS[3], + contentDescription = { context -> + "${context.getString(WIFI_CONNECTION_STRENGTH[3])}," + + context.getString(NO_INTERNET) + }, + description = "No internet level 3 icon", + ), + ), + TestCase( + isDefault = true, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 1), + expected = + Expected( + iconResource = WIFI_FULL_ICONS[1], + contentDescription = { context -> + context.getString(WIFI_CONNECTION_STRENGTH[1]) + }, + description = "Full internet level 1 icon", + ), + ), + // network = CarrierMerged => not shown TestCase( network = WifiNetworkModel.CarrierMerged, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index 3169eef83f07..7d2c56098584 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -20,8 +20,12 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository @@ -55,19 +59,31 @@ class WifiViewModelTest : SysuiTestCase() { @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor + private lateinit var airplaneModeViewModel: AirplaneModeViewModel private lateinit var scope: CoroutineScope @Before fun setUp() { MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) scope = CoroutineScope(IMMEDIATE) + airplaneModeViewModel = AirplaneModeViewModel( + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + ), + logger, + scope, + ) + createAndSetViewModel() } @@ -76,6 +92,8 @@ class WifiViewModelTest : SysuiTestCase() { scope.cancel() } + // See [WifiViewModelIconParameterizedTest] for additional view model tests. + // Note on testing: [WifiViewModel] exposes 3 different instances of // [LocationBasedWifiViewModel]. In practice, these 3 different instances will get the exact // same data for icon, activity, etc. flows. So, most of these tests will test just one of the @@ -460,11 +478,64 @@ class WifiViewModelTest : SysuiTestCase() { job.cancel() } + @Test + fun airplaneSpacer_notAirplaneMode_outputsFalse() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .qs + .isAirplaneSpacerVisible + .onEach { latest = it } + .launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(false) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun airplaneSpacer_airplaneForceHidden_outputsFalse() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .qs + .isAirplaneSpacerVisible + .onEach { latest = it } + .launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE)) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun airplaneSpacer_airplaneIconVisible_outputsTrue() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .qs + .isAirplaneSpacerVisible + .onEach { latest = it } + .launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + private fun createAndSetViewModel() { // [WifiViewModel] creates its flows as soon as it's instantiated, and some of those flow // creations rely on certain config values that we mock out in individual tests. This method // allows tests to create the view model only after those configs are correctly set up. underTest = WifiViewModel( + airplaneModeViewModel, connectivityConstants, context, logger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index 6ace4044b3f7..915e999c2646 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -23,8 +23,12 @@ import static junit.framework.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.PendingIntent; @@ -43,10 +47,14 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.ContentInfo; import android.view.View; +import android.view.ViewRootImpl; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.widget.EditText; import android.widget.ImageButton; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; +import android.window.WindowOnBackInvokedDispatcher; import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; @@ -67,6 +75,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -229,6 +238,67 @@ public class RemoteInputViewTest extends SysuiTestCase { } @Test + public void testPredictiveBack_registerAndUnregister() throws Exception { + NotificationTestHelper helper = new NotificationTestHelper( + mContext, + mDependency, + TestableLooper.get(this)); + ExpandableNotificationRow row = helper.createRow(); + RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); + + ViewRootImpl viewRoot = mock(ViewRootImpl.class); + WindowOnBackInvokedDispatcher backInvokedDispatcher = mock( + WindowOnBackInvokedDispatcher.class); + ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass( + OnBackInvokedCallback.class); + when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher); + view.setViewRootImpl(viewRoot); + + /* verify that predictive back callback registered when RemoteInputView becomes visible */ + view.onVisibilityAggregated(true); + verify(backInvokedDispatcher).registerOnBackInvokedCallback( + eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), + onBackInvokedCallbackCaptor.capture()); + + /* verify that same callback unregistered when RemoteInputView becomes invisible */ + view.onVisibilityAggregated(false); + verify(backInvokedDispatcher).unregisterOnBackInvokedCallback( + eq(onBackInvokedCallbackCaptor.getValue())); + } + + @Test + public void testUiPredictiveBack_openAndDispatchCallback() throws Exception { + NotificationTestHelper helper = new NotificationTestHelper( + mContext, + mDependency, + TestableLooper.get(this)); + ExpandableNotificationRow row = helper.createRow(); + RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); + ViewRootImpl viewRoot = mock(ViewRootImpl.class); + WindowOnBackInvokedDispatcher backInvokedDispatcher = mock( + WindowOnBackInvokedDispatcher.class); + ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass( + OnBackInvokedCallback.class); + when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher); + view.setViewRootImpl(viewRoot); + view.onVisibilityAggregated(true); + view.setEditTextReferenceToSelf(); + + /* capture the callback during registration */ + verify(backInvokedDispatcher).registerOnBackInvokedCallback( + eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), + onBackInvokedCallbackCaptor.capture()); + + view.focus(); + + /* invoke the captured callback */ + onBackInvokedCallbackCaptor.getValue().onBackInvoked(); + + /* verify that the RemoteInputView goes away */ + assertEquals(view.getVisibility(), View.GONE); + } + + @Test public void testUiEventLogging_openAndSend() throws Exception { NotificationTestHelper helper = new NotificationTestHelper( mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt index b10aa125c69d..b68eb88d46db 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt @@ -63,8 +63,6 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { @Mock private lateinit var powerManager: PowerManager - private var shouldIgnoreViewRemoval: Boolean = false - @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -209,26 +207,6 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { verify(windowManager, never()).removeView(any()) } - @Test - fun removeView_shouldIgnoreRemovalFalse_viewRemoved() { - shouldIgnoreViewRemoval = false - underTest.displayView(getState()) - - underTest.removeView("reason") - - verify(windowManager).removeView(any()) - } - - @Test - fun removeView_shouldIgnoreRemovalTrue_viewNotRemoved() { - shouldIgnoreViewRemoval = true - underTest.displayView(getState()) - - underTest.removeView("reason") - - verify(windowManager, never()).removeView(any()) - } - private fun getState(name: String = "name") = ViewInfo(name) private fun getConfigurationListener(): ConfigurationListener { @@ -267,10 +245,6 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { mostRecentViewInfo = newInfo } - override fun shouldIgnoreViewRemoval(info: ViewInfo, removalReason: String): Boolean { - return shouldIgnoreViewRemoval - } - override fun getTouchableRegion(view: View, outRect: Rect) { outRect.setEmpty() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt index c9f2b4db81ef..13e9f608158e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt @@ -19,9 +19,9 @@ package com.android.systemui.temporarydisplay import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogcatEchoTracker import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt index 2af48021d099..9fbf159ec348 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -16,11 +16,8 @@ package com.android.systemui.temporarydisplay.chipbar -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import android.media.MediaRoute2Info import android.os.PowerManager +import android.os.VibrationEffect import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -31,19 +28,19 @@ import android.widget.ImageView import android.widget.TextView import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text import com.android.systemui.media.taptotransfer.common.MediaTttLogger -import com.android.systemui.media.taptotransfer.sender.ChipStateSender -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEvents import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat @@ -53,7 +50,6 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock -import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -64,523 +60,293 @@ import org.mockito.MockitoAnnotations class ChipbarCoordinatorTest : SysuiTestCase() { private lateinit var underTest: FakeChipbarCoordinator - @Mock - private lateinit var packageManager: PackageManager - @Mock - private lateinit var applicationInfo: ApplicationInfo - @Mock - private lateinit var logger: MediaTttLogger - @Mock - private lateinit var accessibilityManager: AccessibilityManager - @Mock - private lateinit var configurationController: ConfigurationController - @Mock - private lateinit var powerManager: PowerManager - @Mock - private lateinit var windowManager: WindowManager - @Mock - private lateinit var falsingManager: FalsingManager - @Mock - private lateinit var falsingCollector: FalsingCollector - @Mock - private lateinit var viewUtil: ViewUtil - private lateinit var fakeAppIconDrawable: Drawable + @Mock private lateinit var logger: MediaTttLogger + @Mock private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var powerManager: PowerManager + @Mock private lateinit var windowManager: WindowManager + @Mock private lateinit var falsingManager: FalsingManager + @Mock private lateinit var falsingCollector: FalsingCollector + @Mock private lateinit var viewUtil: ViewUtil + @Mock private lateinit var vibratorHelper: VibratorHelper private lateinit var fakeClock: FakeSystemClock private lateinit var fakeExecutor: FakeExecutor private lateinit var uiEventLoggerFake: UiEventLoggerFake - private lateinit var senderUiEventLogger: MediaTttSenderUiEventLogger @Before fun setUp() { MockitoAnnotations.initMocks(this) - - fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!! - whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME) - whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable) - whenever(packageManager.getApplicationInfo( - eq(PACKAGE_NAME), any<PackageManager.ApplicationInfoFlags>() - )).thenReturn(applicationInfo) - context.setMockPackageManager(packageManager) + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) uiEventLoggerFake = UiEventLoggerFake() - senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake) - - whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) - underTest = FakeChipbarCoordinator( - context, - logger, - windowManager, - fakeExecutor, - accessibilityManager, - configurationController, - powerManager, - senderUiEventLogger, - falsingManager, - falsingCollector, - viewUtil, - ) + underTest = + FakeChipbarCoordinator( + context, + logger, + windowManager, + fakeExecutor, + accessibilityManager, + configurationController, + powerManager, + falsingManager, + falsingCollector, + viewUtil, + vibratorHelper, + ) underTest.start() } @Test - fun almostCloseToStartCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() { - val state = almostCloseToStartCast() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun almostCloseToEndCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() { - val state = almostCloseToEndCast() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } + fun displayView_loadedIcon_correctlyRendered() { + val drawable = context.getDrawable(R.drawable.ic_celebration)!! - @Test - fun transferToReceiverTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() { - val state = transferToReceiverTriggered() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) + underTest.displayView( + ChipbarInfo( + Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), + Text.Loaded("text"), + endItem = null, + ) ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - @Test - fun transferToThisDeviceTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() { - val state = transferToThisDeviceTriggered() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) + val iconView = getChipbarView().getStartIconView() + assertThat(iconView.drawable).isEqualTo(drawable) + assertThat(iconView.contentDescription).isEqualTo("loadedCD") } @Test - fun transferToReceiverSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() { - val state = transferToReceiverSucceeded() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) + fun displayView_resourceIcon_correctlyRendered() { + val contentDescription = ContentDescription.Resource(R.string.controls_error_timeout) + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.drawable.ic_cake, contentDescription), + Text.Loaded("text"), + endItem = null, + ) ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToReceiverSucceeded_nullUndoRunnable_noUndo() { - underTest.displayView(transferToReceiverSucceeded(undoCallback = null)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToReceiverSucceeded_withUndoRunnable_undoWithClick() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue() - } - @Test - fun transferToReceiverSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() { - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isTrue() + val iconView = getChipbarView().getStartIconView() + assertThat(iconView.contentDescription) + .isEqualTo(contentDescription.loadContentDescription(context)) } @Test - fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() { - whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isFalse() - } - - @Test - fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() { - whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false) - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isTrue() - } - - @Test - fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - - getChipView().getUndoButton().performClick() - - assertThat(getChipView().getChipText()).isEqualTo( - transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id - ) - } - - @Test - fun transferToThisDeviceSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() { - val state = transferToThisDeviceSucceeded() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) + fun displayView_loadedText_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("display view text here"), + endItem = null, + ) ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToThisDeviceSucceeded_nullUndoRunnable_noUndo() { - underTest.displayView(transferToThisDeviceSucceeded(undoCallback = null)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - } - @Test - fun transferToThisDeviceSucceeded_withUndoRunnable_undoWithClick() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToThisDeviceSucceeded(undoCallback)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue() + assertThat(getChipbarView().getChipText()).isEqualTo("display view text here") } @Test - fun transferToThisDeviceSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() { - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToThisDeviceSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isTrue() - } - - @Test - fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToReceiverTriggered() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToThisDeviceSucceeded(undoCallback)) - - getChipView().getUndoButton().performClick() - - assertThat(getChipView().getChipText()).isEqualTo( - transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id + fun displayView_resourceText_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Resource(R.string.screenrecord_start_error), + endItem = null, + ) ) - } - @Test - fun transferToReceiverFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { - val state = transferToReceiverFailed() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(getChipView().getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(getChipbarView().getChipText()) + .isEqualTo(context.getString(R.string.screenrecord_start_error)) } @Test - fun transferToThisDeviceFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { - val state = transferToThisDeviceFailed() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(getChipView().getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) + fun displayView_endItemNull_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = null, + ) ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE) - } - - @Test - fun changeFromAlmostCloseToStartToTransferTriggered_loadingIconAppears() { - underTest.displayView(almostCloseToStartCast()) - underTest.displayView(transferToReceiverTriggered()) - - assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - } - - @Test - fun changeFromTransferTriggeredToTransferSucceeded_loadingIconDisappears() { - underTest.displayView(transferToReceiverTriggered()) - underTest.displayView(transferToReceiverSucceeded()) - assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE) + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } @Test - fun changeFromTransferTriggeredToTransferSucceeded_undoButtonAppears() { - underTest.displayView(transferToReceiverTriggered()) + fun displayView_endItemLoading_correctlyRendered() { underTest.displayView( - transferToReceiverSucceeded( - object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Loading, ) ) - assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE) + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } @Test - fun changeFromTransferSucceededToAlmostCloseToStart_undoButtonDisappears() { - underTest.displayView(transferToReceiverSucceeded()) - underTest.displayView(almostCloseToStartCast()) - - assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.GONE) - } - - @Test - fun changeFromTransferTriggeredToTransferFailed_failureIconAppears() { - underTest.displayView(transferToReceiverTriggered()) - underTest.displayView(transferToReceiverFailed()) + fun displayView_endItemError_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Error, + ) + ) - assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE) + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } @Test - fun transferToReceiverTriggeredThenRemoveView_viewStillDisplayed() { - underTest.displayView(transferToReceiverTriggered()) - fakeClock.advanceTime(1000L) - - underTest.removeView("fakeRemovalReason") - fakeExecutor.runAllReady() + fun displayView_endItemButton_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + onClickListener = {}, + ), + ) + ) - verify(windowManager, never()).removeView(any()) - verify(logger).logRemovalBypass(any(), any()) + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().text).isEqualTo("button text") + assertThat(chipbarView.getEndButton().hasOnClickListeners()).isTrue() } @Test - fun transferToReceiverTriggeredThenRemoveView_eventuallyTimesOut() { - underTest.displayView(transferToReceiverTriggered()) - - underTest.removeView("fakeRemovalReason") - fakeClock.advanceTime(TIMEOUT + 1L) - - verify(windowManager).removeView(any()) - } + fun displayView_endItemButtonClicked_falseTap_listenerNotRun() { + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) + var isClicked = false + val buttonClickListener = View.OnClickListener { isClicked = true } - @Test - fun transferToThisDeviceTriggeredThenRemoveView_viewStillDisplayed() { - underTest.displayView(transferToThisDeviceTriggered()) - fakeClock.advanceTime(1000L) + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + buttonClickListener, + ), + ) + ) - underTest.removeView("fakeRemovalReason") - fakeExecutor.runAllReady() + getChipbarView().getEndButton().performClick() - verify(windowManager, never()).removeView(any()) - verify(logger).logRemovalBypass(any(), any()) + assertThat(isClicked).isFalse() } @Test - fun transferToThisDeviceTriggeredThenRemoveView_eventuallyTimesOut() { - underTest.displayView(transferToThisDeviceTriggered()) - - underTest.removeView("fakeRemovalReason") - fakeClock.advanceTime(TIMEOUT + 1L) - - verify(windowManager).removeView(any()) - } + fun displayView_endItemButtonClicked_notFalseTap_listenerRun() { + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false) + var isClicked = false + val buttonClickListener = View.OnClickListener { isClicked = true } - @Test - fun transferToReceiverSucceededThenRemoveView_viewStillDisplayed() { - underTest.displayView(transferToReceiverSucceeded()) + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + buttonClickListener, + ), + ) + ) - underTest.removeView("fakeRemovalReason") - fakeExecutor.runAllReady() + getChipbarView().getEndButton().performClick() - verify(windowManager, never()).removeView(any()) - verify(logger).logRemovalBypass(any(), any()) + assertThat(isClicked).isTrue() } @Test - fun transferToReceiverSucceededThenRemoveView_eventuallyTimesOut() { - underTest.displayView(transferToReceiverSucceeded()) - - underTest.removeView("fakeRemovalReason") - fakeClock.advanceTime(TIMEOUT + 1L) + fun displayView_vibrationEffect_doubleClickEffect() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = null, + vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK), + ) + ) - verify(windowManager).removeView(any()) + verify(vibratorHelper).vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)) } @Test - fun transferToThisDeviceSucceededThenRemoveView_viewStillDisplayed() { - underTest.displayView(transferToThisDeviceSucceeded()) - - underTest.removeView("fakeRemovalReason") - fakeExecutor.runAllReady() - - verify(windowManager, never()).removeView(any()) - verify(logger).logRemovalBypass(any(), any()) - } + fun updateView_viewUpdated() { + // First, display a view + val drawable = context.getDrawable(R.drawable.ic_celebration)!! - @Test - fun transferToThisDeviceSucceededThenRemoveView_eventuallyTimesOut() { - underTest.displayView(transferToThisDeviceSucceeded()) + underTest.displayView( + ChipbarInfo( + Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), + Text.Loaded("title text"), + endItem = ChipbarEndItem.Loading, + ) + ) - underTest.removeView("fakeRemovalReason") - fakeClock.advanceTime(TIMEOUT + 1L) + val chipbarView = getChipbarView() + assertThat(chipbarView.getStartIconView().drawable).isEqualTo(drawable) + assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("loadedCD") + assertThat(chipbarView.getChipText()).isEqualTo("title text") + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + + // WHEN the view is updated + val newDrawable = context.getDrawable(R.drawable.ic_cake)!! + underTest.updateView( + ChipbarInfo( + Icon.Loaded(newDrawable, ContentDescription.Loaded("new CD")), + Text.Loaded("new title text"), + endItem = ChipbarEndItem.Error, + ), + chipbarView + ) - verify(windowManager).removeView(any()) + // THEN we display the new view + assertThat(chipbarView.getStartIconView().drawable).isEqualTo(newDrawable) + assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("new CD") + assertThat(chipbarView.getChipText()).isEqualTo("new title text") + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } - private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon) + private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon) private fun ViewGroup.getChipText(): String = (this.requireViewById<TextView>(R.id.text)).text as String - private fun ViewGroup.getLoadingIconVisibility(): Int = - this.requireViewById<View>(R.id.loading).visibility + private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading) - private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.undo) + private fun ViewGroup.getEndButton(): TextView = this.requireViewById(R.id.end_button) - private fun ViewGroup.getFailureIcon(): View = this.requireViewById(R.id.failure_icon) + private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error) - private fun getChipView(): ViewGroup { + private fun getChipbarView(): ViewGroup { val viewCaptor = ArgumentCaptor.forClass(View::class.java) verify(windowManager).addView(viewCaptor.capture(), any()) return viewCaptor.value as ViewGroup } - - // TODO(b/245610654): For now, the below methods are duplicated between this test and - // [MediaTttSenderCoordinatorTest]. Once we define a generic API for [ChipbarCoordinator], - // these will no longer be duplicated. - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToStartCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToEndCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) } -private const val APP_NAME = "Fake app name" -private const val OTHER_DEVICE_NAME = "My Tablet" -private const val PACKAGE_NAME = "com.android.systemui" private const val TIMEOUT = 10000 - -private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) - .addFeature("feature") - .setClientPackageName(PACKAGE_NAME) - .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt index 10704ac8fc67..17d402319246 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt @@ -24,8 +24,8 @@ import android.view.accessibility.AccessibilityManager import com.android.systemui.classifier.FalsingCollector import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.view.ViewUtil @@ -39,10 +39,10 @@ class FakeChipbarCoordinator( accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, powerManager: PowerManager, - uiEventLogger: MediaTttSenderUiEventLogger, falsingManager: FalsingManager, falsingCollector: FalsingCollector, viewUtil: ViewUtil, + vibratorHelper: VibratorHelper, ) : ChipbarCoordinator( context, @@ -52,10 +52,10 @@ class FakeChipbarCoordinator( accessibilityManager, configurationController, powerManager, - uiEventLogger, falsingManager, falsingCollector, viewUtil, + vibratorHelper, ) { override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { // Just bypass the animation in tests diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt index d951f366c595..525d8371c9ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt @@ -110,7 +110,7 @@ class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() { val thirdExpectedValue = setUpUsers( count = 2, - hasGuest = true, + isLastGuestUser = true, selectedIndex = 1, ) underTest.refreshUsers() @@ -121,21 +121,25 @@ class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() { } @Test - fun `refreshUsers - sorts by creation time`() = runSelfCancelingTest { + fun `refreshUsers - sorts by creation time - guest user last`() = runSelfCancelingTest { underTest = create(this) val unsortedUsers = setUpUsers( count = 3, selectedIndex = 0, + isLastGuestUser = true, + ) + unsortedUsers[0].creationTime = 999 + unsortedUsers[1].creationTime = 900 + unsortedUsers[2].creationTime = 950 + val expectedUsers = + listOf( + unsortedUsers[1], + unsortedUsers[0], + unsortedUsers[2], // last because this is the guest ) - unsortedUsers[0].creationTime = 900 - unsortedUsers[1].creationTime = 700 - unsortedUsers[2].creationTime = 999 - val expectedUsers = listOf(unsortedUsers[1], unsortedUsers[0], unsortedUsers[2]) var userInfos: List<UserInfo>? = null - var selectedUserInfo: UserInfo? = null underTest.userInfos.onEach { userInfos = it }.launchIn(this) - underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) underTest.refreshUsers() assertThat(userInfos).isEqualTo(expectedUsers) @@ -143,14 +147,14 @@ class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() { private fun setUpUsers( count: Int, - hasGuest: Boolean = false, + isLastGuestUser: Boolean = false, selectedIndex: Int = 0, ): List<UserInfo> { val userInfos = (0 until count).map { index -> createUserInfo( index, - isGuest = hasGuest && index == count - 1, + isGuest = isLastGuestUser && index == count - 1, ) } whenever(manager.aliveUsers).thenReturn(userInfos) diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt index d4b41c18e123..a363a037c499 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt @@ -97,6 +97,7 @@ class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { createUserRecord(2), createActionRecord(UserActionModel.ADD_SUPERVISED_USER), createActionRecord(UserActionModel.ENTER_GUEST_MODE), + createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT), ) ) var models: List<UserModel>? = null @@ -176,15 +177,17 @@ class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { createUserRecord(2), createActionRecord(UserActionModel.ADD_SUPERVISED_USER), createActionRecord(UserActionModel.ENTER_GUEST_MODE), + createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT), ) ) var models: List<UserActionModel>? = null val job = underTest.actions.onEach { models = it }.launchIn(this) - assertThat(models).hasSize(3) + assertThat(models).hasSize(4) assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) + assertThat(models?.get(3)).isEqualTo(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) job.cancel() } @@ -200,6 +203,7 @@ class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { isAddUser = action == UserActionModel.ADD_USER, isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, isGuest = action == UserActionModel.ENTER_GUEST_MODE, + isManageUsers = action == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt index 1540f8552002..97571b23be56 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt @@ -28,6 +28,7 @@ import androidx.test.filters.SmallTest import com.android.internal.R.drawable.ic_account_circle import com.android.systemui.R import com.android.systemui.common.shared.model.Text +import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.source.UserRecord import com.android.systemui.user.domain.model.ShowDialogRequestModel @@ -316,14 +317,16 @@ class UserInteractorRefactoredTest : UserInteractorTest() { keyguardRepository.setKeyguardShowing(false) var dialogRequest: ShowDialogRequestModel? = null val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + val dialogShower: UserSwitchDialogController.DialogShower = mock() - underTest.executeAction(UserActionModel.ADD_USER) + underTest.executeAction(UserActionModel.ADD_USER, dialogShower) assertThat(dialogRequest) .isEqualTo( ShowDialogRequestModel.ShowAddUserDialog( userHandle = userInfos[0].userHandle, isKeyguardShowing = false, showEphemeralMessage = false, + dialogShower = dialogShower, ) ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt index c3a9705bf6ba..6a17c8ddc63d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt @@ -64,13 +64,7 @@ open class UserInteractorUnrefactoredTest : UserInteractorTest() { @Test fun `actions - not actionable when locked and not locked`() = runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) + setActions() userRepository.setActionableWhenLocked(false) keyguardRepository.setKeyguardShowing(false) @@ -92,13 +86,7 @@ open class UserInteractorUnrefactoredTest : UserInteractorTest() { @Test fun `actions - actionable when locked and not locked`() = runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) + setActions() userRepository.setActionableWhenLocked(true) keyguardRepository.setKeyguardShowing(false) @@ -120,13 +108,7 @@ open class UserInteractorUnrefactoredTest : UserInteractorTest() { @Test fun `actions - actionable when locked and locked`() = runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) + setActions() userRepository.setActionableWhenLocked(true) keyguardRepository.setKeyguardShowing(true) @@ -182,6 +164,10 @@ open class UserInteractorUnrefactoredTest : UserInteractorTest() { verify(activityStarter).startActivity(any(), anyBoolean()) } + private fun setActions() { + userRepository.setActions(UserActionModel.values().toList()) + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index 0344e3f991e2..c12a868dbaed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -268,6 +268,26 @@ class UserSwitcherViewModelTest : SysuiTestCase() { } @Test + fun `menu actions`() = + runBlocking(IMMEDIATE) { + userRepository.setActions(UserActionModel.values().toList()) + var actions: List<UserActionViewModel>? = null + val job = underTest.menu.onEach { actions = it }.launchIn(this) + + assertThat(actions?.map { it.viewKey }) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(), + UserActionModel.ADD_USER.ordinal.toLong(), + UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(), + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(), + ) + ) + + job.cancel() + } + + @Test fun `isFinishRequested - finishes when user is switched`() = runBlocking(IMMEDIATE) { setUsers(count = 2) diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt deleted file mode 100644 index 5e09b81da4e8..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.util.collection - -import android.testing.AndroidTestingRunner -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertSame -import org.junit.Assert.assertThrows -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.MockitoAnnotations - -@SmallTest -@RunWith(AndroidTestingRunner::class) -class RingBufferTest : SysuiTestCase() { - - private val buffer = RingBuffer(5) { TestElement() } - - private val history = mutableListOf<TestElement>() - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - } - - @Test - fun testBarelyFillBuffer() { - fillBuffer(5) - - assertEquals(0, buffer[0].id) - assertEquals(1, buffer[1].id) - assertEquals(2, buffer[2].id) - assertEquals(3, buffer[3].id) - assertEquals(4, buffer[4].id) - } - - @Test - fun testPartiallyFillBuffer() { - fillBuffer(3) - - assertEquals(3, buffer.size) - - assertEquals(0, buffer[0].id) - assertEquals(1, buffer[1].id) - assertEquals(2, buffer[2].id) - - assertThrows(IndexOutOfBoundsException::class.java) { buffer[3] } - assertThrows(IndexOutOfBoundsException::class.java) { buffer[4] } - } - - @Test - fun testSpinBuffer() { - fillBuffer(277) - - assertEquals(272, buffer[0].id) - assertEquals(273, buffer[1].id) - assertEquals(274, buffer[2].id) - assertEquals(275, buffer[3].id) - assertEquals(276, buffer[4].id) - assertThrows(IndexOutOfBoundsException::class.java) { buffer[5] } - - assertEquals(5, buffer.size) - } - - @Test - fun testElementsAreRecycled() { - fillBuffer(23) - - assertSame(history[4], buffer[1]) - assertSame(history[9], buffer[1]) - assertSame(history[14], buffer[1]) - assertSame(history[19], buffer[1]) - } - - @Test - fun testIterator() { - fillBuffer(13) - - val iterator = buffer.iterator() - - for (i in 0 until 5) { - assertEquals(history[8 + i], iterator.next()) - } - assertFalse(iterator.hasNext()) - assertThrows(NoSuchElementException::class.java) { iterator.next() } - } - - @Test - fun testForEach() { - fillBuffer(13) - var i = 8 - - buffer.forEach { - assertEquals(history[i], it) - i++ - } - assertEquals(13, i) - } - - private fun fillBuffer(count: Int) { - for (i in 0 until count) { - val elem = buffer.advance() - elem.id = history.size - history.add(elem) - } - } -} - -private class TestElement(var id: Int = 0) { - override fun toString(): String { - return "{TestElement $id}" - } -}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 09da52e7685c..fa7ebf6a2449 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -80,6 +80,7 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; +import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; @@ -91,6 +92,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.RankingBuilder; @@ -190,6 +192,8 @@ public class BubblesTest extends SysuiTestCase { private NotificationShadeWindowView mNotificationShadeWindowView; @Mock private AuthController mAuthController; + @Mock + private ShadeExpansionStateManager mShadeExpansionStateManager; private SysUiState mSysUiState; private boolean mSysUiStateBubblesExpanded; @@ -290,7 +294,7 @@ public class BubblesTest extends SysuiTestCase { mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController, mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController, mColorExtractor, mDumpManager, mKeyguardStateController, - mScreenOffAnimationController, mAuthController); + mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager); mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView); mNotificationShadeWindowController.attach(); @@ -343,7 +347,8 @@ public class BubblesTest extends SysuiTestCase { mock(NotificationInterruptLogger.class), mock(Handler.class), mock(NotifPipelineFlags.class), - mock(KeyguardNotificationVisibilityProvider.class) + mock(KeyguardNotificationVisibilityProvider.class), + mock(UiEventLogger.class) ); when(mShellTaskOrganizer.getExecutor()).thenReturn(syncExecutor); mBubbleController = new TestableBubbleController( diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java index 9635faf6e858..e5316bc83a12 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java @@ -22,6 +22,7 @@ import android.os.Handler; import android.os.PowerManager; import android.service.dreams.IDreamManager; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider; @@ -46,7 +47,8 @@ public class TestableNotificationInterruptStateProviderImpl NotificationInterruptLogger logger, Handler mainHandler, NotifPipelineFlags flags, - KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) { + KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider, + UiEventLogger uiEventLogger) { super(contentResolver, powerManager, dreamManager, @@ -58,7 +60,8 @@ public class TestableNotificationInterruptStateProviderImpl logger, mainHandler, flags, - keyguardNotificationVisibilityProvider); + keyguardNotificationVisibilityProvider, + uiEventLogger); mUseHeadsUp = true; } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java index cebe946a459c..7af66f641837 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java @@ -34,6 +34,8 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.tracing.ProtoTracer; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.desktopmode.DesktopMode; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.floating.FloatingTasks; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEventCallback; @@ -49,6 +51,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Optional; +import java.util.concurrent.Executor; /** * Tests for {@link WMShell}. @@ -76,12 +79,14 @@ public class WMShellTest extends SysuiTestCase { @Mock UserTracker mUserTracker; @Mock ShellExecutor mSysUiMainExecutor; @Mock FloatingTasks mFloatingTasks; + @Mock DesktopMode mDesktopMode; @Before public void setUp() { MockitoAnnotations.initMocks(this); mWMShell = new WMShell(mContext, mShellInterface, Optional.of(mPip), Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mFloatingTasks), + Optional.of(mDesktopMode), mCommandQueue, mConfigurationController, mKeyguardStateController, mKeyguardUpdateMonitor, mScreenLifecycle, mSysUiState, mProtoTracer, mWakefulnessLifecycle, mUserTracker, mSysUiMainExecutor); @@ -103,4 +108,12 @@ public class WMShellTest extends SysuiTestCase { verify(mOneHanded).registerTransitionCallback(any(OneHandedTransitionCallback.class)); verify(mOneHanded).registerEventCallback(any(OneHandedEventCallback.class)); } + + @Test + public void initDesktopMode_registersListener() { + mWMShell.initDesktopMode(mDesktopMode); + verify(mDesktopMode).addListener( + any(DesktopModeTaskRepository.VisibleTasksListener.class), + any(Executor.class)); + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt index 5d52be2675e3..a60b7735fbd4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt @@ -26,7 +26,7 @@ class FakeFeatureFlags : FeatureFlags { private val listenerFlagIds = mutableMapOf<FlagListenable.Listener, MutableSet<Int>>() init { - Flags.getFlagFields().forEach { field -> + Flags.flagFields.forEach { field -> val flag: Flag<*> = field.get(null) as Flag<*> knownFlagNames[flag.id] = field.name } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 725b1f41372c..0c126805fb78 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.common.shared.model.Position +import com.android.systemui.keyguard.shared.model.StatusBarState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -44,6 +45,9 @@ class FakeKeyguardRepository : KeyguardRepository { private val _dozeAmount = MutableStateFlow(0f) override val dozeAmount: Flow<Float> = _dozeAmount + private val _statusBarState = MutableStateFlow(StatusBarState.SHADE) + override val statusBarState: Flow<StatusBarState> = _statusBarState + override fun isKeyguardShowing(): Boolean { return _isKeyguardShowing.value } 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 index 527258579372..c33ce5d9484d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt @@ -16,7 +16,7 @@ package com.android.systemui.qs -import android.view.View +import com.android.systemui.animation.Expandable import com.android.systemui.qs.FgsManagerController.OnDialogDismissedListener import com.android.systemui.qs.FgsManagerController.OnNumberOfPackagesChangedListener import kotlinx.coroutines.flow.MutableStateFlow @@ -54,7 +54,7 @@ class FakeFgsManagerController( override fun init() {} - override fun showDialog(viewLaunchedFrom: View?) {} + override fun showDialog(expandable: Expandable?) {} override fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) { numRunningPackagesListeners.add(listener) 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 index 2a9aeddc9aa8..325da4ead666 100644 --- 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 @@ -57,7 +57,6 @@ 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 @@ -68,7 +67,6 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher 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) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt index 8d171be87cb6..69575a987e5d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt @@ -26,7 +26,9 @@ package com.android.systemui.util.mockito import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatcher import org.mockito.Mockito +import org.mockito.Mockito.`when` import org.mockito.stubbing.OngoingStubbing +import org.mockito.stubbing.Stubber /** * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when @@ -89,7 +91,8 @@ inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T: * * @see Mockito.when */ -fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall) +fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall) +fun <T> Stubber.whenever(mock: T): T = `when`(mock) /** * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index 38237fa8eabd..23cacf3c54cf 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -866,6 +866,33 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } @Override + public void setAppWidgetHidden(String callingPackage, int hostId) { + final int userId = UserHandle.getCallingUserId(); + + if (DEBUG) { + Slog.i(TAG, "setAppWidgetHidden() " + userId); + } + + mSecurityPolicy.enforceCallFromPackage(callingPackage); + + synchronized (mLock) { + ensureGroupStateLoadedLocked(userId, /* enforceUserUnlockingOrUnlocked */false); + + HostId id = new HostId(Binder.getCallingUid(), hostId, callingPackage); + Host host = lookupHostLocked(id); + + if (host != null) { + try { + mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false); + } catch (NullPointerException e) { + Slog.e(TAG, "setAppWidgetHidden(): Getting host uids: " + host.toString(), e); + throw e; + } + } + } + } + + @Override public void deleteAppWidgetId(String callingPackage, int appWidgetId) { final int userId = UserHandle.getCallingUserId(); diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index 8d878af4b788..fcc1ef2a3882 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -152,6 +152,8 @@ final class UiModeManagerService extends SystemService { // flag set by resource, whether to start dream immediately upon docking even if unlocked. private boolean mStartDreamImmediatelyOnDock = true; + // flag set by resource, whether to disable dreams when ambient mode suppression is enabled. + private boolean mDreamsDisabledByAmbientModeSuppression = false; // flag set by resource, whether to enable Car dock launch when starting car mode. private boolean mEnableCarDockLaunch = true; // flag set by resource, whether to lock UI mode to the default one or not. @@ -364,6 +366,11 @@ final class UiModeManagerService extends SystemService { mStartDreamImmediatelyOnDock = startDreamImmediatelyOnDock; } + @VisibleForTesting + void setDreamsDisabledByAmbientModeSuppression(boolean disabledByAmbientModeSuppression) { + mDreamsDisabledByAmbientModeSuppression = disabledByAmbientModeSuppression; + } + @Override public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) { mCurrentUser = to.getUserIdentifier(); @@ -424,6 +431,8 @@ final class UiModeManagerService extends SystemService { final Resources res = context.getResources(); mStartDreamImmediatelyOnDock = res.getBoolean( com.android.internal.R.bool.config_startDreamImmediatelyOnDock); + mDreamsDisabledByAmbientModeSuppression = res.getBoolean( + com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig); mNightMode = res.getInteger( com.android.internal.R.integer.config_defaultNightMode); mDefaultUiModeType = res.getInteger( @@ -1827,10 +1836,14 @@ final class UiModeManagerService extends SystemService { // Send the new configuration. applyConfigurationExternallyLocked(); + final boolean dreamsSuppressed = mDreamsDisabledByAmbientModeSuppression + && mLocalPowerManager.isAmbientDisplaySuppressed(); + // If we did not start a dock app, then start dreaming if appropriate. - if (category != null && !dockAppStarted && (mStartDreamImmediatelyOnDock - || mWindowManager.isKeyguardShowingAndNotOccluded() - || !mPowerManager.isInteractive())) { + if (category != null && !dockAppStarted && !dreamsSuppressed && ( + mStartDreamImmediatelyOnDock + || mWindowManager.isKeyguardShowingAndNotOccluded() + || !mPowerManager.isInteractive())) { mInjector.startDreamWhenDockedIfAppropriate(getContext()); } } diff --git a/services/core/java/com/android/server/biometrics/log/ALSProbe.java b/services/core/java/com/android/server/biometrics/log/ALSProbe.java index 1a5f31c8ac90..da4361843681 100644 --- a/services/core/java/com/android/server/biometrics/log/ALSProbe.java +++ b/services/core/java/com/android/server/biometrics/log/ALSProbe.java @@ -52,16 +52,13 @@ final class ALSProbe implements Probe { private boolean mDestroyed = false; private boolean mDestroyRequested = false; private boolean mDisableRequested = false; - private volatile NextConsumer mNextConsumer = null; + private NextConsumer mNextConsumer = null; private volatile float mLastAmbientLux = -1; private final SensorEventListener mLightSensorListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { - mLastAmbientLux = event.values[0]; - if (mNextConsumer != null) { - completeNextConsumer(mLastAmbientLux); - } + onNext(event.values[0]); } @Override @@ -133,11 +130,29 @@ final class ALSProbe implements Probe { // if a final consumer is set it will call destroy/disable on the next value if requested if (!mDestroyed && mNextConsumer == null) { - disable(); + disableLightSensorLoggingLocked(); mDestroyed = true; } } + private synchronized void onNext(float value) { + mLastAmbientLux = value; + + final NextConsumer consumer = mNextConsumer; + mNextConsumer = null; + if (consumer != null) { + Slog.v(TAG, "Finishing next consumer"); + + if (mDestroyRequested) { + destroy(); + } else if (mDisableRequested) { + disable(); + } + + consumer.consume(value); + } + } + /** The most recent lux reading. */ public float getMostRecentLux() { return mLastAmbientLux; @@ -160,7 +175,7 @@ final class ALSProbe implements Probe { @Nullable Handler handler) { final NextConsumer nextConsumer = new NextConsumer(consumer, handler); final float current = mLastAmbientLux; - if (current > 0) { + if (current > -1f) { nextConsumer.consume(current); } else if (mDestroyed) { nextConsumer.consume(-1f); @@ -172,23 +187,6 @@ final class ALSProbe implements Probe { } } - private synchronized void completeNextConsumer(float value) { - Slog.v(TAG, "Finishing next consumer"); - - final NextConsumer consumer = mNextConsumer; - mNextConsumer = null; - - if (mDestroyRequested) { - destroy(); - } else if (mDisableRequested) { - disable(); - } - - if (consumer != null) { - consumer.consume(value); - } - } - private void enableLightSensorLoggingLocked() { if (!mEnabled) { mEnabled = true; @@ -219,9 +217,13 @@ final class ALSProbe implements Probe { } } - private void onTimeout() { + private synchronized void onTimeout() { Slog.e(TAG, "Max time exceeded for ALS logger - disabling: " + mLightSensorListener.hashCode()); + + // if consumers are waiting but there was no sensor change, complete them with the latest + // value before disabling + onNext(mLastAmbientLux); disable(); } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index fa751007198e..a778b573f225 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -16,6 +16,7 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; +import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR; import android.annotation.NonNull; @@ -57,6 +58,7 @@ import com.android.server.biometrics.sensors.SensorOverlays; import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler; import com.android.server.biometrics.sensors.fingerprint.Udfps; +import java.time.Clock; import java.util.ArrayList; import java.util.function.Supplier; @@ -88,7 +90,9 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> private long mWaitForAuthKeyguard; private long mWaitForAuthBp; private long mIgnoreAuthFor; + private long mSideFpsLastAcquireStartTime; private Runnable mAuthSuccessRunnable; + private final Clock mClock; FingerprintAuthenticationClient( @NonNull Context context, @@ -112,7 +116,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> @Nullable ISidefpsController sidefpsController, boolean allowBackgroundAuthentication, @NonNull FingerprintSensorPropertiesInternal sensorProps, - @NonNull Handler handler) { + @NonNull Handler handler, + @NonNull Clock clock) { super( context, lazyDaemon, @@ -154,6 +159,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> mSkipWaitForPowerVendorAcquireMessage = context.getResources().getInteger( R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage); + mSideFpsLastAcquireStartTime = -1; + mClock = clock; if (mSensorProps.isAnySidefpsType()) { if (Build.isDebuggable()) { @@ -235,8 +242,14 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> return; } delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp; - Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps waiting for power for: " - + delay + "ms"); + + if (mSideFpsLastAcquireStartTime != -1) { + delay = Math.max(0, + delay - (mClock.millis() - mSideFpsLastAcquireStartTime)); + } + + Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps " + + "waiting for power until: " + delay + "ms"); } if (mHandler.hasMessages(MESSAGE_FINGER_UP)) { @@ -260,13 +273,15 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo)); super.onAcquired(acquiredInfo, vendorCode); if (mSensorProps.isAnySidefpsType()) { + if (acquiredInfo == FINGERPRINT_ACQUIRED_START) { + mSideFpsLastAcquireStartTime = mClock.millis(); + } final boolean shouldLookForVendor = mSkipWaitForPowerAcquireMessage == FINGERPRINT_ACQUIRED_VENDOR; final boolean acquireMessageMatch = acquiredInfo == mSkipWaitForPowerAcquireMessage; final boolean vendorMessageMatch = vendorCode == mSkipWaitForPowerVendorAcquireMessage; final boolean ignorePowerPress = - (acquireMessageMatch && !shouldLookForVendor) || (shouldLookForVendor - && acquireMessageMatch && vendorMessageMatch); + acquireMessageMatch && (!shouldLookForVendor || vendorMessageMatch); if (ignorePowerPress) { Slog.d(TAG, "(sideFPS) onFingerUp"); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 6f6c09b91a66..dabb2cf5819a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -47,6 +47,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; import android.os.UserManager; import android.util.Slog; import android.util.SparseArray; @@ -447,7 +448,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mBiometricContext, isStrongBiometric, mTaskStackListener, mSensors.get(sensorId).getLockoutCache(), mUdfpsOverlayController, mSidefpsController, allowBackgroundAuthentication, - mSensors.get(sensorId).getSensorProperties(), mHandler); + mSensors.get(sensorId).getSensorProperties(), mHandler, + SystemClock.elapsedRealtimeClock()); scheduleForSensor(sensorId, client, mBiometricStateCallback); }); } diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java index 17215e5ae4ad..8f59ffd30bba 100644 --- a/services/core/java/com/android/server/display/BrightnessTracker.java +++ b/services/core/java/com/android/server/display/BrightnessTracker.java @@ -220,6 +220,11 @@ public class BrightnessTracker { } private void backgroundStart(float initialBrightness) { + synchronized (mDataCollectionLock) { + if (mStarted) { + return; + } + } if (DEBUG) { Slog.d(TAG, "Background start"); } @@ -250,6 +255,11 @@ public class BrightnessTracker { /** Stop listening for events */ void stop() { + synchronized (mDataCollectionLock) { + if (!mStarted) { + return; + } + } if (DEBUG) { Slog.d(TAG, "Stop"); } diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 1ae37bb5d66b..687d03d4f774 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -150,7 +150,7 @@ import javax.xml.datatype.DatatypeConfigurationException; * <quirk>canSetBrightnessViaHwc</quirk> * </quirks> * - * <autoBrightness> + * <autoBrightness enable="true"> * <brighteningLightDebounceMillis> * 2000 * </brighteningLightDebounceMillis> @@ -507,6 +507,11 @@ public class DisplayDeviceConfig { private long mAutoBrightnessDarkeningLightDebounce = INVALID_AUTO_BRIGHTNESS_LIGHT_DEBOUNCE; + // This setting allows non-default displays to have autobrightness enabled. + private boolean mAutoBrightnessAvailable = false; + // This stores the raw value loaded from the config file - true if not written. + private boolean mDdcAutoBrightnessAvailable = true; + // Brightness Throttling data may be updated via the DeviceConfig. Here we store the original // data, which comes from the ddc, and the current one, which may be the DeviceConfig // overwritten value. @@ -1119,6 +1124,10 @@ public class DisplayDeviceConfig { return mProximitySensor; } + boolean isAutoBrightnessAvailable() { + return mAutoBrightnessAvailable; + } + /** * @param quirkValue The quirk to test. * @return {@code true} if the specified quirk is present in this configuration, {@code false} @@ -1184,6 +1193,60 @@ public class DisplayDeviceConfig { return mBrightnessLevelsNits; } + /** + * @return Default peak refresh rate of the associated display + */ + public int getDefaultPeakRefreshRate() { + return mContext.getResources().getInteger(R.integer.config_defaultPeakRefreshRate); + } + + /** + * @return Default refresh rate of the associated display + */ + public int getDefaultRefreshRate() { + return mContext.getResources().getInteger(R.integer.config_defaultRefreshRate); + } + + /** + * @return An array of lower display brightness thresholds. This, in combination with lower + * ambient brightness thresholds help define buckets in which the refresh rate switching is not + * allowed + */ + public int[] getLowDisplayBrightnessThresholds() { + return mContext.getResources().getIntArray( + R.array.config_brightnessThresholdsOfPeakRefreshRate); + } + + /** + * @return An array of lower ambient brightness thresholds. This, in combination with lower + * display brightness thresholds help define buckets in which the refresh rate switching is not + * allowed + */ + public int[] getLowAmbientBrightnessThresholds() { + return mContext.getResources().getIntArray( + R.array.config_ambientThresholdsOfPeakRefreshRate); + } + + /** + * @return An array of high display brightness thresholds. This, in combination with high + * ambient brightness thresholds help define buckets in which the refresh rate switching is not + * allowed + */ + public int[] getHighDisplayBrightnessThresholds() { + return mContext.getResources().getIntArray( + R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate); + } + + /** + * @return An array of high ambient brightness thresholds. This, in combination with high + * display brightness thresholds help define buckets in which the refresh rate switching is not + * allowed + */ + public int[] getHighAmbientBrightnessThresholds() { + return mContext.getResources().getIntArray( + R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate); + } + @Override public String toString() { return "DisplayDeviceConfig{" @@ -1271,6 +1334,8 @@ public class DisplayDeviceConfig { + mAutoBrightnessDarkeningLightDebounce + ", mBrightnessLevelsLux= " + Arrays.toString(mBrightnessLevelsLux) + ", mBrightnessLevelsNits= " + Arrays.toString(mBrightnessLevelsNits) + + ", mDdcAutoBrightnessAvailable= " + mDdcAutoBrightnessAvailable + + ", mAutoBrightnessAvailable= " + mAutoBrightnessAvailable + "}"; } @@ -1349,6 +1414,7 @@ public class DisplayDeviceConfig { loadBrightnessChangeThresholdsFromXml(); setProxSensorUnspecified(); loadAutoBrightnessConfigsFromConfigXml(); + loadAutoBrightnessAvailableFromConfigXml(); mLoadedFrom = "<config.xml>"; } @@ -1367,6 +1433,7 @@ public class DisplayDeviceConfig { setSimpleMappingStrategyValues(); loadAmbientLightSensorFromConfigXml(); setProxSensorUnspecified(); + loadAutoBrightnessAvailableFromConfigXml(); } private void copyUninitializedValuesFromSecondaryConfig(DisplayConfiguration defaultConfig) { @@ -1559,9 +1626,11 @@ public class DisplayDeviceConfig { } private void loadAutoBrightnessConfigValues(DisplayConfiguration config) { - loadAutoBrightnessBrighteningLightDebounce(config.getAutoBrightness()); - loadAutoBrightnessDarkeningLightDebounce(config.getAutoBrightness()); - loadAutoBrightnessDisplayBrightnessMapping(config.getAutoBrightness()); + final AutoBrightness autoBrightness = config.getAutoBrightness(); + loadAutoBrightnessBrighteningLightDebounce(autoBrightness); + loadAutoBrightnessDarkeningLightDebounce(autoBrightness); + loadAutoBrightnessDisplayBrightnessMapping(autoBrightness); + loadEnableAutoBrightness(autoBrightness); } /** @@ -1623,6 +1692,11 @@ public class DisplayDeviceConfig { } } + private void loadAutoBrightnessAvailableFromConfigXml() { + mAutoBrightnessAvailable = mContext.getResources().getBoolean( + R.bool.config_automatic_brightness_available); + } + private void loadBrightnessMapFromConfigXml() { // Use the config.xml mapping final Resources res = mContext.getResources(); @@ -2263,6 +2337,20 @@ public class DisplayDeviceConfig { return levels; } + private void loadEnableAutoBrightness(AutoBrightness autobrightness) { + // mDdcAutoBrightnessAvailable is initialised to true, so that we fallback to using the + // config.xml values if the autobrightness tag is not defined in the ddc file. + // Autobrightness can still be turned off globally via config_automatic_brightness_available + mDdcAutoBrightnessAvailable = true; + if (autobrightness != null) { + mDdcAutoBrightnessAvailable = autobrightness.getEnabled(); + } + + mAutoBrightnessAvailable = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_automatic_brightness_available) + && mDdcAutoBrightnessAvailable; + } + static class SensorData { public String type; public String name; diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index b153c1ba98e0..b8ff6ed93666 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1519,6 +1519,7 @@ public final class DisplayManagerService extends SystemService { display.setUserDisabledHdrTypes(mUserDisabledHdrTypes); } if (isDefault) { + notifyDefaultDisplayDeviceUpdated(display); recordStableDisplayStatsIfNeededLocked(display); recordTopInsetLocked(display); } @@ -1612,6 +1613,11 @@ public final class DisplayManagerService extends SystemService { mHandler.post(work); } final int displayId = display.getDisplayIdLocked(); + + if (displayId == Display.DEFAULT_DISPLAY) { + notifyDefaultDisplayDeviceUpdated(display); + } + DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); if (dpc != null) { dpc.onDisplayChanged(); @@ -1621,6 +1627,11 @@ public final class DisplayManagerService extends SystemService { handleLogicalDisplayChangedLocked(display); } + private void notifyDefaultDisplayDeviceUpdated(LogicalDisplay display) { + mDisplayModeDirector.defaultDisplayDeviceUpdated(display.getPrimaryDisplayDeviceLocked() + .mDisplayDeviceConfig); + } + private void handleLogicalDisplayDeviceStateTransitionLocked(@NonNull LogicalDisplay display) { final int displayId = display.getDisplayIdLocked(); final DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); @@ -1759,9 +1770,13 @@ public final class DisplayManagerService extends SystemService { if (displayDevice == null) { return; } - mPersistentDataStore.setUserPreferredResolution( - displayDevice, resolutionWidth, resolutionHeight); - mPersistentDataStore.setUserPreferredRefreshRate(displayDevice, refreshRate); + try { + mPersistentDataStore.setUserPreferredResolution( + displayDevice, resolutionWidth, resolutionHeight); + mPersistentDataStore.setUserPreferredRefreshRate(displayDevice, refreshRate); + } finally { + mPersistentDataStore.saveIfNeeded(); + } } private void setUserPreferredModeForDisplayLocked(int displayId, Display.Mode mode) { diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java index ac72b1725432..912b1b2e8da2 100644 --- a/services/core/java/com/android/server/display/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/DisplayModeDirector.java @@ -79,6 +79,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.concurrent.Callable; /** * The DisplayModeDirector is responsible for determining what modes are allowed to be automatically @@ -153,8 +154,10 @@ public class DisplayModeDirector { mSupportedModesByDisplay = new SparseArray<>(); mDefaultModeByDisplay = new SparseArray<>(); mAppRequestObserver = new AppRequestObserver(); - mSettingsObserver = new SettingsObserver(context, handler); mDisplayObserver = new DisplayObserver(context, handler); + mDeviceConfig = injector.getDeviceConfig(); + mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings(); + mSettingsObserver = new SettingsObserver(context, handler); mBrightnessObserver = new BrightnessObserver(context, handler, injector); mUdfpsObserver = new UdfpsObserver(); final BallotBox ballotBox = (displayId, priority, vote) -> { @@ -164,10 +167,8 @@ public class DisplayModeDirector { }; mSensorObserver = new SensorObserver(context, ballotBox, injector); mSkinThermalStatusObserver = new SkinThermalStatusObserver(injector, ballotBox); - mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings(); mHbmObserver = new HbmObserver(injector, ballotBox, BackgroundThread.getHandler(), mDeviceConfigDisplaySettings); - mDeviceConfig = injector.getDeviceConfig(); mAlwaysRespectAppRequest = false; } @@ -518,6 +519,15 @@ public class DisplayModeDirector { } /** + * A utility to make this class aware of the new display configs whenever the default display is + * changed + */ + public void defaultDisplayDeviceUpdated(DisplayDeviceConfig displayDeviceConfig) { + mSettingsObserver.setRefreshRates(displayDeviceConfig); + mBrightnessObserver.updateBlockingZoneThresholds(displayDeviceConfig); + } + + /** * When enabled the app requested display mode is always selected and all * other votes will be ignored. This is used for testing purposes. */ @@ -1132,10 +1142,19 @@ public class DisplayModeDirector { SettingsObserver(@NonNull Context context, @NonNull Handler handler) { super(handler); mContext = context; - mDefaultPeakRefreshRate = (float) context.getResources().getInteger( - R.integer.config_defaultPeakRefreshRate); + setRefreshRates(/* displayDeviceConfig= */ null); + } + + /** + * This is used to update the refresh rate configs from the DeviceConfig, which + * if missing from DisplayDeviceConfig, and finally fallback to config.xml. + */ + public void setRefreshRates(DisplayDeviceConfig displayDeviceConfig) { + setDefaultPeakRefreshRate(displayDeviceConfig); mDefaultRefreshRate = - (float) context.getResources().getInteger(R.integer.config_defaultRefreshRate); + (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger( + R.integer.config_defaultRefreshRate) + : (float) displayDeviceConfig.getDefaultRefreshRate(); } public void observe() { @@ -1196,6 +1215,23 @@ public class DisplayModeDirector { } } + private void setDefaultPeakRefreshRate(DisplayDeviceConfig displayDeviceConfig) { + Float defaultPeakRefreshRate = null; + try { + defaultPeakRefreshRate = + mDeviceConfigDisplaySettings.getDefaultPeakRefreshRate(); + } catch (Exception exception) { + // Do nothing + } + if (defaultPeakRefreshRate == null) { + defaultPeakRefreshRate = + (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger( + R.integer.config_defaultPeakRefreshRate) + : (float) displayDeviceConfig.getDefaultPeakRefreshRate(); + } + mDefaultPeakRefreshRate = defaultPeakRefreshRate; + } + private void updateLowPowerModeSettingLocked() { boolean inLowPowerMode = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.LOW_POWER_MODE, 0 /*default*/) != 0; @@ -1508,12 +1544,31 @@ public class DisplayModeDirector { mContext = context; mHandler = handler; mInjector = injector; + updateBlockingZoneThresholds(/* displayDeviceConfig= */ null); + mRefreshRateInHighZone = context.getResources().getInteger( + R.integer.config_fixedRefreshRateInHighZone); + } - mLowDisplayBrightnessThresholds = context.getResources().getIntArray( - R.array.config_brightnessThresholdsOfPeakRefreshRate); - mLowAmbientBrightnessThresholds = context.getResources().getIntArray( - R.array.config_ambientThresholdsOfPeakRefreshRate); - + /** + * This is used to update the blocking zone thresholds from the DeviceConfig, which + * if missing from DisplayDeviceConfig, and finally fallback to config.xml. + */ + public void updateBlockingZoneThresholds(DisplayDeviceConfig displayDeviceConfig) { + loadLowBrightnessThresholds(displayDeviceConfig); + loadHighBrightnessThresholds(displayDeviceConfig); + } + + private void loadLowBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig) { + mLowDisplayBrightnessThresholds = loadBrightnessThresholds( + () -> mDeviceConfigDisplaySettings.getLowDisplayBrightnessThresholds(), + () -> displayDeviceConfig.getLowDisplayBrightnessThresholds(), + R.array.config_brightnessThresholdsOfPeakRefreshRate, + displayDeviceConfig); + mLowAmbientBrightnessThresholds = loadBrightnessThresholds( + () -> mDeviceConfigDisplaySettings.getLowAmbientBrightnessThresholds(), + () -> displayDeviceConfig.getLowAmbientBrightnessThresholds(), + R.array.config_ambientThresholdsOfPeakRefreshRate, + displayDeviceConfig); if (mLowDisplayBrightnessThresholds.length != mLowAmbientBrightnessThresholds.length) { throw new RuntimeException("display low brightness threshold array and ambient " + "brightness threshold array have different length: " @@ -1522,11 +1577,19 @@ public class DisplayModeDirector { + ", ambientBrightnessThresholds=" + Arrays.toString(mLowAmbientBrightnessThresholds)); } + } - mHighDisplayBrightnessThresholds = context.getResources().getIntArray( - R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate); - mHighAmbientBrightnessThresholds = context.getResources().getIntArray( - R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate); + private void loadHighBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig) { + mHighDisplayBrightnessThresholds = loadBrightnessThresholds( + () -> mDeviceConfigDisplaySettings.getHighDisplayBrightnessThresholds(), + () -> displayDeviceConfig.getHighDisplayBrightnessThresholds(), + R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate, + displayDeviceConfig); + mHighAmbientBrightnessThresholds = loadBrightnessThresholds( + () -> mDeviceConfigDisplaySettings.getHighAmbientBrightnessThresholds(), + () -> displayDeviceConfig.getHighAmbientBrightnessThresholds(), + R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate, + displayDeviceConfig); if (mHighDisplayBrightnessThresholds.length != mHighAmbientBrightnessThresholds.length) { throw new RuntimeException("display high brightness threshold array and ambient " @@ -1536,8 +1599,32 @@ public class DisplayModeDirector { + ", ambientBrightnessThresholds=" + Arrays.toString(mHighAmbientBrightnessThresholds)); } - mRefreshRateInHighZone = context.getResources().getInteger( - R.integer.config_fixedRefreshRateInHighZone); + } + + private int[] loadBrightnessThresholds( + Callable<int[]> loadFromDeviceConfigDisplaySettingsCallable, + Callable<int[]> loadFromDisplayDeviceConfigCallable, + int brightnessThresholdOfFixedRefreshRateKey, + DisplayDeviceConfig displayDeviceConfig) { + int[] brightnessThresholds = null; + try { + brightnessThresholds = + loadFromDeviceConfigDisplaySettingsCallable.call(); + } catch (Exception exception) { + // Do nothing + } + if (brightnessThresholds == null) { + try { + brightnessThresholds = + (displayDeviceConfig == null) ? mContext.getResources().getIntArray( + brightnessThresholdOfFixedRefreshRateKey) + : loadFromDisplayDeviceConfigCallable.call(); + } catch (Exception e) { + Slog.e(TAG, "Unexpectedly failed to load display brightness threshold"); + e.printStackTrace(); + } + } + return brightnessThresholds; } /** @@ -1590,7 +1677,6 @@ public class DisplayModeDirector { mLowAmbientBrightnessThresholds = lowAmbientBrightnessThresholds; } - int[] highDisplayBrightnessThresholds = mDeviceConfigDisplaySettings.getHighDisplayBrightnessThresholds(); int[] highAmbientBrightnessThresholds = diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 458cf1a31b97..95dc23fc3120 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -558,13 +558,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mScreenBrightnessForVrRangeMinimum = clampAbsoluteBrightness( pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM_VR)); - // Check the setting, but also verify that it is the default display. Only the default - // display has an automatic brightness controller running. - // TODO: b/179021925 - Fix to work with multiple displays - mUseSoftwareAutoBrightnessConfig = resources.getBoolean( - com.android.internal.R.bool.config_automatic_brightness_available) - && mDisplayId == Display.DEFAULT_DISPLAY; - mAllowAutoBrightnessWhileDozingConfig = resources.getBoolean( com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing); @@ -938,6 +931,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } private void setUpAutoBrightness(Resources resources, Handler handler) { + mUseSoftwareAutoBrightnessConfig = mDisplayDeviceConfig.isAutoBrightnessAvailable(); + if (!mUseSoftwareAutoBrightnessConfig) { return; } diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java index b9a0738d15c4..a11f1721a4b9 100644 --- a/services/core/java/com/android/server/display/PersistentDataStore.java +++ b/services/core/java/com/android/server/display/PersistentDataStore.java @@ -75,6 +75,11 @@ import java.util.Objects; * </brightness-curve> * </brightness-configuration> * </brightness-configurations> + * <display-mode>0< + * <resolution-width>1080</resolution-width> + * <resolution-height>1920</resolution-height> + * <refresh-rate>60</refresh-rate> + * </display-mode> * </display> * </display-states> * <stable-device-values> @@ -121,6 +126,10 @@ final class PersistentDataStore { private static final String ATTR_PACKAGE_NAME = "package-name"; private static final String ATTR_TIME_STAMP = "timestamp"; + private static final String TAG_RESOLUTION_WIDTH = "resolution-width"; + private static final String TAG_RESOLUTION_HEIGHT = "resolution-height"; + private static final String TAG_REFRESH_RATE = "refresh-rate"; + // Remembered Wifi display devices. private ArrayList<WifiDisplay> mRememberedWifiDisplays = new ArrayList<WifiDisplay>(); @@ -696,6 +705,18 @@ final class PersistentDataStore { case TAG_BRIGHTNESS_CONFIGURATIONS: mDisplayBrightnessConfigurations.loadFromXml(parser); break; + case TAG_RESOLUTION_WIDTH: + String width = parser.nextText(); + mWidth = Integer.parseInt(width); + break; + case TAG_RESOLUTION_HEIGHT: + String height = parser.nextText(); + mHeight = Integer.parseInt(height); + break; + case TAG_REFRESH_RATE: + String refreshRate = parser.nextText(); + mRefreshRate = Float.parseFloat(refreshRate); + break; } } } @@ -712,6 +733,18 @@ final class PersistentDataStore { serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS); mDisplayBrightnessConfigurations.saveToXml(serializer); serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATIONS); + + serializer.startTag(null, TAG_RESOLUTION_WIDTH); + serializer.text(Integer.toString(mWidth)); + serializer.endTag(null, TAG_RESOLUTION_WIDTH); + + serializer.startTag(null, TAG_RESOLUTION_HEIGHT); + serializer.text(Integer.toString(mHeight)); + serializer.endTag(null, TAG_RESOLUTION_HEIGHT); + + serializer.startTag(null, TAG_REFRESH_RATE); + serializer.text(Float.toString(mRefreshRate)); + serializer.endTag(null, TAG_REFRESH_RATE); } public void dump(final PrintWriter pw, final String prefix) { @@ -719,6 +752,8 @@ final class PersistentDataStore { pw.println(prefix + "BrightnessValue=" + mBrightness); pw.println(prefix + "DisplayBrightnessConfigurations: "); mDisplayBrightnessConfigurations.dump(pw, prefix); + pw.println(prefix + "Resolution=" + mWidth + " " + mHeight); + pw.println(prefix + "RefreshRate=" + mRefreshRate); } } diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java index b8af1bfcc254..b11a06eda025 100644 --- a/services/core/java/com/android/server/dreams/DreamController.java +++ b/services/core/java/com/android/server/dreams/DreamController.java @@ -34,13 +34,13 @@ import android.os.UserHandle; import android.service.dreams.DreamService; import android.service.dreams.IDreamService; import android.util.Slog; -import android.view.IWindowManager; -import android.view.WindowManagerGlobal; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Iterator; import java.util.NoSuchElementException; /** @@ -60,9 +60,6 @@ final class DreamController { private final Context mContext; private final Handler mHandler; private final Listener mListener; - private final IWindowManager mIWindowManager; - private long mDreamStartTime; - private String mSavedStopReason; private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED) .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); @@ -73,27 +70,20 @@ final class DreamController { private DreamRecord mCurrentDream; - private final Runnable mStopUnconnectedDreamRunnable = new Runnable() { - @Override - public void run() { - if (mCurrentDream != null && mCurrentDream.mBound && !mCurrentDream.mConnected) { - Slog.w(TAG, "Bound dream did not connect in the time allotted"); - stopDream(true /*immediate*/, "slow to connect"); - } - } - }; + // Whether a dreaming started intent has been broadcast. + private boolean mSentStartBroadcast = false; - private final Runnable mStopStubbornDreamRunnable = () -> { - Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted"); - stopDream(true /*immediate*/, "slow to finish"); - mSavedStopReason = null; - }; + // When a new dream is started and there is an existing dream, the existing dream is allowed to + // live a little longer until the new dream is started, for a smoother transition. This dream is + // stopped as soon as the new dream is started, and this list is cleared. Usually there should + // only be one previous dream while waiting for a new dream to start, but we store a list to + // proof the edge case of multiple previous dreams. + private final ArrayList<DreamRecord> mPreviousDreams = new ArrayList<>(); public DreamController(Context context, Handler handler, Listener listener) { mContext = context; mHandler = handler; mListener = listener; - mIWindowManager = WindowManagerGlobal.getWindowManagerService(); mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); mCloseNotificationShadeIntent.putExtra("reason", "dream"); } @@ -109,18 +99,17 @@ final class DreamController { pw.println(" mUserId=" + mCurrentDream.mUserId); pw.println(" mBound=" + mCurrentDream.mBound); pw.println(" mService=" + mCurrentDream.mService); - pw.println(" mSentStartBroadcast=" + mCurrentDream.mSentStartBroadcast); pw.println(" mWakingGently=" + mCurrentDream.mWakingGently); } else { pw.println(" mCurrentDream: null"); } + + pw.println(" mSentStartBroadcast=" + mSentStartBroadcast); } public void startDream(Binder token, ComponentName name, boolean isPreviewMode, boolean canDoze, int userId, PowerManager.WakeLock wakeLock, ComponentName overlayComponentName, String reason) { - stopDream(true /*immediate*/, "starting new dream"); - Trace.traceBegin(Trace.TRACE_TAG_POWER, "startDream"); try { // Close the notification shade. No need to send to all, but better to be explicit. @@ -130,9 +119,12 @@ final class DreamController { + ", isPreviewMode=" + isPreviewMode + ", canDoze=" + canDoze + ", userId=" + userId + ", reason='" + reason + "'"); + if (mCurrentDream != null) { + mPreviousDreams.add(mCurrentDream); + } mCurrentDream = new DreamRecord(token, name, isPreviewMode, canDoze, userId, wakeLock); - mDreamStartTime = SystemClock.elapsedRealtime(); + mCurrentDream.mDreamStartTime = SystemClock.elapsedRealtime(); MetricsLogger.visible(mContext, mCurrentDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING); @@ -155,31 +147,49 @@ final class DreamController { } mCurrentDream.mBound = true; - mHandler.postDelayed(mStopUnconnectedDreamRunnable, DREAM_CONNECTION_TIMEOUT); + mHandler.postDelayed(mCurrentDream.mStopUnconnectedDreamRunnable, + DREAM_CONNECTION_TIMEOUT); } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } } + /** + * Stops dreaming. + * + * The current dream, if any, and any unstopped previous dreams are stopped. The device stops + * dreaming. + */ public void stopDream(boolean immediate, String reason) { - if (mCurrentDream == null) { + stopPreviousDreams(); + stopDreamInstance(immediate, reason, mCurrentDream); + } + + /** + * Stops the given dream instance. + * + * The device may still be dreaming afterwards if there are other dreams running. + */ + private void stopDreamInstance(boolean immediate, String reason, DreamRecord dream) { + if (dream == null) { return; } Trace.traceBegin(Trace.TRACE_TAG_POWER, "stopDream"); try { if (!immediate) { - if (mCurrentDream.mWakingGently) { + if (dream.mWakingGently) { return; // already waking gently } - if (mCurrentDream.mService != null) { + if (dream.mService != null) { // Give the dream a moment to wake up and finish itself gently. - mCurrentDream.mWakingGently = true; + dream.mWakingGently = true; try { - mSavedStopReason = reason; - mCurrentDream.mService.wakeUp(); - mHandler.postDelayed(mStopStubbornDreamRunnable, DREAM_FINISH_TIMEOUT); + dream.mStopReason = reason; + dream.mService.wakeUp(); + mHandler.postDelayed(dream.mStopStubbornDreamRunnable, + DREAM_FINISH_TIMEOUT); return; } catch (RemoteException ex) { // oh well, we tried, finish immediately instead @@ -187,54 +197,73 @@ final class DreamController { } } - final DreamRecord oldDream = mCurrentDream; - mCurrentDream = null; - Slog.i(TAG, "Stopping dream: name=" + oldDream.mName - + ", isPreviewMode=" + oldDream.mIsPreviewMode - + ", canDoze=" + oldDream.mCanDoze - + ", userId=" + oldDream.mUserId + Slog.i(TAG, "Stopping dream: name=" + dream.mName + + ", isPreviewMode=" + dream.mIsPreviewMode + + ", canDoze=" + dream.mCanDoze + + ", userId=" + dream.mUserId + ", reason='" + reason + "'" - + (mSavedStopReason == null ? "" : "(from '" + mSavedStopReason + "')")); + + (dream.mStopReason == null ? "" : "(from '" + + dream.mStopReason + "')")); MetricsLogger.hidden(mContext, - oldDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING); + dream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING); MetricsLogger.histogram(mContext, - oldDream.mCanDoze ? "dozing_minutes" : "dreaming_minutes" , - (int) ((SystemClock.elapsedRealtime() - mDreamStartTime) / (1000L * 60L))); + dream.mCanDoze ? "dozing_minutes" : "dreaming_minutes", + (int) ((SystemClock.elapsedRealtime() - dream.mDreamStartTime) / (1000L + * 60L))); - mHandler.removeCallbacks(mStopUnconnectedDreamRunnable); - mHandler.removeCallbacks(mStopStubbornDreamRunnable); - mSavedStopReason = null; + mHandler.removeCallbacks(dream.mStopUnconnectedDreamRunnable); + mHandler.removeCallbacks(dream.mStopStubbornDreamRunnable); - if (oldDream.mSentStartBroadcast) { - mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL); - } - - if (oldDream.mService != null) { + if (dream.mService != null) { try { - oldDream.mService.detach(); + dream.mService.detach(); } catch (RemoteException ex) { // we don't care; this thing is on the way out } try { - oldDream.mService.asBinder().unlinkToDeath(oldDream, 0); + dream.mService.asBinder().unlinkToDeath(dream, 0); } catch (NoSuchElementException ex) { // don't care } - oldDream.mService = null; + dream.mService = null; } - if (oldDream.mBound) { - mContext.unbindService(oldDream); + if (dream.mBound) { + mContext.unbindService(dream); } - oldDream.releaseWakeLockIfNeeded(); + dream.releaseWakeLockIfNeeded(); + + // Current dream stopped, device no longer dreaming. + if (dream == mCurrentDream) { + mCurrentDream = null; + + if (mSentStartBroadcast) { + mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL); + } - mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken)); + mListener.onDreamStopped(dream.mToken); + } } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } } + /** + * Stops all previous dreams, if any. + */ + private void stopPreviousDreams() { + if (mPreviousDreams.isEmpty()) { + return; + } + + // Using an iterator because mPreviousDreams is modified while the iteration is in process. + for (final Iterator<DreamRecord> it = mPreviousDreams.iterator(); it.hasNext(); ) { + stopDreamInstance(true /*immediate*/, "stop previous dream", it.next()); + it.remove(); + } + } + private void attach(IDreamService service) { try { service.asBinder().linkToDeath(mCurrentDream, 0); @@ -248,9 +277,9 @@ final class DreamController { mCurrentDream.mService = service; - if (!mCurrentDream.mIsPreviewMode) { + if (!mCurrentDream.mIsPreviewMode && !mSentStartBroadcast) { mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL); - mCurrentDream.mSentStartBroadcast = true; + mSentStartBroadcast = true; } } @@ -272,10 +301,35 @@ final class DreamController { public boolean mBound; public boolean mConnected; public IDreamService mService; - public boolean mSentStartBroadcast; - + private String mStopReason; + private long mDreamStartTime; public boolean mWakingGently; + private final Runnable mStopPreviousDreamsIfNeeded = this::stopPreviousDreamsIfNeeded; + private final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded; + + private final Runnable mStopUnconnectedDreamRunnable = () -> { + if (mBound && !mConnected) { + Slog.w(TAG, "Bound dream did not connect in the time allotted"); + stopDream(true /*immediate*/, "slow to connect" /*reason*/); + } + }; + + private final Runnable mStopStubbornDreamRunnable = () -> { + Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted"); + stopDream(true /*immediate*/, "slow to finish" /*reason*/); + mStopReason = null; + }; + + private final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() { + // May be called on any thread. + @Override + public void sendResult(Bundle data) { + mHandler.post(mStopPreviousDreamsIfNeeded); + mHandler.post(mReleaseWakeLockIfNeeded); + } + }; + DreamRecord(Binder token, ComponentName name, boolean isPreviewMode, boolean canDoze, int userId, PowerManager.WakeLock wakeLock) { mToken = token; @@ -286,7 +340,9 @@ final class DreamController { mWakeLock = wakeLock; // Hold the lock while we're waiting for the service to connect and start dreaming. // Released after the service has started dreaming, we stop dreaming, or it timed out. - mWakeLock.acquire(); + if (mWakeLock != null) { + mWakeLock.acquire(); + } mHandler.postDelayed(mReleaseWakeLockIfNeeded, 10000); } @@ -326,6 +382,12 @@ final class DreamController { }); } + void stopPreviousDreamsIfNeeded() { + if (mCurrentDream == DreamRecord.this) { + stopPreviousDreams(); + } + } + void releaseWakeLockIfNeeded() { if (mWakeLock != null) { mWakeLock.release(); @@ -333,15 +395,5 @@ final class DreamController { mHandler.removeCallbacks(mReleaseWakeLockIfNeeded); } } - - final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded; - - final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() { - // May be called on any thread. - @Override - public void sendResult(Bundle data) throws RemoteException { - mHandler.post(mReleaseWakeLockIfNeeded); - } - }; } } diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index c9557d60c8b7..5589673973c3 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -493,8 +493,6 @@ public final class DreamManagerService extends SystemService { return; } - stopDreamLocked(true /*immediate*/, "starting new dream"); - Slog.i(TAG, "Entering dreamland."); mCurrentDream = new DreamRecord(name, userId, isPreviewMode, canDoze); diff --git a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java index 6759d79eedca..9a190316f4eb 100644 --- a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java +++ b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java @@ -32,7 +32,6 @@ import android.os.Handler; import android.os.PowerWhitelistManager; import android.os.UserHandle; import android.text.TextUtils; -import android.util.EventLog; import android.util.Log; import android.view.KeyEvent; @@ -118,12 +117,6 @@ final class MediaButtonReceiverHolder { int componentType = getComponentType(pendingIntent); ComponentName componentName = getComponentName(pendingIntent, componentType); if (componentName != null) { - if (!TextUtils.equals(componentName.getPackageName(), sessionPackageName)) { - EventLog.writeEvent(0x534e4554, "238177121", -1, ""); // SafetyNet logging - throw new IllegalArgumentException("ComponentName does not belong to " - + "sessionPackageName. sessionPackageName = " + sessionPackageName - + ", ComponentName pkg = " + componentName.getPackageName()); - } return new MediaButtonReceiverHolder(userId, pendingIntent, componentName, componentType); } diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index b8131a8ee5b5..604e8f3949f4 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -52,8 +52,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.SystemClock; -import android.text.TextUtils; -import android.util.EventLog; import android.util.Log; import android.view.KeyEvent; @@ -940,14 +938,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR @Override public void setMediaButtonReceiver(PendingIntent pi, String sessionPackageName) throws RemoteException { - //mPackageName has been verified in MediaSessionService.enforcePackageName(). - if (!TextUtils.equals(sessionPackageName, mPackageName)) { - EventLog.writeEvent(0x534e4554, "238177121", -1, ""); // SafetyNet logging - throw new IllegalArgumentException("sessionPackageName name does not match " - + "package name provided to MediaSessionRecord. sessionPackageName = " - + sessionPackageName + ", pkg = " - + mPackageName); - } final long token = Binder.clearCallingIdentity(); try { if ((mPolicies & MediaSessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_RECEIVER) @@ -966,15 +956,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR public void setMediaButtonBroadcastReceiver(ComponentName receiver) throws RemoteException { final long token = Binder.clearCallingIdentity(); try { - //mPackageName has been verified in MediaSessionService.enforcePackageName(). - if (receiver != null && !TextUtils.equals( - mPackageName, receiver.getPackageName())) { - EventLog.writeEvent(0x534e4554, "238177121", -1, ""); // SafetyNet logging - throw new IllegalArgumentException("receiver does not belong to " - + "package name provided to MediaSessionRecord. Pkg = " + mPackageName - + ", Receiver Pkg = " + receiver.getPackageName()); - } - if ((mPolicies & MediaSessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_RECEIVER) != 0) { return; diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index fd1fdce4cf77..37f980d699bd 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -61,6 +61,7 @@ import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_TELECOM; import static android.content.pm.PackageManager.FEATURE_TELEVISION; import static android.content.pm.PackageManager.MATCH_ALL; +import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -1977,34 +1978,39 @@ public class NotificationManagerService extends SystemService { return (haystack & needle) != 0; } - public boolean isInLockDownMode() { - return mIsInLockDownMode; + // Return whether the user is in lockdown mode. + // If the flag is not set, we assume the user is not in lockdown. + public boolean isInLockDownMode(int userId) { + return mUserInLockDownMode.get(userId, false); } @Override public synchronized void onStrongAuthRequiredChanged(int userId) { boolean userInLockDownModeNext = containsFlag(getStrongAuthForUser(userId), STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); - mUserInLockDownMode.put(userId, userInLockDownModeNext); - boolean isInLockDownModeNext = mUserInLockDownMode.indexOfValue(true) != -1; - if (mIsInLockDownMode == isInLockDownModeNext) { + // Nothing happens if the lockdown mode of userId keeps the same. + if (userInLockDownModeNext == isInLockDownMode(userId)) { return; } - if (isInLockDownModeNext) { - cancelNotificationsWhenEnterLockDownMode(); + // When the lockdown mode is changed, we perform the following steps. + // If the userInLockDownModeNext is true, all the function calls to + // notifyPostedLocked and notifyRemovedLocked will not be executed. + // The cancelNotificationsWhenEnterLockDownMode calls notifyRemovedLocked + // and postNotificationsWhenExitLockDownMode calls notifyPostedLocked. + // So we shall call cancelNotificationsWhenEnterLockDownMode before + // we set mUserInLockDownMode as true. + // On the other hand, if the userInLockDownModeNext is false, we shall call + // postNotificationsWhenExitLockDownMode after we put false into mUserInLockDownMode + if (userInLockDownModeNext) { + cancelNotificationsWhenEnterLockDownMode(userId); } - // When the mIsInLockDownMode is true, both notifyPostedLocked and - // notifyRemovedLocked will be dismissed. So we shall call - // cancelNotificationsWhenEnterLockDownMode before we set mIsInLockDownMode - // as true and call postNotificationsWhenExitLockDownMode after we set - // mIsInLockDownMode as false. - mIsInLockDownMode = isInLockDownModeNext; + mUserInLockDownMode.put(userId, userInLockDownModeNext); - if (!isInLockDownModeNext) { - postNotificationsWhenExitLockDownMode(); + if (!userInLockDownModeNext) { + postNotificationsWhenExitLockDownMode(userId); } } } @@ -9678,11 +9684,14 @@ public class NotificationManagerService extends SystemService { } } - private void cancelNotificationsWhenEnterLockDownMode() { + private void cancelNotificationsWhenEnterLockDownMode(int userId) { synchronized (mNotificationLock) { int numNotifications = mNotificationList.size(); for (int i = 0; i < numNotifications; i++) { NotificationRecord rec = mNotificationList.get(i); + if (rec.getUser().getIdentifier() != userId) { + continue; + } mListeners.notifyRemovedLocked(rec, REASON_CANCEL_ALL, rec.getStats()); } @@ -9690,14 +9699,23 @@ public class NotificationManagerService extends SystemService { } } - private void postNotificationsWhenExitLockDownMode() { + private void postNotificationsWhenExitLockDownMode(int userId) { synchronized (mNotificationLock) { int numNotifications = mNotificationList.size(); + // Set the delay to spread out the burst of notifications. + long delay = 0; for (int i = 0; i < numNotifications; i++) { NotificationRecord rec = mNotificationList.get(i); - mListeners.notifyPostedLocked(rec, rec); + if (rec.getUser().getIdentifier() != userId) { + continue; + } + mHandler.postDelayed(() -> { + synchronized (mNotificationLock) { + mListeners.notifyPostedLocked(rec, rec); + } + }, delay); + delay += 20; } - } } @@ -9876,6 +9894,9 @@ public class NotificationManagerService extends SystemService { for (int i = 0; i < N; i++) { NotificationRecord record = mNotificationList.get(i); + if (isInLockDownMode(record.getUser().getIdentifier())) { + continue; + } if (!isVisibleToListener(record.getSbn(), record.getNotificationType(), info)) { continue; } @@ -9917,8 +9938,8 @@ public class NotificationManagerService extends SystemService { rankings.toArray(new NotificationListenerService.Ranking[0])); } - boolean isInLockDownMode() { - return mStrongAuthTracker.isInLockDownMode(); + boolean isInLockDownMode(int userId) { + return mStrongAuthTracker.isInLockDownMode(userId); } boolean hasCompanionDevice(ManagedServiceInfo info) { @@ -10633,10 +10654,18 @@ public class NotificationManagerService extends SystemService { private final ArraySet<ManagedServiceInfo> mLightTrimListeners = new ArraySet<>(); ArrayMap<Pair<ComponentName, Integer>, NotificationListenerFilter> mRequestedNotificationListeners = new ArrayMap<>(); + private final boolean mIsHeadlessSystemUserMode; public NotificationListeners(Context context, Object lock, UserProfiles userProfiles, IPackageManager pm) { + this(context, lock, userProfiles, pm, UserManager.isHeadlessSystemUserMode()); + } + + @VisibleForTesting + public NotificationListeners(Context context, Object lock, UserProfiles userProfiles, + IPackageManager pm, boolean isHeadlessSystemUserMode) { super(context, lock, userProfiles, pm); + this.mIsHeadlessSystemUserMode = isHeadlessSystemUserMode; } @Override @@ -10661,10 +10690,16 @@ public class NotificationManagerService extends SystemService { if (TextUtils.isEmpty(listeners[i])) { continue; } + int packageQueryFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE; + // In the headless system user mode, packages might not be installed for the + // system user. Match packages for any user since apps can be installed only for + // non-system users and would be considering uninstalled for the system user. + if (mIsHeadlessSystemUserMode) { + packageQueryFlags += MATCH_ANY_USER; + } ArraySet<ComponentName> approvedListeners = - this.queryPackageForServices(listeners[i], - MATCH_DIRECT_BOOT_AWARE - | MATCH_DIRECT_BOOT_UNAWARE, USER_SYSTEM); + this.queryPackageForServices(listeners[i], packageQueryFlags, + USER_SYSTEM); for (int k = 0; k < approvedListeners.size(); k++) { ComponentName cn = approvedListeners.valueAt(k); addDefaultComponentOrPackage(cn.flattenToString()); @@ -10974,7 +11009,7 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") void notifyPostedLocked(NotificationRecord r, NotificationRecord old, boolean notifyAllListeners) { - if (isInLockDownMode()) { + if (isInLockDownMode(r.getUser().getIdentifier())) { return; } @@ -11080,7 +11115,7 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") public void notifyRemovedLocked(NotificationRecord r, int reason, NotificationStats notificationStats) { - if (isInLockDownMode()) { + if (isInLockDownMode(r.getUser().getIdentifier())) { return; } @@ -11129,10 +11164,6 @@ public class NotificationManagerService extends SystemService { */ @GuardedBy("mNotificationLock") public void notifyRankingUpdateLocked(List<NotificationRecord> changedHiddenNotifications) { - if (isInLockDownMode()) { - return; - } - boolean isHiddenRankingUpdate = changedHiddenNotifications != null && changedHiddenNotifications.size() > 0; // TODO (b/73052211): if the ranking update changed the notification type, diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index 014d580e520f..18c29fa9f3a2 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -644,8 +644,8 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt Permission bp = mRegistry.getPermission(info.name); added = bp == null; int fixedLevel = PermissionInfo.fixProtectionLevel(info.protectionLevel); + enforcePermissionCapLocked(info, tree); if (added) { - enforcePermissionCapLocked(info, tree); bp = new Permission(info.name, tree.getPackageName(), Permission.TYPE_DYNAMIC); } else if (!bp.isDynamic()) { throw new SecurityException("Not allowed to modify non-dynamic permission " diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 4ec92ec9190b..725fb3fec616 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -6695,6 +6695,11 @@ public final class PowerManagerService extends SystemService public void nap(long eventTime, boolean allowWake) { napInternal(eventTime, Process.SYSTEM_UID, allowWake); } + + @Override + public boolean isAmbientDisplaySuppressed() { + return mAmbientDisplaySuppressionController.isSuppressed(); + } } /** diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 653b51a95993..3df8f584fef1 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -2253,6 +2253,25 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; + boolean proto = false; + for (int i = 0; i < args.length; i++) { + if ("--proto".equals(args[i])) { + proto = true; + } + } + if (proto) { + if (mBar == null) return; + try (TransferPipe tp = new TransferPipe()) { + // Sending the command to the remote, which needs to execute async to avoid blocking + // See Binder#dumpAsync() for inspiration + mBar.dumpProto(args, tp.getWriteFd()); + // Times out after 5s + tp.go(fd); + } catch (Throwable t) { + Slog.e(TAG, "Error sending command to IStatusBar", t); + } + return; + } synchronized (mLock) { for (int i = 0; i < mDisplayUiState.size(); i++) { diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java index 0799b955b6f1..9c455dbb5320 100644 --- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java +++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java @@ -65,6 +65,9 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { public final DeviceVibrationEffectAdapter deviceEffectAdapter; public final VibrationThread.VibratorManagerHooks vibratorManagerHooks; + // Not guarded by lock because they're not modified by this conductor, it's used here only to + // check immutable attributes. The status and other mutable states are changed by the service or + // by the vibrator steps. private final Vibration mVibration; private final SparseArray<VibratorController> mVibrators = new SparseArray<>(); @@ -405,6 +408,16 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { } } + /** Returns true if a cancellation signal was sent via {@link #notifyCancelled}. */ + public boolean wasNotifiedToCancel() { + if (Build.IS_DEBUGGABLE) { + expectIsVibrationThread(false); + } + synchronized (mLock) { + return mSignalCancel != null; + } + } + @GuardedBy("mLock") private boolean hasPendingNotifySignalLocked() { if (Build.IS_DEBUGGABLE) { diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 97275583ac36..14693599c3ec 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -852,8 +852,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } Vibration currentVibration = mCurrentVibration.getVibration(); - if (currentVibration.hasEnded()) { - // Current vibration is finishing up, it should not block incoming vibrations. + if (currentVibration.hasEnded() || mCurrentVibration.wasNotifiedToCancel()) { + // Current vibration has ended or is cancelling, should not block incoming vibrations. return null; } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index f25929c36060..189b86fd0ff1 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -1166,7 +1166,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub try { connection.mService.attach(connection, mToken, TYPE_WALLPAPER, false, wpdData.mWidth, wpdData.mHeight, - wpdData.mPadding, mDisplayId); + wpdData.mPadding, mDisplayId, FLAG_SYSTEM | FLAG_LOCK); } catch (RemoteException e) { Slog.w(TAG, "Failed attaching wallpaper on display", e); if (wallpaper != null && !wallpaper.wallpaperUpdating diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index e3916cbc30bd..fe691c61a96b 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2593,6 +2593,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { // activity lifecycle transaction to make sure the override pending app // transition will be applied immediately. targetActivity.applyOptionsAnimation(); + if (activityOptions != null && activityOptions.getLaunchCookie() != null) { + targetActivity.mLaunchCookie = activityOptions.getLaunchCookie(); + } } finally { mActivityMetricsLogger.notifyActivityLaunched(launchingState, START_TASK_TO_FRONT, false /* newActivityCreated */, diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index bf4b65da8b43..3a8fbbbaa77d 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -173,6 +173,7 @@ abstract class InsetsSourceProvider { mWindowContainer = windowContainer; // TODO: remove the frame provider for non-WindowState container. mFrameProvider = frameProvider; + mOverrideFrames.clear(); mOverrideFrameProviders = overrideFrameProviders; if (windowContainer == null) { setServerVisible(false); @@ -234,6 +235,8 @@ abstract class InsetsSourceProvider { updateSourceFrameForServerVisibility(); if (mOverrideFrameProviders != null) { + // Not necessary to clear the mOverrideFrames here. It will be cleared every time the + // override frame provider updates. for (int i = mOverrideFrameProviders.size() - 1; i >= 0; i--) { final int windowType = mOverrideFrameProviders.keyAt(i); final Rect overrideFrame; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 731754e1f0cb..eba49bbc7301 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -21,7 +21,6 @@ import static android.app.ActivityTaskManager.RESIZE_MODE_FORCED; import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION; import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN; import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; @@ -5855,12 +5854,10 @@ class Task extends TaskFragment { return false; } - // Existing Tasks can be reused if a new root task will be created anyway, or for the - // Dream - because there can only ever be one DreamActivity. + // Existing Tasks can be reused if a new root task will be created anyway. final int windowingMode = getWindowingMode(); final int activityType = getActivityType(); - return DisplayContent.alwaysCreateRootTask(windowingMode, activityType) - || activityType == ACTIVITY_TYPE_DREAM; + return DisplayContent.alwaysCreateRootTask(windowingMode, activityType); } void addChild(WindowContainer child, final boolean toTop, boolean showForAllUsers) { diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 867833a3271a..509b1e6f41ca 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -184,19 +184,30 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } void dispose() { - while (!mOrganizedTaskFragments.isEmpty()) { - final TaskFragment taskFragment = mOrganizedTaskFragments.get(0); - // Cleanup before remove to prevent it from sending any additional event, such as - // #onTaskFragmentVanished, to the removed organizer. + for (int i = mOrganizedTaskFragments.size() - 1; i >= 0; i--) { + // Cleanup the TaskFragmentOrganizer from all TaskFragments it organized before + // removing the windows to prevent it from adding any additional TaskFragment + // pending event. + final TaskFragment taskFragment = mOrganizedTaskFragments.get(i); taskFragment.onTaskFragmentOrganizerRemoved(); - taskFragment.removeImmediately(); - mOrganizedTaskFragments.remove(taskFragment); } + + // Defer to avoid unnecessary layout when there are multiple TaskFragments removal. + mAtmService.deferWindowLayout(); + try { + while (!mOrganizedTaskFragments.isEmpty()) { + final TaskFragment taskFragment = mOrganizedTaskFragments.remove(0); + taskFragment.removeImmediately(); + } + } finally { + mAtmService.continueWindowLayout(); + } + for (int i = mDeferredTransitions.size() - 1; i >= 0; i--) { // Cleanup any running transaction to unblock the current transition. onTransactionFinished(mDeferredTransitions.keyAt(i)); } - mOrganizer.asBinder().unlinkToDeath(this, 0 /*flags*/); + mOrganizer.asBinder().unlinkToDeath(this, 0 /* flags */); } @NonNull @@ -426,7 +437,6 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr @Override public void unregisterOrganizer(@NonNull ITaskFragmentOrganizer organizer) { - validateAndGetState(organizer); final int pid = Binder.getCallingPid(); final long uid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); @@ -607,6 +617,13 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr int opType, @NonNull Throwable exception) { validateAndGetState(organizer); Slog.w(TAG, "onTaskFragmentError ", exception); + final PendingTaskFragmentEvent vanishedEvent = taskFragment != null + ? getPendingTaskFragmentEvent(taskFragment, PendingTaskFragmentEvent.EVENT_VANISHED) + : null; + if (vanishedEvent != null) { + // No need to notify if the TaskFragment has been removed. + return; + } addPendingEvent(new PendingTaskFragmentEvent.Builder( PendingTaskFragmentEvent.EVENT_ERROR, organizer) .setErrorCallbackToken(errorCallbackToken) @@ -690,11 +707,17 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } private void removeOrganizer(@NonNull ITaskFragmentOrganizer organizer) { - final TaskFragmentOrganizerState state = validateAndGetState(organizer); + final TaskFragmentOrganizerState state = mTaskFragmentOrganizerState.get( + organizer.asBinder()); + if (state == null) { + Slog.w(TAG, "The organizer has already been removed."); + return; + } + // Remove any pending event of this organizer first because state.dispose() may trigger + // event dispatch as result of surface placement. + mPendingTaskFragmentEvents.remove(organizer.asBinder()); // remove all of the children of the organized TaskFragment state.dispose(); - // Remove any pending event of this organizer. - mPendingTaskFragmentEvents.remove(organizer.asBinder()); mTaskFragmentOrganizerState.remove(organizer.asBinder()); } @@ -878,23 +901,6 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr return null; } - private boolean shouldSendEventWhenTaskInvisible(@NonNull PendingTaskFragmentEvent event) { - if (event.mEventType == PendingTaskFragmentEvent.EVENT_ERROR - // Always send parent info changed to update task visibility - || event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) { - return true; - } - - final TaskFragmentOrganizerState state = - mTaskFragmentOrganizerState.get(event.mTaskFragmentOrg.asBinder()); - final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos.get(event.mTaskFragment); - final TaskFragmentInfo info = event.mTaskFragment.getTaskFragmentInfo(); - // Send an info changed callback if this event is for the last activities to finish in a - // TaskFragment so that the {@link TaskFragmentOrganizer} can delete this TaskFragment. - return event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED - && lastInfo != null && lastInfo.hasRunningActivity() && info.isEmpty(); - } - void dispatchPendingEvents() { if (mAtmService.mWindowManager.mWindowPlacerLocked.isLayoutDeferred() || mPendingTaskFragmentEvents.isEmpty()) { @@ -908,37 +914,19 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } } - void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state, + private void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state, @NonNull List<PendingTaskFragmentEvent> pendingEvents) { if (pendingEvents.isEmpty()) { return; } - - final ArrayList<Task> visibleTasks = new ArrayList<>(); - final ArrayList<Task> invisibleTasks = new ArrayList<>(); - final ArrayList<PendingTaskFragmentEvent> candidateEvents = new ArrayList<>(); - for (int i = 0, n = pendingEvents.size(); i < n; i++) { - final PendingTaskFragmentEvent event = pendingEvents.get(i); - final Task task = event.mTaskFragment != null ? event.mTaskFragment.getTask() : null; - // TODO(b/251132298): move visibility check to the client side. - if (task != null && (task.lastActiveTime <= event.mDeferTime - || !(isTaskVisible(task, visibleTasks, invisibleTasks) - || shouldSendEventWhenTaskInvisible(event)))) { - // Defer sending events to the TaskFragment until the host task is active again. - event.mDeferTime = task.lastActiveTime; - continue; - } - candidateEvents.add(event); - } - final int numEvents = candidateEvents.size(); - if (numEvents == 0) { + if (shouldDeferPendingEvents(state, pendingEvents)) { return; } - mTmpTaskSet.clear(); + final int numEvents = pendingEvents.size(); final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); for (int i = 0; i < numEvents; i++) { - final PendingTaskFragmentEvent event = candidateEvents.get(i); + final PendingTaskFragmentEvent event = pendingEvents.get(i); if (event.mEventType == PendingTaskFragmentEvent.EVENT_APPEARED || event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) { final Task task = event.mTaskFragment.getTask(); @@ -954,7 +942,47 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } mTmpTaskSet.clear(); state.dispatchTransaction(transaction); - pendingEvents.removeAll(candidateEvents); + pendingEvents.clear(); + } + + /** + * Whether or not to defer sending the events to the organizer to avoid waking the app process + * when it is in background. We want to either send all events or none to avoid inconsistency. + */ + private boolean shouldDeferPendingEvents(@NonNull TaskFragmentOrganizerState state, + @NonNull List<PendingTaskFragmentEvent> pendingEvents) { + final ArrayList<Task> visibleTasks = new ArrayList<>(); + final ArrayList<Task> invisibleTasks = new ArrayList<>(); + for (int i = 0, n = pendingEvents.size(); i < n; i++) { + final PendingTaskFragmentEvent event = pendingEvents.get(i); + if (event.mEventType != PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED + && event.mEventType != PendingTaskFragmentEvent.EVENT_INFO_CHANGED + && event.mEventType != PendingTaskFragmentEvent.EVENT_APPEARED) { + // Send events for any other types. + return false; + } + + // Check if we should send the event given the Task visibility and events. + final Task task; + if (event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) { + task = event.mTask; + } else { + task = event.mTaskFragment.getTask(); + } + if (task.lastActiveTime > event.mDeferTime + && isTaskVisible(task, visibleTasks, invisibleTasks)) { + // Send events when the app has at least one visible Task. + return false; + } else if (shouldSendEventWhenTaskInvisible(task, state, event)) { + // Sent events even if the Task is invisible. + return false; + } + + // Defer sending events to the organizer until the host task is active (visible) again. + event.mDeferTime = task.lastActiveTime; + } + // Defer for invisible Task. + return true; } private static boolean isTaskVisible(@NonNull Task task, @@ -975,6 +1003,28 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } } + private boolean shouldSendEventWhenTaskInvisible(@NonNull Task task, + @NonNull TaskFragmentOrganizerState state, + @NonNull PendingTaskFragmentEvent event) { + final TaskFragmentParentInfo lastParentInfo = state.mLastSentTaskFragmentParentInfos + .get(task.mTaskId); + if (lastParentInfo == null || lastParentInfo.isVisible()) { + // When the Task was visible, or when there was no Task info changed sent (in which case + // the organizer will consider it as visible by default), always send the event to + // update the Task visibility. + return true; + } + if (event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) { + // Send info changed if the TaskFragment is becoming empty/non-empty so the + // organizer can choose whether or not to remove the TaskFragment. + final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos + .get(event.mTaskFragment); + final boolean isEmpty = event.mTaskFragment.getNonFinishingActivityCount() == 0; + return lastInfo == null || lastInfo.isEmpty() != isEmpty; + } + return false; + } + void dispatchPendingInfoChangedEvent(@NonNull TaskFragment taskFragment) { final PendingTaskFragmentEvent event = getPendingTaskFragmentEvent(taskFragment, PendingTaskFragmentEvent.EVENT_INFO_CHANGED); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 46253c1933b6..32f61978d730 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1602,7 +1602,11 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe change.setMode(info.getTransitMode(target)); change.setStartAbsBounds(info.mAbsoluteBounds); change.setFlags(info.getChangeFlags(target)); + final Task task = target.asTask(); + final TaskFragment taskFragment = target.asTaskFragment(); + final ActivityRecord activityRecord = target.asActivityRecord(); + if (task != null) { final ActivityManager.RunningTaskInfo tinfo = new ActivityManager.RunningTaskInfo(); task.fillTaskInfo(tinfo); @@ -1636,12 +1640,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe change.setEndRelOffset(bounds.left - parentBounds.left, bounds.top - parentBounds.top); int endRotation = target.getWindowConfiguration().getRotation(); - final ActivityRecord activityRecord = target.asActivityRecord(); if (activityRecord != null) { - final Task arTask = activityRecord.getTask(); - final int backgroundColor = ColorUtils.setAlphaComponent( - arTask.getTaskDescription().getBackgroundColor(), 255); - change.setBackgroundColor(backgroundColor); // TODO(b/227427984): Shell needs to aware letterbox. // Always use parent bounds of activity because letterbox area (e.g. fixed aspect // ratio or size compat mode) should be included in the animation. @@ -1654,6 +1653,18 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } else { change.setEndAbsBounds(bounds); } + + if (activityRecord != null || (taskFragment != null && taskFragment.isEmbedded())) { + // Set background color to Task theme color for activity and embedded TaskFragment + // in case we want to show background during the animation. + final Task parentTask = activityRecord != null + ? activityRecord.getTask() + : taskFragment.getTask(); + final int backgroundColor = ColorUtils.setAlphaComponent( + parentTask.getTaskDescription().getBackgroundColor(), 255); + change.setBackgroundColor(backgroundColor); + } + change.setRotation(info.mRotation, endRotation); if (info.mSnapshot != null) { change.setSnapshot(info.mSnapshot, info.mSnapshotLuma); 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 4a51b831da57..f53a1cfcfb3c 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -383,6 +383,7 @@ </xs:complexType> <xs:complexType name="autoBrightness"> + <xs:attribute name="enabled" type="xs:boolean" use="optional" default="true"/> <xs:sequence> <!-- Sets the debounce for autoBrightness brightening in millis--> <xs:element name="brighteningLightDebounceMillis" type="xs:nonNegativeInteger" diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index 748ef4be9cc7..d89bd7cc9aa2 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -6,9 +6,11 @@ package com.android.server.display.config { 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 boolean getEnabled(); 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); + method public void setEnabled(boolean); } public class BrightnessThresholds { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 2ee4af56d320..46141940a907 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1823,7 +1823,8 @@ public final class SystemServer implements Dumpable { t.traceBegin("StartStatusBarManagerService"); try { statusBar = new StatusBarManagerService(context); - ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar); + ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar, false, + DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO); } catch (Throwable e) { reportWtf("starting StatusBarManagerService", e); } diff --git a/services/tests/mockingservicestests/OWNERS b/services/tests/mockingservicestests/OWNERS index 2bb16496e0f0..4dda51f2004f 100644 --- a/services/tests/mockingservicestests/OWNERS +++ b/services/tests/mockingservicestests/OWNERS @@ -1,5 +1,8 @@ include platform/frameworks/base:/services/core/java/com/android/server/am/OWNERS + +# Game Platform per-file FakeGameClassifier.java = file:/GAME_MANAGER_OWNERS per-file FakeGameServiceProviderInstance = file:/GAME_MANAGER_OWNERS per-file FakeServiceConnector.java = file:/GAME_MANAGER_OWNERS per-file Game* = file:/GAME_MANAGER_OWNERS +per-file res/xml/game_manager* = file:/GAME_MANAGER_OWNERS diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java index 68c9ce4a9f86..0cff4f14bf23 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java @@ -178,6 +178,23 @@ public class ALSProbeTest { } @Test + public void testWatchDogCompletesAwait() { + mProbe.enable(); + + AtomicInteger lux = new AtomicInteger(-9); + mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */); + + verify(mSensorManager).registerListener( + mSensorEventListenerCaptor.capture(), any(), anyInt()); + + moveTimeBy(TIMEOUT_MS); + + assertThat(lux.get()).isEqualTo(-1); + verify(mSensorManager).unregisterListener(any(SensorEventListener.class)); + verifyNoMoreInteractions(mSensorManager); + } + + @Test public void testNextLuxWhenAlreadyEnabledAndNotAvailable() { testNextLuxWhenAlreadyEnabled(false /* dataIsAvailable */); } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index a5c181d53286..606f4864643c 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -41,6 +41,7 @@ import android.hardware.biometrics.common.OperationContext; import android.hardware.biometrics.fingerprint.ISession; import android.hardware.biometrics.fingerprint.PointerContext; import android.hardware.fingerprint.Fingerprint; +import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.ISidefpsController; import android.hardware.fingerprint.IUdfpsOverlayController; @@ -73,6 +74,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.time.Clock; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -128,6 +130,8 @@ public class FingerprintAuthenticationClientTest { private ICancellationSignal mCancellationSignal; @Mock private Probe mLuxProbe; + @Mock + private Clock mClock; @Captor private ArgumentCaptor<OperationContext> mOperationContextCaptor; @Captor @@ -447,6 +451,52 @@ public class FingerprintAuthenticationClientTest { } @Test + public void sideFingerprintSkipsWindowIfVendorMessageMatch() throws Exception { + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + final int vendorAcquireMessage = 1234; + + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, + FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR); + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage, + vendorAcquireMessage); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + mLooper.dispatchAll(); + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, vendorAcquireMessage); + mLooper.dispatchAll(); + + verify(mCallback).onClientFinished(any(), eq(true)); + } + + @Test + public void sideFingerprintDoesNotSkipWindowOnVendorErrorMismatch() throws Exception { + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + final int vendorAcquireMessage = 1234; + + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, + FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR); + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage, + vendorAcquireMessage); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + mLooper.dispatchAll(); + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 1); + mLooper.dispatchAll(); + + verify(mCallback, never()).onClientFinished(any(), anyBoolean()); + } + + @Test public void sideFingerprintSendsAuthIfFingerUp() throws Exception { when(mSensorProps.isAnySidefpsType()).thenReturn(true); @@ -493,6 +543,79 @@ public class FingerprintAuthenticationClientTest { verify(mCallback).onClientFinished(any(), eq(true)); } + @Test + public void sideFingerprintPowerWindowStartsOnAcquireStart() throws Exception { + final int powerWindow = 500; + final long authStart = 300; + + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsBpPowerPressWindow, powerWindow); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + + // Acquire start occurs at time = 0ms + when(mClock.millis()).thenReturn(0L); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); + + // Auth occurs at time = 300 + when(mClock.millis()).thenReturn(authStart); + // At this point the delay should be 500 - (300 - 0) == 200 milliseconds. + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + mLooper.dispatchAll(); + verify(mCallback, never()).onClientFinished(any(), anyBoolean()); + + // After waiting 200 milliseconds, auth should succeed. + mLooper.moveTimeForward(powerWindow - authStart); + mLooper.dispatchAll(); + verify(mCallback).onClientFinished(any(), eq(true)); + } + + @Test + public void sideFingerprintPowerWindowStartsOnLastAcquireStart() throws Exception { + final int powerWindow = 500; + + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsBpPowerPressWindow, powerWindow); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + // Acquire start occurs at time = 0ms + when(mClock.millis()).thenReturn(0L); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); + + // Auth reject occurs at time = 300ms + when(mClock.millis()).thenReturn(300L); + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + false /* authenticated */, new ArrayList<>()); + mLooper.dispatchAll(); + + mLooper.moveTimeForward(300); + mLooper.dispatchAll(); + verify(mCallback, never()).onClientFinished(any(), anyBoolean()); + + when(mClock.millis()).thenReturn(1300L); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); + + // If code is correct, the new acquired start timestamp should be used + // and the code should only have to wait 500 - (1500-1300)ms. + when(mClock.millis()).thenReturn(1500L); + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + mLooper.dispatchAll(); + + mLooper.moveTimeForward(299); + mLooper.dispatchAll(); + verify(mCallback, never()).onClientFinished(any(), anyBoolean()); + + mLooper.moveTimeForward(1); + mLooper.dispatchAll(); + verify(mCallback).onClientFinished(any(), eq(true)); + } + private FingerprintAuthenticationClient createClient() throws RemoteException { return createClient(100 /* version */, true /* allowBackgroundAuthentication */); } @@ -520,7 +643,7 @@ public class FingerprintAuthenticationClientTest { null /* taskStackListener */, mLockoutCache, mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication, mSensorProps, - new Handler(mLooper.getLooper())) { + new Handler(mLooper.getLooper()), mClock) { @Override protected ActivityTaskManager getActivityTaskManager() { return mActivityTaskManager; diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java index 356600d84099..06422281ab25 100644 --- a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -885,6 +886,29 @@ public class BrightnessTrackerTest { assertNull(mInjector.mLightSensor); } + @Test + public void testOnlyOneReceiverRegistered() { + assertNull(mInjector.mLightSensor); + assertNull(mInjector.mSensorListener); + startTracker(mTracker, 0.3f, false); + + assertNotNull(mInjector.mLightSensor); + assertNotNull(mInjector.mSensorListener); + Sensor registeredLightSensor = mInjector.mLightSensor; + SensorEventListener registeredSensorListener = mInjector.mSensorListener; + + mTracker.start(0.3f); + assertSame(registeredLightSensor, mInjector.mLightSensor); + assertSame(registeredSensorListener, mInjector.mSensorListener); + + mTracker.stop(); + assertNull(mInjector.mLightSensor); + assertNull(mInjector.mSensorListener); + + // mInjector asserts that we aren't removing a null receiver + mTracker.stop(); + } + private InputStream getInputStream(String data) { return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); } 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 a719f526a1d8..30024fb5c221 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -49,6 +49,13 @@ import java.nio.file.Path; @Presubmit @RunWith(AndroidJUnit4.class) public final class DisplayDeviceConfigTest { + private static final int DEFAULT_PEAK_REFRESH_RATE = 75; + private static final int DEFAULT_REFRESH_RATE = 120; + private static final int[] LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{10, 30}; + private static final int[] LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{1, 21}; + private static final int[] HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{160}; + private static final int[] HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{30000}; + private DisplayDeviceConfig mDisplayDeviceConfig; private static final float ZERO_DELTA = 0.0f; private static final float SMALL_DELTA = 0.0001f; @@ -204,6 +211,16 @@ public final class DisplayDeviceConfigTest { assertArrayEquals(new float[]{29, 30, 31}, mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA); + assertEquals(mDisplayDeviceConfig.getDefaultRefreshRate(), DEFAULT_REFRESH_RATE); + assertEquals(mDisplayDeviceConfig.getDefaultPeakRefreshRate(), DEFAULT_PEAK_REFRESH_RATE); + assertArrayEquals(mDisplayDeviceConfig.getLowDisplayBrightnessThresholds(), + LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE); + assertArrayEquals(mDisplayDeviceConfig.getLowAmbientBrightnessThresholds(), + LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE); + assertArrayEquals(mDisplayDeviceConfig.getHighDisplayBrightnessThresholds(), + HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE); + assertArrayEquals(mDisplayDeviceConfig.getHighAmbientBrightnessThresholds(), + HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE); // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping, // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor. } @@ -465,6 +482,21 @@ public final class DisplayDeviceConfigTest { when(mResources.getIntArray(R.array.config_screenDarkeningThresholds)) .thenReturn(new int[]{370, 380, 390}); + // Configs related to refresh rates and blocking zones + when(mResources.getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate)) + .thenReturn(DEFAULT_PEAK_REFRESH_RATE); + when(mResources.getInteger(com.android.internal.R.integer.config_defaultRefreshRate)) + .thenReturn(DEFAULT_REFRESH_RATE); + when(mResources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate)) + .thenReturn(LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE); + when(mResources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate)) + .thenReturn(LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE); + when(mResources.getIntArray( + R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate)) + .thenReturn(HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE); + when(mResources.getIntArray( + R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate)) + .thenReturn(HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE); mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true); } diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java index 968e1d8c546b..18dd264558ac 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java @@ -1853,6 +1853,20 @@ public class DisplayModeDirectorTest { assertNull(vote); } + @Test + public void testNotifyDefaultDisplayDeviceUpdated() { + DisplayDeviceConfig displayDeviceConfig = mock(DisplayDeviceConfig.class); + when(displayDeviceConfig.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{}); + when(displayDeviceConfig.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{}); + when(displayDeviceConfig.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{}); + when(displayDeviceConfig.getHighAmbientBrightnessThresholds()).thenReturn(new int[]{}); + DisplayModeDirector director = + createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0); + director.defaultDisplayDeviceUpdated(displayDeviceConfig); + verify(displayDeviceConfig).getDefaultRefreshRate(); + verify(displayDeviceConfig).getDefaultPeakRefreshRate(); + } + private Temperature getSkinTemp(@Temperature.ThrottlingStatus int status) { return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status); } diff --git a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java index 9fe8609c85a1..3b0a22f80c30 100644 --- a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java @@ -275,6 +275,75 @@ public class PersistentDataStoreTest { assertNull(mDataStore.getBrightnessConfiguration(userSerial)); } + @Test + public void testStoreAndRestoreResolution() { + final String uniqueDisplayId = "test:123"; + DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) { + @Override + public boolean hasStableUniqueId() { + return true; + } + + @Override + public DisplayDeviceInfo getDisplayDeviceInfoLocked() { + return null; + } + }; + int width = 35; + int height = 45; + mDataStore.loadIfNeeded(); + mDataStore.setUserPreferredResolution(testDisplayDevice, width, height); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + mInjector.setWriteStream(baos); + mDataStore.saveIfNeeded(); + mTestLooper.dispatchAll(); + assertTrue(mInjector.wasWriteSuccessful()); + TestInjector newInjector = new TestInjector(); + PersistentDataStore newDataStore = new PersistentDataStore(newInjector); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + newInjector.setReadStream(bais); + newDataStore.loadIfNeeded(); + assertNotNull(newDataStore.getUserPreferredResolution(testDisplayDevice)); + assertEquals(35, newDataStore.getUserPreferredResolution(testDisplayDevice).x); + assertEquals(35, mDataStore.getUserPreferredResolution(testDisplayDevice).x); + assertEquals(45, newDataStore.getUserPreferredResolution(testDisplayDevice).y); + assertEquals(45, mDataStore.getUserPreferredResolution(testDisplayDevice).y); + } + + @Test + public void testStoreAndRestoreRefreshRate() { + final String uniqueDisplayId = "test:123"; + DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) { + @Override + public boolean hasStableUniqueId() { + return true; + } + + @Override + public DisplayDeviceInfo getDisplayDeviceInfoLocked() { + return null; + } + }; + float refreshRate = 85.3f; + mDataStore.loadIfNeeded(); + mDataStore.setUserPreferredRefreshRate(testDisplayDevice, refreshRate); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + mInjector.setWriteStream(baos); + mDataStore.saveIfNeeded(); + mTestLooper.dispatchAll(); + assertTrue(mInjector.wasWriteSuccessful()); + TestInjector newInjector = new TestInjector(); + PersistentDataStore newDataStore = new PersistentDataStore(newInjector); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + newInjector.setReadStream(bais); + newDataStore.loadIfNeeded(); + assertNotNull(newDataStore.getUserPreferredRefreshRate(testDisplayDevice)); + assertEquals(85.3f, mDataStore.getUserPreferredRefreshRate(testDisplayDevice), 01.f); + assertEquals(85.3f, newDataStore.getUserPreferredRefreshRate(testDisplayDevice), 0.1f); + } + public class TestInjector extends PersistentDataStore.Injector { private InputStream mReadStream; private OutputStream mWriteStream; diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java new file mode 100644 index 000000000000..303a370b0ba9 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java @@ -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.server.dreams; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.content.ServiceConnection; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.IRemoteCallback; +import android.os.RemoteException; +import android.os.test.TestLooper; +import android.service.dreams.IDreamService; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DreamControllerTest { + @Mock + private DreamController.Listener mListener; + @Mock + private Context mContext; + @Mock + private IBinder mIBinder; + @Mock + private IDreamService mIDreamService; + + @Captor + private ArgumentCaptor<ServiceConnection> mServiceConnectionACaptor; + @Captor + private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor; + + private final TestLooper mLooper = new TestLooper(); + private final Handler mHandler = new Handler(mLooper.getLooper()); + + private DreamController mDreamController; + + private Binder mToken; + private ComponentName mDreamName; + private ComponentName mOverlayName; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(mIDreamService.asBinder()).thenReturn(mIBinder); + when(mIBinder.queryLocalInterface(anyString())).thenReturn(mIDreamService); + when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true); + + mToken = new Binder(); + mDreamName = ComponentName.unflattenFromString("dream"); + mOverlayName = ComponentName.unflattenFromString("dream_overlay"); + mDreamController = new DreamController(mContext, mHandler, mListener); + } + + @Test + public void startDream_attachOnServiceConnected() throws RemoteException { + // Call dream controller to start dreaming. + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + + // Mock service connected. + final ServiceConnection serviceConnection = captureServiceConnection(); + serviceConnection.onServiceConnected(mDreamName, mIBinder); + mLooper.dispatchAll(); + + // Verify that dream service is called to attach. + verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/, any()); + } + + @Test + public void startDream_startASecondDream_detachOldDreamOnceNewDreamIsStarted() + throws RemoteException { + // Start first dream. + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + captureServiceConnection().onServiceConnected(mDreamName, mIBinder); + mLooper.dispatchAll(); + clearInvocations(mContext); + + // Set up second dream. + final Binder newToken = new Binder(); + final ComponentName newDreamName = ComponentName.unflattenFromString("new_dream"); + final ComponentName newOverlayName = ComponentName.unflattenFromString("new_dream_overlay"); + final IDreamService newDreamService = mock(IDreamService.class); + final IBinder newBinder = mock(IBinder.class); + when(newDreamService.asBinder()).thenReturn(newBinder); + when(newBinder.queryLocalInterface(anyString())).thenReturn(newDreamService); + + // Start second dream. + mDreamController.startDream(newToken, newDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, newOverlayName, "test" /*reason*/); + captureServiceConnection().onServiceConnected(newDreamName, newBinder); + mLooper.dispatchAll(); + + // Mock second dream started. + verify(newDreamService).attach(eq(newToken), eq(false) /*doze*/, + mRemoteCallbackCaptor.capture()); + mRemoteCallbackCaptor.getValue().sendResult(null /*data*/); + mLooper.dispatchAll(); + + // Verify that the first dream is called to detach. + verify(mIDreamService).detach(); + } + + @Test + public void stopDream_detachFromService() throws RemoteException { + // Start dream. + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + captureServiceConnection().onServiceConnected(mDreamName, mIBinder); + mLooper.dispatchAll(); + + // Stop dream. + mDreamController.stopDream(true /*immediate*/, "test stop dream" /*reason*/); + + // Verify that dream service is called to detach. + verify(mIDreamService).detach(); + } + + private ServiceConnection captureServiceConnection() { + verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(), + any()); + return mServiceConnectionACaptor.getValue(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java index 235849c1cd8b..c484f457faea 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java @@ -53,7 +53,8 @@ final class FakeVibratorControllerProvider { private boolean mIsAvailable = true; private boolean mIsInfoLoadSuccessful = true; - private long mLatency; + private long mOnLatency; + private long mOffLatency; private int mOffCount; private int mCapabilities; @@ -97,7 +98,7 @@ final class FakeVibratorControllerProvider { public long on(long milliseconds, long vibrationId) { recordEffectSegment(vibrationId, new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, /* frequencyHz= */ 0, (int) milliseconds)); - applyLatency(); + applyLatency(mOnLatency); scheduleListener(milliseconds, vibrationId); return milliseconds; } @@ -105,12 +106,13 @@ final class FakeVibratorControllerProvider { @Override public void off() { mOffCount++; + applyLatency(mOffLatency); } @Override public void setAmplitude(float amplitude) { mAmplitudes.add(amplitude); - applyLatency(); + applyLatency(mOnLatency); } @Override @@ -121,7 +123,7 @@ final class FakeVibratorControllerProvider { } recordEffectSegment(vibrationId, new PrebakedSegment((int) effect, false, (int) strength)); - applyLatency(); + applyLatency(mOnLatency); scheduleListener(EFFECT_DURATION, vibrationId); return EFFECT_DURATION; } @@ -141,7 +143,7 @@ final class FakeVibratorControllerProvider { duration += EFFECT_DURATION + primitive.getDelay(); recordEffectSegment(vibrationId, primitive); } - applyLatency(); + applyLatency(mOnLatency); scheduleListener(duration, vibrationId); return duration; } @@ -154,7 +156,7 @@ final class FakeVibratorControllerProvider { recordEffectSegment(vibrationId, primitive); } recordBraking(vibrationId, braking); - applyLatency(); + applyLatency(mOnLatency); scheduleListener(duration, vibrationId); return duration; } @@ -193,10 +195,10 @@ final class FakeVibratorControllerProvider { return mIsInfoLoadSuccessful; } - private void applyLatency() { + private void applyLatency(long latencyMillis) { try { - if (mLatency > 0) { - Thread.sleep(mLatency); + if (latencyMillis > 0) { + Thread.sleep(latencyMillis); } } catch (InterruptedException e) { } @@ -240,10 +242,15 @@ final class FakeVibratorControllerProvider { /** * Sets the latency this controller should fake for turning the vibrator hardware on or setting - * it's vibration amplitude. + * the vibration amplitude. */ - public void setLatency(long millis) { - mLatency = millis; + public void setOnLatency(long millis) { + mOnLatency = millis; + } + + /** Sets the latency this controller should fake for turning the vibrator off. */ + public void setOffLatency(long millis) { + mOffLatency = millis; } /** Set the capabilities of the fake vibrator hardware. */ diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java index 0551bfc70bda..42a2c10a24d6 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java @@ -1118,7 +1118,7 @@ public class VibrationThreadTest { // 25% of the first waveform step will be spent on the native on() call. // 25% of each waveform step will be spent on the native setAmplitude() call.. - mVibratorProviders.get(VIBRATOR_ID).setLatency(stepDuration / 4); + mVibratorProviders.get(VIBRATOR_ID).setOnLatency(stepDuration / 4); mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); int stepCount = totalDuration / stepDuration; @@ -1149,7 +1149,7 @@ public class VibrationThreadTest { fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); long latency = 5_000; // 5s - fakeVibrator.setLatency(latency); + fakeVibrator.setOnLatency(latency); long vibrationId = 1; VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); @@ -1163,8 +1163,7 @@ public class VibrationThreadTest { // fail at waitForCompletion(cancellingThread). Thread cancellingThread = new Thread( () -> conductor.notifyCancelled( - new Vibration.EndInfo( - Vibration.Status.CANCELLED_BY_USER), + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER), /* immediate= */ false)); cancellingThread.start(); diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java index fe0a79c2d944..039e159a347b 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -817,6 +817,39 @@ public class VibratorManagerServiceTest { verify(mBatteryStatsMock).noteVibratorOn(UID, 5000); // The second vibration shouldn't have recorded that the vibrators were turned on. verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong()); + // No segment played is the prebaked CLICK from the second vibration. + assertFalse(mVibratorProviders.get(1).getAllEffectSegments().stream() + .anyMatch(PrebakedSegment.class::isInstance)); + // Clean up repeating effect. + service.cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, service); + } + + @Test + public void vibrate_withOngoingRepeatingVibrationBeingCancelled_playsAfterPreviousIsCancelled() + throws Exception { + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setOffLatency(50); // Add latency so cancellation is slow. + fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); + VibratorManagerService service = createSystemReadyService(); + + VibrationEffect repeatingEffect = VibrationEffect.createWaveform( + new long[]{10, 10_000}, new int[]{255, 0}, 1); + vibrate(service, repeatingEffect, ALARM_ATTRS); + + // VibrationThread will start this vibration async, wait until the off waveform step. + assertTrue(waitUntil(s -> fakeVibrator.getOffCount() > 0, service, TEST_TIMEOUT_MILLIS)); + + // Cancel vibration right before requesting a new one. + // This should trigger slow IVibrator.off before setting the vibration status to cancelled. + service.cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, service); + vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), + ALARM_ATTRS); + + // Check that second vibration was played. + assertTrue(fakeVibrator.getAllEffectSegments().stream() + .anyMatch(PrebakedSegment.class::isInstance)); } @Test @@ -867,6 +900,11 @@ public class VibratorManagerServiceTest { // The second vibration shouldn't have recorded that the vibrators were turned on. verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong()); + // The second vibration shouldn't have played any prebaked segment. + assertFalse(mVibratorProviders.get(1).getAllEffectSegments().stream() + .anyMatch(PrebakedSegment.class::isInstance)); + // Clean up long effect. + service.cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, service); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java index 91c2fe0eb262..8e81e2d8997c 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java @@ -1371,6 +1371,39 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); } + @Test + public void dreamWhenDocked_ambientModeSuppressed_suppressionEnabled() { + mUiManagerService.setStartDreamImmediatelyOnDock(true); + mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true); + + when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true); + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mInjector, never()).startDreamWhenDockedIfAppropriate(mContext); + } + + @Test + public void dreamWhenDocked_ambientModeSuppressed_suppressionDisabled() { + mUiManagerService.setStartDreamImmediatelyOnDock(true); + mUiManagerService.setDreamsDisabledByAmbientModeSuppression(false); + + when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true); + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); + } + + @Test + public void dreamWhenDocked_ambientModeNotSuppressed_suppressionEnabled() { + mUiManagerService.setStartDreamImmediatelyOnDock(true); + mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true); + + when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(false); + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); + } + private void triggerDockIntent() { final Intent dockedIntent = new Intent(Intent.ACTION_DOCK_EVENT) diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java index c5131c8f8c1d..bf66dee936fe 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java @@ -15,6 +15,7 @@ */ package com.android.server.notification; +import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; @@ -30,9 +31,11 @@ import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.intThat; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -49,6 +52,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.content.pm.VersionedPackage; +import android.content.res.Resources; import android.os.Bundle; import android.os.UserHandle; import android.service.notification.NotificationListenerFilter; @@ -69,6 +73,7 @@ import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.internal.util.reflection.FieldSetter; @@ -77,6 +82,7 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.util.Arrays; import java.util.List; public class NotificationListenersTest extends UiServiceTestCase { @@ -85,6 +91,8 @@ public class NotificationListenersTest extends UiServiceTestCase { private PackageManager mPm; @Mock private IPackageManager miPm; + @Mock + private Resources mResources; @Mock NotificationManagerService mNm; @@ -96,7 +104,8 @@ public class NotificationListenersTest extends UiServiceTestCase { private ComponentName mCn1 = new ComponentName("pkg", "pkg.cmp"); private ComponentName mCn2 = new ComponentName("pkg2", "pkg2.cmp2"); - + private ComponentName mUninstalledComponent = new ComponentName("pkg3", + "pkg3.NotificationListenerService"); @Before public void setUp() throws Exception { @@ -111,7 +120,7 @@ public class NotificationListenersTest extends UiServiceTestCase { @Test public void testReadExtraTag() throws Exception { - String xml = "<" + TAG_REQUESTED_LISTENERS+ ">" + String xml = "<" + TAG_REQUESTED_LISTENERS + ">" + "<listener component=\"" + mCn1.flattenToString() + "\" user=\"0\">" + "<allowed types=\"7\" />" + "</listener>" @@ -131,11 +140,55 @@ public class NotificationListenersTest extends UiServiceTestCase { } @Test + public void loadDefaultsFromConfig_forHeadlessSystemUser_loadUninstalled() throws Exception { + // setup with headless system user mode + mListeners = spy(mNm.new NotificationListeners( + mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm, + /* isHeadlessSystemUserMode= */ true)); + mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent); + + mListeners.loadDefaultsFromConfig(); + + assertThat(mListeners.getDefaultComponents()).contains(mUninstalledComponent); + } + + @Test + public void loadDefaultsFromConfig_forNonHeadlessSystemUser_ignoreUninstalled() + throws Exception { + // setup without headless system user mode + mListeners = spy(mNm.new NotificationListeners( + mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm, + /* isHeadlessSystemUserMode= */ false)); + mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent); + + mListeners.loadDefaultsFromConfig(); + + assertThat(mListeners.getDefaultComponents()).doesNotContain(mUninstalledComponent); + } + + private void mockDefaultListenerConfigForUninstalledComponent(ComponentName componentName) { + ArraySet<ComponentName> components = new ArraySet<>(Arrays.asList(componentName)); + when(mResources + .getString( + com.android.internal.R.string.config_defaultListenerAccessPackages)) + .thenReturn(componentName.getPackageName()); + when(mContext.getResources()).thenReturn(mResources); + doReturn(components).when(mListeners).queryPackageForServices( + eq(componentName.getPackageName()), + intThat(hasIntBitFlag(MATCH_ANY_USER)), + anyInt()); + } + + public static ArgumentMatcher<Integer> hasIntBitFlag(int flag) { + return arg -> arg != null && ((arg & flag) == flag); + } + + @Test public void testWriteExtraTag() throws Exception { NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>()); VersionedPackage a1 = new VersionedPackage("pkg1", 243); NotificationListenerFilter nlf2 = - new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1})); + new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[]{a1})); mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf); mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2); @@ -435,63 +488,112 @@ public class NotificationListenersTest extends UiServiceTestCase { @Test public void testNotifyPostedLockedInLockdownMode() { - NotificationRecord r = mock(NotificationRecord.class); - NotificationRecord old = mock(NotificationRecord.class); - - // before the lockdown mode - when(mNm.isInLockDownMode()).thenReturn(false); - mListeners.notifyPostedLocked(r, old, true); - mListeners.notifyPostedLocked(r, old, false); - verify(r, atLeast(2)).getSbn(); - - // in the lockdown mode - reset(r); - reset(old); - when(mNm.isInLockDownMode()).thenReturn(true); - mListeners.notifyPostedLocked(r, old, true); - mListeners.notifyPostedLocked(r, old, false); - verify(r, never()).getSbn(); - } - - @Test - public void testnotifyRankingUpdateLockedInLockdownMode() { - List chn = mock(List.class); - - // before the lockdown mode - when(mNm.isInLockDownMode()).thenReturn(false); - mListeners.notifyRankingUpdateLocked(chn); - verify(chn, atLeast(1)).size(); - - // in the lockdown mode - reset(chn); - when(mNm.isInLockDownMode()).thenReturn(true); - mListeners.notifyRankingUpdateLocked(chn); - verify(chn, never()).size(); + NotificationRecord r0 = mock(NotificationRecord.class); + NotificationRecord old0 = mock(NotificationRecord.class); + UserHandle uh0 = mock(UserHandle.class); + + NotificationRecord r1 = mock(NotificationRecord.class); + NotificationRecord old1 = mock(NotificationRecord.class); + UserHandle uh1 = mock(UserHandle.class); + + // Neither user0 and user1 is in the lockdown mode + when(r0.getUser()).thenReturn(uh0); + when(uh0.getIdentifier()).thenReturn(0); + when(mNm.isInLockDownMode(0)).thenReturn(false); + + when(r1.getUser()).thenReturn(uh1); + when(uh1.getIdentifier()).thenReturn(1); + when(mNm.isInLockDownMode(1)).thenReturn(false); + + mListeners.notifyPostedLocked(r0, old0, true); + mListeners.notifyPostedLocked(r0, old0, false); + verify(r0, atLeast(2)).getSbn(); + + mListeners.notifyPostedLocked(r1, old1, true); + mListeners.notifyPostedLocked(r1, old1, false); + verify(r1, atLeast(2)).getSbn(); + + // Reset + reset(r0); + reset(old0); + reset(r1); + reset(old1); + + // Only user 0 is in the lockdown mode + when(r0.getUser()).thenReturn(uh0); + when(uh0.getIdentifier()).thenReturn(0); + when(mNm.isInLockDownMode(0)).thenReturn(true); + + when(r1.getUser()).thenReturn(uh1); + when(uh1.getIdentifier()).thenReturn(1); + when(mNm.isInLockDownMode(1)).thenReturn(false); + + mListeners.notifyPostedLocked(r0, old0, true); + mListeners.notifyPostedLocked(r0, old0, false); + verify(r0, never()).getSbn(); + + mListeners.notifyPostedLocked(r1, old1, true); + mListeners.notifyPostedLocked(r1, old1, false); + verify(r1, atLeast(2)).getSbn(); } @Test public void testNotifyRemovedLockedInLockdownMode() throws NoSuchFieldException { - NotificationRecord r = mock(NotificationRecord.class); - NotificationStats rs = mock(NotificationStats.class); + NotificationRecord r0 = mock(NotificationRecord.class); + NotificationStats rs0 = mock(NotificationStats.class); + UserHandle uh0 = mock(UserHandle.class); + + NotificationRecord r1 = mock(NotificationRecord.class); + NotificationStats rs1 = mock(NotificationStats.class); + UserHandle uh1 = mock(UserHandle.class); + StatusBarNotification sbn = mock(StatusBarNotification.class); FieldSetter.setField(mNm, NotificationManagerService.class.getDeclaredField("mHandler"), mock(NotificationManagerService.WorkerHandler.class)); - // before the lockdown mode - when(mNm.isInLockDownMode()).thenReturn(false); - when(r.getSbn()).thenReturn(sbn); - mListeners.notifyRemovedLocked(r, 0, rs); - mListeners.notifyRemovedLocked(r, 0, rs); - verify(r, atLeast(2)).getSbn(); - - // in the lockdown mode - reset(r); - reset(rs); - when(mNm.isInLockDownMode()).thenReturn(true); - when(r.getSbn()).thenReturn(sbn); - mListeners.notifyRemovedLocked(r, 0, rs); - mListeners.notifyRemovedLocked(r, 0, rs); - verify(r, never()).getSbn(); + // Neither user0 and user1 is in the lockdown mode + when(r0.getUser()).thenReturn(uh0); + when(uh0.getIdentifier()).thenReturn(0); + when(mNm.isInLockDownMode(0)).thenReturn(false); + when(r0.getSbn()).thenReturn(sbn); + + when(r1.getUser()).thenReturn(uh1); + when(uh1.getIdentifier()).thenReturn(1); + when(mNm.isInLockDownMode(1)).thenReturn(false); + when(r1.getSbn()).thenReturn(sbn); + + mListeners.notifyRemovedLocked(r0, 0, rs0); + mListeners.notifyRemovedLocked(r0, 0, rs0); + verify(r0, atLeast(2)).getSbn(); + + mListeners.notifyRemovedLocked(r1, 0, rs1); + mListeners.notifyRemovedLocked(r1, 0, rs1); + verify(r1, atLeast(2)).getSbn(); + + // Reset + reset(r0); + reset(rs0); + reset(r1); + reset(rs1); + + // Only user 0 is in the lockdown mode + when(r0.getUser()).thenReturn(uh0); + when(uh0.getIdentifier()).thenReturn(0); + when(mNm.isInLockDownMode(0)).thenReturn(true); + when(r0.getSbn()).thenReturn(sbn); + + when(r1.getUser()).thenReturn(uh1); + when(uh1.getIdentifier()).thenReturn(1); + when(mNm.isInLockDownMode(1)).thenReturn(false); + when(r1.getSbn()).thenReturn(sbn); + + mListeners.notifyRemovedLocked(r0, 0, rs0); + mListeners.notifyRemovedLocked(r0, 0, rs0); + verify(r0, never()).getSbn(); + + mListeners.notifyRemovedLocked(r1, 0, rs1); + mListeners.notifyRemovedLocked(r1, 0, rs1); + verify(r1, atLeast(2)).getSbn(); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index a545c8344c27..beaa6e066d7d 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -174,6 +174,7 @@ import android.service.notification.Adjustment; import android.service.notification.ConversationChannelWrapper; import android.service.notification.NotificationListenerFilter; import android.service.notification.NotificationListenerService; +import android.service.notification.NotificationRankingUpdate; import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.service.notification.ZenPolicy; @@ -9781,10 +9782,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mStrongAuthTracker.setGetStrongAuthForUserReturnValue( STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId()); - assertTrue(mStrongAuthTracker.isInLockDownMode()); - mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0); + assertTrue(mStrongAuthTracker.isInLockDownMode(mContext.getUserId())); + mStrongAuthTracker.setGetStrongAuthForUserReturnValue(mContext.getUserId()); mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId()); - assertFalse(mStrongAuthTracker.isInLockDownMode()); + assertFalse(mStrongAuthTracker.isInLockDownMode(mContext.getUserId())); } @Test @@ -9800,8 +9801,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // when entering the lockdown mode, cancel the 2 notifications. mStrongAuthTracker.setGetStrongAuthForUserReturnValue( STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); - mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId()); - assertTrue(mStrongAuthTracker.isInLockDownMode()); + mStrongAuthTracker.onStrongAuthRequiredChanged(0); + assertTrue(mStrongAuthTracker.isInLockDownMode(0)); // the notifyRemovedLocked function is called twice due to REASON_CANCEL_ALL. ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class); @@ -9810,10 +9811,46 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // exit lockdown mode. mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0); - mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId()); + mStrongAuthTracker.onStrongAuthRequiredChanged(0); + assertFalse(mStrongAuthTracker.isInLockDownMode(0)); // the notifyPostedLocked function is called twice. - verify(mListeners, times(2)).notifyPostedLocked(any(), any()); + verify(mWorkerHandler, times(2)).postDelayed(any(Runnable.class), anyLong()); + //verify(mListeners, times(2)).notifyPostedLocked(any(), any()); + } + + @Test + public void testMakeRankingUpdateLockedInLockDownMode() { + // post 2 notifications from a same package + NotificationRecord pkgA = new NotificationRecord(mContext, + generateSbn("a", 1000, 9, 0), mTestNotificationChannel); + mService.addNotification(pkgA); + NotificationRecord pkgB = new NotificationRecord(mContext, + generateSbn("a", 1000, 9, 1), mTestNotificationChannel); + mService.addNotification(pkgB); + + mService.setIsVisibleToListenerReturnValue(true); + NotificationRankingUpdate nru = mService.makeRankingUpdateLocked(null); + assertEquals(2, nru.getRankingMap().getOrderedKeys().length); + + // when only user 0 entering the lockdown mode, its notification will be suppressed. + mStrongAuthTracker.setGetStrongAuthForUserReturnValue( + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); + mStrongAuthTracker.onStrongAuthRequiredChanged(0); + assertTrue(mStrongAuthTracker.isInLockDownMode(0)); + assertFalse(mStrongAuthTracker.isInLockDownMode(1)); + + nru = mService.makeRankingUpdateLocked(null); + assertEquals(1, nru.getRankingMap().getOrderedKeys().length); + + // User 0 exits lockdown mode. Its notification will be resumed. + mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0); + mStrongAuthTracker.onStrongAuthRequiredChanged(0); + assertFalse(mStrongAuthTracker.isInLockDownMode(0)); + assertFalse(mStrongAuthTracker.isInLockDownMode(1)); + + nru = mService.makeRankingUpdateLocked(null); + assertEquals(2, nru.getRankingMap().getOrderedKeys().length); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java deleted file mode 100644 index d7650420788c..000000000000 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java +++ /dev/null @@ -1,551 +0,0 @@ -/* - * Copyright (C) 2017 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.notification; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertNull; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import android.app.ActivityManager; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Person; -import android.app.RemoteInput; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Typeface; -import android.graphics.drawable.Icon; -import android.net.Uri; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.style.StyleSpan; -import android.util.Pair; -import android.widget.RemoteViews; - -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.server.UiServiceTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class NotificationTest extends UiServiceTestCase { - - @Mock - ActivityManager mAm; - - @Mock - Resources mResources; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testDoesNotStripsExtenders() { - Notification.Builder nb = new Notification.Builder(mContext, "channel"); - nb.extend(new Notification.CarExtender().setColor(Color.RED)); - nb.extend(new Notification.TvExtender().setChannelId("different channel")); - nb.extend(new Notification.WearableExtender().setDismissalId("dismiss")); - Notification before = nb.build(); - Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before); - - assertTrue(before == after); - - assertEquals("different channel", new Notification.TvExtender(before).getChannelId()); - assertEquals(Color.RED, new Notification.CarExtender(before).getColor()); - assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId()); - } - - @Test - public void testStyleChangeVisiblyDifferent_noStyles() { - Notification.Builder n1 = new Notification.Builder(mContext, "test"); - Notification.Builder n2 = new Notification.Builder(mContext, "test"); - - assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testStyleChangeVisiblyDifferent_noStyleToStyle() { - Notification.Builder n1 = new Notification.Builder(mContext, "test"); - Notification.Builder n2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle()); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testStyleChangeVisiblyDifferent_styleToNoStyle() { - Notification.Builder n2 = new Notification.Builder(mContext, "test"); - Notification.Builder n1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle()); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testStyleChangeVisiblyDifferent_changeStyle() { - Notification.Builder n1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.InboxStyle()); - Notification.Builder n2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle()); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testInboxTextChange() { - Notification.Builder nInbox1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.InboxStyle().addLine("a").addLine("b")); - Notification.Builder nInbox2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.InboxStyle().addLine("b").addLine("c")); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2)); - } - - @Test - public void testBigTextTextChange() { - Notification.Builder nBigText1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle().bigText("something")); - Notification.Builder nBigText2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle().bigText("else")); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2)); - } - - @Test - public void testBigPictureChange() { - Bitmap bitA = mock(Bitmap.class); - when(bitA.getGenerationId()).thenReturn(100); - Bitmap bitB = mock(Bitmap.class); - when(bitB.getGenerationId()).thenReturn(200); - - Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigPictureStyle().bigPicture(bitA)); - Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigPictureStyle().bigPicture(bitB)); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2)); - } - - @Test - public void testMessagingChange_text() { - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class))) - .addMessage(new Notification.MessagingStyle.Message( - "b", 100, mock(Person.class))) - ); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_data() { - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)) - .setData("text", mock(Uri.class)))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)))); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_sender() { - Person a = mock(Person.class); - when(a.getName()).thenReturn("A"); - Person b = mock(Person.class); - when(b.getName()).thenReturn("b"); - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_key() { - Person a = mock(Person.class); - when(a.getKey()).thenReturn("A"); - Person b = mock(Person.class); - when(b.getKey()).thenReturn("b"); - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_ignoreTimeChange() { - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 1000, mock(Person.class))) - ); - - assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testRemoteViews_nullChange() { - Notification.Builder n1 = new Notification.Builder(mContext, "test") - .setContent(mock(RemoteViews.class)); - Notification.Builder n2 = new Notification.Builder(mContext, "test"); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test"); - n2 = new Notification.Builder(mContext, "test") - .setContent(mock(RemoteViews.class)); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test") - .setCustomBigContentView(mock(RemoteViews.class)); - n2 = new Notification.Builder(mContext, "test"); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test"); - n2 = new Notification.Builder(mContext, "test") - .setCustomBigContentView(mock(RemoteViews.class)); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test"); - n2 = new Notification.Builder(mContext, "test"); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_layoutChange() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(189); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_layoutSame() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(234); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_sequenceChange() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - when(a.getSequenceNumber()).thenReturn(1); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(234); - when(b.getSequenceNumber()).thenReturn(2); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_sequenceSame() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - when(a.getSequenceNumber()).thenReturn(1); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(234); - when(b.getSequenceNumber()).thenReturn(1); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testActionsDifferent_null() { - Notification n1 = new Notification.Builder(mContext, "test") - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentSame() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentText() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) - .build(); - - assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentSpannables() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, - new SpannableStringBuilder().append("test1", - new StyleSpan(Typeface.BOLD), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE), - intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "test1", intent).build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentNumber() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) - .build(); - - assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentIntent() { - PendingIntent intent1 = mock(PendingIntent.class); - PendingIntent intent2 = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsIgnoresRemoteInputs() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(new RemoteInput.Builder("a") - .setChoices(new CharSequence[] {"i", "m"}) - .build()) - .build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(new RemoteInput.Builder("a") - .setChoices(new CharSequence[] {"t", "m"}) - .build()) - .build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testFreeformRemoteInputActionPair_noRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - Notification notification = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .build()) - .build(); - assertNull(notification.findRemoteInputActionPair(false)); - } - - @Test - public void testFreeformRemoteInputActionPair_hasRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - RemoteInput remoteInput = new RemoteInput.Builder("a").build(); - - Notification.Action actionWithRemoteInput = - new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(remoteInput) - .addRemoteInput(remoteInput) - .build(); - - Notification.Action actionWithoutRemoteInput = - new Notification.Action.Builder(icon, "TEXT 2", intent) - .build(); - - Notification notification = new Notification.Builder(mContext, "test") - .addAction(actionWithoutRemoteInput) - .addAction(actionWithRemoteInput) - .build(); - - Pair<RemoteInput, Notification.Action> remoteInputActionPair = - notification.findRemoteInputActionPair(false); - - assertNotNull(remoteInputActionPair); - assertEquals(remoteInput, remoteInputActionPair.first); - assertEquals(actionWithRemoteInput, remoteInputActionPair.second); - } - - @Test - public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - Notification notification = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput( - new RemoteInput.Builder("a") - .setAllowFreeFormInput(false).build()) - .build()) - .build(); - assertNull(notification.findRemoteInputActionPair(true)); - } - - @Test - public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - RemoteInput remoteInput = - new RemoteInput.Builder("a").setAllowFreeFormInput(false).build(); - RemoteInput freeformRemoteInput = - new RemoteInput.Builder("b").setAllowFreeFormInput(true).build(); - - Notification.Action actionWithFreeformRemoteInput = - new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(remoteInput) - .addRemoteInput(freeformRemoteInput) - .build(); - - Notification.Action actionWithoutFreeformRemoteInput = - new Notification.Action.Builder(icon, "TEXT 2", intent) - .addRemoteInput(remoteInput) - .build(); - - Notification notification = new Notification.Builder(mContext, "test") - .addAction(actionWithoutFreeformRemoteInput) - .addAction(actionWithFreeformRemoteInput) - .build(); - - Pair<RemoteInput, Notification.Action> remoteInputActionPair = - notification.findRemoteInputActionPair(true); - - assertNotNull(remoteInputActionPair); - assertEquals(freeformRemoteInput, remoteInputActionPair.first); - assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second); - } -} - diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java index b49e5cbfa9dc..8cf74fbf88b7 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java @@ -19,10 +19,12 @@ package com.android.server.notification; import android.companion.ICompanionDeviceManager; import android.content.ComponentName; import android.content.Context; +import android.service.notification.StatusBarNotification; import androidx.annotation.Nullable; import com.android.internal.logging.InstanceIdSequence; +import com.android.server.notification.ManagedServices.ManagedServiceInfo; import java.util.HashSet; import java.util.Set; @@ -37,6 +39,9 @@ public class TestableNotificationManagerService extends NotificationManagerServi @Nullable NotificationAssistantAccessGrantedCallback mNotificationAssistantAccessGrantedCallback; + @Nullable + Boolean mIsVisibleToListenerReturnValue = null; + TestableNotificationManagerService(Context context, NotificationRecordLogger logger, InstanceIdSequence notificationInstanceIdSequence) { super(context, logger, notificationInstanceIdSequence); @@ -119,6 +124,19 @@ public class TestableNotificationManagerService extends NotificationManagerServi mShowReviewPermissionsNotification = setting; } + protected void setIsVisibleToListenerReturnValue(boolean value) { + mIsVisibleToListenerReturnValue = value; + } + + @Override + boolean isVisibleToListener(StatusBarNotification sbn, int notificationType, + ManagedServiceInfo listener) { + if (mIsVisibleToListenerReturnValue != null) { + return mIsVisibleToListenerReturnValue; + } + return super.isVisibleToListener(sbn, notificationType, listener); + } + public class StrongAuthTrackerFake extends NotificationManagerService.StrongAuthTracker { private int mGetStrongAuthForUserReturnValue = 0; StrongAuthTrackerFake(Context context) { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java index d5e336b1cf2f..eed32d7d815c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java @@ -40,14 +40,18 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; +import android.app.ActivityOptions; import android.app.WaitResult; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.os.Binder; import android.os.ConditionVariable; +import android.os.IBinder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import android.view.Display; @@ -308,4 +312,40 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase { waitHandlerIdle(mAtm.mH); verify(mRootWindowContainer, timeout(TIMEOUT_MS)).startHomeOnEmptyDisplays("userUnlocked"); } + + /** Verifies that launch from recents sets the launch cookie on the activity. */ + @Test + public void testStartActivityFromRecents_withLaunchCookie() { + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + + IBinder launchCookie = new Binder("test_launch_cookie"); + ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchCookie(launchCookie); + SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(options.toBundle()); + + doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(), + anyInt(), any()); + + mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions); + + assertThat(activity.mLaunchCookie).isEqualTo(launchCookie); + verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions)); + } + + /** Verifies that launch from recents doesn't set the launch cookie on the activity. */ + @Test + public void testStartActivityFromRecents_withoutLaunchCookie() { + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + + SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle( + ActivityOptions.makeBasic().toBundle()); + + doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(), + anyInt(), any()); + + mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions); + + assertThat(activity.mLaunchCookie).isNull(); + verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions)); + } } 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 1cd0b198ff5a..e30e5dbcaf46 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -242,7 +242,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { private IOnBackInvokedCallback createOnBackInvokedCallback() { return new IOnBackInvokedCallback.Stub() { @Override - public void onBackStarted() { + public void onBackStarted(BackEvent backEvent) { } @Override 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 0b23359627fb..4202f46c188c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -56,6 +56,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -91,6 +92,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; import java.util.List; @@ -762,6 +764,50 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test + public void testOrganizerRemovedWithPendingEvents() { + final TaskFragment tf0 = new TaskFragmentBuilder(mAtm) + .setCreateParentTask() + .setOrganizer(mOrganizer) + .setFragmentToken(mFragmentToken) + .build(); + final TaskFragment tf1 = new TaskFragmentBuilder(mAtm) + .setCreateParentTask() + .setOrganizer(mOrganizer) + .setFragmentToken(new Binder()) + .build(); + assertTrue(tf0.isOrganizedTaskFragment()); + assertTrue(tf1.isOrganizedTaskFragment()); + assertTrue(tf0.isAttached()); + assertTrue(tf0.isAttached()); + + // Mock the behavior that remove TaskFragment can trigger event dispatch. + final Answer<Void> removeImmediately = invocation -> { + invocation.callRealMethod(); + mController.dispatchPendingEvents(); + return null; + }; + doAnswer(removeImmediately).when(tf0).removeImmediately(); + doAnswer(removeImmediately).when(tf1).removeImmediately(); + + // Add pending events. + mController.onTaskFragmentAppeared(mIOrganizer, tf0); + mController.onTaskFragmentAppeared(mIOrganizer, tf1); + + // Remove organizer. + mController.unregisterOrganizer(mIOrganizer); + mController.dispatchPendingEvents(); + + // Nothing should happen after the organizer is removed. + verify(mOrganizer, never()).onTransactionReady(any()); + + // TaskFragments should be removed. + assertFalse(tf0.isOrganizedTaskFragment()); + assertFalse(tf1.isOrganizedTaskFragment()); + assertFalse(tf0.isAttached()); + assertFalse(tf0.isAttached()); + } + + @Test public void testTaskFragmentInPip_startActivityInTaskFragment() { setupTaskFragmentInPip(); final ActivityRecord activity = mTaskFragment.getTopMostActivity(); @@ -874,29 +920,87 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { @Test public void testDeferPendingTaskFragmentEventsOfInvisibleTask() { - // Task - TaskFragment - Activity. final Task task = createTask(mDisplayContent); final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) .setParentTask(task) .setOrganizer(mOrganizer) .setFragmentToken(mFragmentToken) .build(); - - // Mock the task to invisible doReturn(false).when(task).shouldBeVisible(any()); - // Sending events - taskFragment.mTaskFragmentAppearedSent = true; - mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + // Dispatch the initial event in the Task to update the Task visibility to the organizer. + mController.onTaskFragmentAppeared(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); + verify(mOrganizer).onTransactionReady(any()); - // Verifies that event was not sent + // Verify that events were not sent when the Task is in background. + clearInvocations(mOrganizer); + final Rect bounds = new Rect(0, 0, 500, 1000); + task.setBoundsUnchecked(bounds); + mController.onTaskFragmentParentInfoChanged(mIOrganizer, task); + mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + mController.dispatchPendingEvents(); verify(mOrganizer, never()).onTransactionReady(any()); + + // Verify that the events were sent when the Task becomes visible. + doReturn(true).when(task).shouldBeVisible(any()); + task.lastActiveTime++; + mController.dispatchPendingEvents(); + verify(mOrganizer).onTransactionReady(any()); + } + + @Test + public void testSendAllPendingTaskFragmentEventsWhenAnyTaskIsVisible() { + // Invisible Task. + final Task invisibleTask = createTask(mDisplayContent); + final TaskFragment invisibleTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(invisibleTask) + .setOrganizer(mOrganizer) + .setFragmentToken(mFragmentToken) + .build(); + doReturn(false).when(invisibleTask).shouldBeVisible(any()); + + // Visible Task. + final IBinder fragmentToken = new Binder(); + final Task visibleTask = createTask(mDisplayContent); + final TaskFragment visibleTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(visibleTask) + .setOrganizer(mOrganizer) + .setFragmentToken(fragmentToken) + .build(); + doReturn(true).when(invisibleTask).shouldBeVisible(any()); + + // Sending events + invisibleTaskFragment.mTaskFragmentAppearedSent = true; + visibleTaskFragment.mTaskFragmentAppearedSent = true; + mController.onTaskFragmentInfoChanged(mIOrganizer, invisibleTaskFragment); + mController.onTaskFragmentInfoChanged(mIOrganizer, visibleTaskFragment); + mController.dispatchPendingEvents(); + + // Verify that both events are sent. + verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); + final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); + final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); + + // There should be two Task info changed with two TaskFragment info changed. + assertEquals(4, changes.size()); + // Invisible Task info changed + assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(0).getType()); + assertEquals(invisibleTask.mTaskId, changes.get(0).getTaskId()); + // Invisible TaskFragment info changed + assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(1).getType()); + assertEquals(invisibleTaskFragment.getFragmentToken(), + changes.get(1).getTaskFragmentToken()); + // Visible Task info changed + assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(2).getType()); + assertEquals(visibleTask.mTaskId, changes.get(2).getTaskId()); + // Visible TaskFragment info changed + assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(3).getType()); + assertEquals(visibleTaskFragment.getFragmentToken(), changes.get(3).getTaskFragmentToken()); } @Test public void testCanSendPendingTaskFragmentEventsAfterActivityResumed() { - // Task - TaskFragment - Activity. final Task task = createTask(mDisplayContent); final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) .setParentTask(task) @@ -905,24 +1009,26 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { .createActivityCount(1) .build(); final ActivityRecord activity = taskFragment.getTopMostActivity(); - - // Mock the task to invisible doReturn(false).when(task).shouldBeVisible(any()); taskFragment.setResumedActivity(null, "test"); - // Sending events - taskFragment.mTaskFragmentAppearedSent = true; - mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + // Dispatch the initial event in the Task to update the Task visibility to the organizer. + mController.onTaskFragmentAppeared(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); + verify(mOrganizer).onTransactionReady(any()); - // Verifies that event was not sent + // Verify the info changed event is not sent because the Task is invisible + clearInvocations(mOrganizer); + final Rect bounds = new Rect(0, 0, 500, 1000); + task.setBoundsUnchecked(bounds); + mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + mController.dispatchPendingEvents(); verify(mOrganizer, never()).onTransactionReady(any()); - // Mock the task becomes visible, and activity resumed + // Mock the task becomes visible, and activity resumed. Verify the info changed event is + // sent. doReturn(true).when(task).shouldBeVisible(any()); taskFragment.setResumedActivity(activity, "test"); - - // Verifies that event is sent. mController.dispatchPendingEvents(); verify(mOrganizer).onTransactionReady(any()); } @@ -977,25 +1083,24 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity(); // Add another activity in the Task so that it always contains a non-finishing activity. createActivityRecord(task); - assertTrue(task.shouldBeVisible(null)); + doReturn(false).when(task).shouldBeVisible(any()); - // Dispatch pending info changed event from creating the activity - taskFragment.mTaskFragmentAppearedSent = true; - mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + // Dispatch the initial event in the Task to update the Task visibility to the organizer. + mController.onTaskFragmentAppeared(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); verify(mOrganizer).onTransactionReady(any()); - // Verify the info changed callback is not called when the task is invisible + // Verify the info changed event is not sent because the Task is invisible clearInvocations(mOrganizer); - doReturn(false).when(task).shouldBeVisible(any()); + final Rect bounds = new Rect(0, 0, 500, 1000); + task.setBoundsUnchecked(bounds); mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); verify(mOrganizer, never()).onTransactionReady(any()); - // Finish the embedded activity, and verify the info changed callback is called because the + // Finish the embedded activity, and verify the info changed event is sent because the // TaskFragment is becoming empty. embeddedActivity.finishing = true; - mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); verify(mOrganizer).onTransactionReady(any()); } 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 9fd085021d66..66e46a2bb187 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -59,7 +59,9 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.app.ActivityManager; import android.content.res.Configuration; +import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; @@ -79,6 +81,8 @@ import android.window.TransitionInfo; import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; +import com.android.internal.graphics.ColorUtils; + import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -1384,6 +1388,50 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testChangeSetBackgroundColor() { + final Transition transition = createTestTransition(TRANSIT_CHANGE); + final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; + final ArraySet<WindowContainer> participants = transition.mParticipants; + + // Test background color for Activity and embedded TaskFragment. + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + mAtm.mTaskFragmentOrganizerController.registerOrganizer( + ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder())); + final Task task = createTask(mDisplayContent); + final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer); + final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity(); + final ActivityRecord nonEmbeddedActivity = createActivityRecord(task); + final ActivityManager.TaskDescription taskDescription = + new ActivityManager.TaskDescription.Builder() + .setBackgroundColor(Color.YELLOW) + .build(); + task.setTaskDescription(taskDescription); + + // Start states: + embeddedActivity.mVisibleRequested = true; + nonEmbeddedActivity.mVisibleRequested = false; + changes.put(embeddedTf, new Transition.ChangeInfo(embeddedTf)); + changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(nonEmbeddedActivity)); + // End states: + embeddedActivity.mVisibleRequested = false; + nonEmbeddedActivity.mVisibleRequested = true; + + participants.add(embeddedTf); + participants.add(nonEmbeddedActivity); + final ArrayList<WindowContainer> targets = Transition.calculateTargets( + participants, changes); + final TransitionInfo info = Transition.calculateTransitionInfo(transition.mType, + 0 /* flags */, targets, changes, mMockT); + + // Background color should be set on both Activity and embedded TaskFragment. + final int expectedBackgroundColor = ColorUtils.setAlphaComponent( + taskDescription.getBackgroundColor(), 255); + assertEquals(2, info.getChanges().size()); + assertEquals(expectedBackgroundColor, info.getChanges().get(0).getBackgroundColor()); + assertEquals(expectedBackgroundColor, info.getChanges().get(1).getBackgroundColor()); + } + + @Test public void testTransitionVisibleChange() { registerTestTransitionPlayer(); final ActivityRecord app = createActivityRecord(mDisplayContent); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 3da47110fb49..352d8d115b6a 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -136,9 +136,6 @@ public class VoiceInteractionManagerService extends SystemService { private final RemoteCallbackList<IVoiceInteractionSessionListener> mVoiceInteractionSessionListeners = new RemoteCallbackList<>(); - // TODO(b/226201975): remove once RoleService supports pre-created users - private final ArrayList<UserHandle> mIgnoredPreCreatedUsers = new ArrayList<>(); - public VoiceInteractionManagerService(Context context) { super(context); mContext = context; @@ -308,24 +305,14 @@ public class VoiceInteractionManagerService extends SystemService { return hotwordDetectionConnection.mIdentity; } + // TODO(b/226201975): remove this method once RoleService supports pre-created users @Override public void onPreCreatedUserConversion(int userId) { - Slogf.d(TAG, "onPreCreatedUserConversion(%d)", userId); - - for (int i = 0; i < mIgnoredPreCreatedUsers.size(); i++) { - UserHandle preCreatedUser = mIgnoredPreCreatedUsers.get(i); - if (preCreatedUser.getIdentifier() == userId) { - Slogf.d(TAG, "Updating role on pre-created user %d", userId); - mServiceStub.mRoleObserver.onRoleHoldersChanged(RoleManager.ROLE_ASSISTANT, - preCreatedUser); - mIgnoredPreCreatedUsers.remove(i); - return; - } - } - Slogf.w(TAG, "onPreCreatedUserConversion(%d): not available on " - + "mIgnoredPreCreatedUserIds (%s)", userId, mIgnoredPreCreatedUsers); + Slogf.d(TAG, "onPreCreatedUserConversion(%d): calling onRoleHoldersChanged() again", + userId); + mServiceStub.mRoleObserver.onRoleHoldersChanged(RoleManager.ROLE_ASSISTANT, + UserHandle.of(userId)); } - } // implementation entry point and binder service @@ -807,8 +794,10 @@ public class VoiceInteractionManagerService extends SystemService { if (TextUtils.isEmpty(curInteractor)) { return null; } - if (DEBUG) Slog.d(TAG, "getCurInteractor curInteractor=" + curInteractor + if (DEBUG) { + Slog.d(TAG, "getCurInteractor curInteractor=" + curInteractor + " user=" + userHandle); + } return ComponentName.unflattenFromString(curInteractor); } @@ -816,8 +805,9 @@ public class VoiceInteractionManagerService extends SystemService { Settings.Secure.putStringForUser(mContext.getContentResolver(), Settings.Secure.VOICE_INTERACTION_SERVICE, comp != null ? comp.flattenToShortString() : "", userHandle); - if (DEBUG) Slog.d(TAG, "setCurInteractor comp=" + comp - + " user=" + userHandle); + if (DEBUG) { + Slog.d(TAG, "setCurInteractor comp=" + comp + " user=" + userHandle); + } } ComponentName findAvailRecognizer(String prefPackage, int userHandle) { @@ -1912,7 +1902,6 @@ public class VoiceInteractionManagerService extends SystemService { pw.println(" mTemporarilyDisabled: " + mTemporarilyDisabled); pw.println(" mCurUser: " + mCurUser); pw.println(" mCurUserSupported: " + mCurUserSupported); - pw.println(" mIgnoredPreCreatedUsers: " + mIgnoredPreCreatedUsers); dumpSupportedUsers(pw, " "); mDbHelper.dump(pw); if (mImpl == null) { @@ -2026,6 +2015,11 @@ public class VoiceInteractionManagerService extends SystemService { List<String> roleHolders = mRm.getRoleHoldersAsUser(roleName, user); + if (DEBUG) { + Slogf.d(TAG, "onRoleHoldersChanged(%s, %s): roleHolders=%s", roleName, user, + roleHolders); + } + // TODO(b/226201975): this method is beling called when a pre-created user is added, // at which point it doesn't have any role holders. But it's not called again when // the actual user is added (i.e., when the pre-created user is converted), so we @@ -2036,9 +2030,9 @@ public class VoiceInteractionManagerService extends SystemService { if (roleHolders.isEmpty()) { UserInfo userInfo = mUserManagerInternal.getUserInfo(user.getIdentifier()); if (userInfo != null && userInfo.preCreated) { - Slogf.d(TAG, "onRoleHoldersChanged(): ignoring pre-created user %s for now", - userInfo.toFullString()); - mIgnoredPreCreatedUsers.add(user); + Slogf.d(TAG, "onRoleHoldersChanged(): ignoring pre-created user %s for now," + + " this method will be called again when it's converted to a real" + + " user", userInfo.toFullString()); return; } } |