Merge "Fix touches in Large screen header" into tm-qpr-dev
diff --git a/core/java/android/app/LocaleManager.java b/core/java/android/app/LocaleManager.java
index 794c694..c5dedb3 100644
--- a/core/java/android/app/LocaleManager.java
+++ b/core/java/android/app/LocaleManager.java
@@ -173,7 +173,7 @@
@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 e837920..8b41aa4 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -8468,8 +8468,8 @@
}
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/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java
index 24e47bf..2dced96 100644
--- a/core/java/android/appwidget/AppWidgetHost.java
+++ b/core/java/android/appwidget/AppWidgetHost.java
@@ -329,6 +329,22 @@
}
/**
+ * 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
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index cb0dce9..32bdf79 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -1047,7 +1047,7 @@
}
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;
}
diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java
new file mode 100644
index 0000000..dd4385c
--- /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 47796de..6af8ddd 100644
--- a/core/java/android/window/IOnBackInvokedCallback.aidl
+++ b/core/java/android/window/IOnBackInvokedCallback.aidl
@@ -28,17 +28,18 @@
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 1a37e57..5823084 100644
--- a/core/java/android/window/OnBackAnimationCallback.java
+++ b/core/java/android/window/OnBackAnimationCallback.java
@@ -40,10 +40,12 @@
* @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 6e2d4f9..62c41bf 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 @@
@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 0730f3d..fda39c1 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -218,19 +218,24 @@
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 @@
Handler.getMain().post(() -> {
final OnBackAnimationCallback callback = getBackAnimationCallback();
if (callback != null) {
- callback.onBackProgressed(backEvent);
+ mProgressAnimator.onBackProgressed(backEvent);
}
});
}
@@ -248,6 +253,7 @@
@Override
public void onBackCancelled() {
Handler.getMain().post(() -> {
+ mProgressAnimator.reset();
final OnBackAnimationCallback callback = getBackAnimationCallback();
if (callback != null) {
callback.onBackCancelled();
@@ -258,6 +264,7 @@
@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 0000000..65e8c64
--- /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 3c53d07..7dd1d26 100644
--- a/core/java/com/android/internal/app/LocalePicker.java
+++ b/core/java/com/android/internal/app/LocalePicker.java
@@ -311,8 +311,7 @@
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 965895f..3efd279 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 com.android.internal.R;
-import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
@@ -54,6 +49,7 @@
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 @@
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 @@
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 @@
* 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;
+ mLocaleList = localePickerController.getSupportedLocaleList(
+ parent, translatedOnly, 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);
- }
-
- 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 @@
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 @@
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 @@
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 4a1f7eb..42b46cd 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -647,15 +647,16 @@
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 1be1247..5ed0e8b 100644
--- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
+++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
@@ -70,17 +70,17 @@
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 @@
@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 0000000..9a6d4c1
--- /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 2d68cb4..51b56db 100644
--- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
+++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
@@ -45,6 +45,7 @@
@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 5de7240..a05062b 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_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 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 @@
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 @@
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 @@
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/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 44cfe1a..1d4b246 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -322,4 +322,7 @@
/** 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 964fe2d..f7467b5 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 c4599da..8df624f 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 9e1c6e2..d6d9c5c 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 @@
<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 740f528..2c2e501 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 @@
<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/values-es-rUS/strings.xml b/core/res/res/values-es-rUS/strings.xml
index 2a3f916..e8e5a51 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 79faaac..0daa324 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 9ab64e6..d11eca6 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 4191ab9..881d499 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>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 5897e3a..7a7b43a 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" />
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index 0b8b29b..bcb13d2 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.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.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.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.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 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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 f448cb3..f370ebd 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -60,6 +60,8 @@
private OnBackAnimationCallback mCallback1;
@Mock
private OnBackAnimationCallback mCallback2;
+ @Mock
+ private BackEvent mBackEvent;
@Before
public void setUp() throws Exception {
@@ -85,14 +87,14 @@
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 @@
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 @@
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/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index 00be5a6e..77284c41 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 @@
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 bf7326a..1d513e4 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 @@
@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 @@
/**
* 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 @@
}
/**
- * @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 00943f2d..231da05 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 @@
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 18712ae..71b8840 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 @@
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;
}
- return mPendingAppearedActivities.isEmpty()
- && mInfo.getActivities().size() == collectNonFinishingActivities().size();
+ 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;
+ }
+ // 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 a403031..87d0278 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 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 @@
private SplitController mSplitController;
private SplitPresenter mSplitPresenter;
+ private Consumer<List<SplitInfo>> mEmbeddingCallback;
+ private List<SplitInfo> mSplitInfos;
private TransactionManager mTransactionManager;
@Before
@@ -141,9 +144,16 @@
.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 @@
}
@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 @@
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 @@
/** 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 @@
// 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 35415d8..d43c471 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 @@
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 0fdfed8..17b1d1b 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 351e192..2babc3e 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 765aaba..fe43cd6 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 0ec5db1..c22f542 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 bd4a079..cf5394b 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 a04a50d..c525f52 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 2cc0ab3..bec284d 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 a2badaf..c84bb48 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 084ea86..60c1f85 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 195c335..b57f0c8 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 0ad387b..aed03ac 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 4b85a22..bddc2c3 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 cc90578..696a520 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 a1d0b58..7550a09 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 5df1e04..a79adf8 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 c4b386a..865e2dc 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 f5a106d..47e6696 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 95484d5..2c643de 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 5ae72eb..d3bca33 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 d36a83f..f157fcf 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 31d483e..ead1757 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 5c9425e..12c962d 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 787ac3e..b284361 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 927f0d7..2d842b8 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 955b2f8..3243a64 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 a69e105..3a655b8 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 15bde88..b800e3e 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 275efa2..94339a4 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 3048630..d282453 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 9013828..5db8d6d 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 dc884e9..779cf5c 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 a2f24f2..0ceee2d 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 6a70d8d..7ba49e5 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 d1d7db8..dd3ebe4 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 d2a96d9..c3056ca 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 cc2b2c3..56a6fb1 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 98a775e..b96caf2 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 5724061..a1ec3b5 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 12f022e..2f995e5 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 cdd8706..3d9bde4 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 fef3792..b39fd04 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 2d10e20..f4d4cee 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 0eeeca7..6d050c2 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 2b105bdb..025e2e6 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 a092531..97bb680 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 0330125..ce73bd5 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 6e4a768..511db6f 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 df911ed..16cbf12 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 f484022..59d87a0 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/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 938189f..cbcd949 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 @@
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 @@
@Nullable
private IOnBackInvokedCallback mBackToLauncherCallback;
private float mTriggerThreshold;
- private float mProgressThreshold;
private final Runnable mResetTransitionRunnable = () -> {
finishAnimation();
mTransitionInProgress = false;
@@ -125,7 +125,6 @@
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 @@
};
/**
- * 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 @@
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 @@
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 @@
return;
}
- mTouchTracker.update(touchX, touchY, swipeEdge);
+ mTouchTracker.update(touchX, touchY);
if (keyAction == MotionEvent.ACTION_DOWN) {
if (!mBackGestureStarted) {
mShouldStartOnNextMoveEvent = true;
@@ -414,7 +376,7 @@
// 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 @@
}
}
- 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 @@
displayTargetScreenshot(hardwareBuffer,
backNavigationInfo.getTaskWindowConfiguration());
}
+ targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
mTransaction.apply();
} else if (dispatchToLauncher) {
targetCallback = mBackToLauncherCallback;
@@ -474,7 +437,10 @@
targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
}
if (!USE_TRANSITION || !dispatchToLauncher) {
- dispatchOnBackStarted(targetCallback);
+ dispatchOnBackStarted(
+ targetCallback,
+ mTouchTracker.createStartEvent(
+ mBackNavigationInfo.getDepartingAnimationTarget()));
}
}
@@ -514,29 +480,15 @@
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 @@
|| 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 @@
}
}
- 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 @@
return;
}
mTriggerBack = triggerBack;
+ mTouchTracker.setTriggerBack(triggerBack);
}
private void setSwipeThresholds(float triggerThreshold, float progressThreshold) {
- mProgressThreshold = progressThreshold;
+ mTouchTracker.setProgressThreshold(progressThreshold);
mTriggerThreshold = triggerThreshold;
}
@@ -686,6 +651,7 @@
BackNavigationInfo backNavigationInfo = mBackNavigationInfo;
boolean triggerBack = mTriggerBack;
mBackNavigationInfo = null;
+ mAnimationTarget = null;
mTriggerBack = false;
mShouldStartOnNextMoveEvent = false;
if (backNavigationInfo == null) {
@@ -762,17 +728,9 @@
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 0000000..ccfac65
--- /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 d6803e8..d3a9a67 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 @@
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 5dab8a0..4ded3ea 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 @@
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 44a467f..cbd544c 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 @@
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 b96facf..34ff6d8 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_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 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 @@
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 @@
/**
* 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 @@
for (RunningTaskInfo task : taskInfos) {
wct.reorder(task.token, true);
}
-
- if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
- mShellTaskOrganizer.applyTransaction(wct);
- }
-
return wct;
}
@@ -237,17 +256,29 @@
@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 @@
*/
@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 988601c..c91d54a 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 @@
* 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 @@
fun addActiveTask(taskId: Int) {
val added = activeTasks.add(taskId)
if (added) {
- listeners.onEach { it.onActiveTasksChanged() }
+ activeTasksListeners.onEach { it.onActiveTasksChanged() }
}
}
@@ -62,7 +83,7 @@
fun removeActiveTask(taskId: Int) {
val removed = activeTasks.remove(taskId)
if (removed) {
- listeners.onEach { it.onActiveTasksChanged() }
+ activeTasksListeners.onEach { it.onActiveTasksChanged() }
}
}
@@ -81,9 +102,43 @@
}
/**
- * 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.
*/
- interface Listener {
- fun onActiveTasksChanged()
+ 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 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 f82a346..eaa7158 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 @@
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 @@
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 @@
"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 afb64c9..43d3f36 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 @@
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/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 08f3db6..f9172ba 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 @@
* 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 @@
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 8cee4f1..6ce981e 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 @@
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/util/SplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java
index e903897..f209521 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 @@
// 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 @@
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 @@
appsStackedVertically = parcel.readBoolean();
leftTopTaskId = parcel.readInt();
rightBottomTaskId = parcel.readInt();
+ dividerWidthPercent = parcel.readInt();
+ dividerHeightPercent = parcel.readInt();
}
@Override
@@ -87,6 +95,8 @@
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 7d1f130..9d61c14 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
@@ -101,9 +101,9 @@
final int shadowRadiusID = taskInfo.isFocused
? R.dimen.freeform_decor_shadow_focused_thickness
: R.dimen.freeform_decor_shadow_unfocused_thickness;
- final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode()
- == WindowConfiguration.WINDOWING_MODE_FREEFORM;
- final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable;
+ final boolean isFreeform =
+ taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
+ final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
@@ -114,6 +114,7 @@
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;
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 01cab9a..b314163 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
@@ -91,7 +91,7 @@
SurfaceControl mTaskBackgroundSurface;
SurfaceControl mCaptionContainerSurface;
- private CaptionWindowManager mCaptionWindowManager;
+ private WindowlessWindowManager mCaptionWindowManager;
private SurfaceControlViewHost mViewHost;
private final Rect mCaptionInsetsRect = new Rect();
@@ -199,13 +199,14 @@
}
final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
- final int decorContainerOffsetX = -loadResource(params.mOutsetLeftId);
- final int decorContainerOffsetY = -loadResource(params.mOutsetTopId);
+ final Resources resources = mDecorWindowContext.getResources();
+ final int decorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId);
+ final int decorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId);
outResult.mWidth = taskBounds.width()
- + loadResource(params.mOutsetRightId)
+ + loadDimensionPixelSize(resources, params.mOutsetRightId)
- decorContainerOffsetX;
outResult.mHeight = taskBounds.height()
- + loadResource(params.mOutsetBottomId)
+ + loadDimensionPixelSize(resources, params.mOutsetBottomId)
- decorContainerOffsetY;
startT.setPosition(
mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY)
@@ -225,7 +226,7 @@
.build();
}
- float shadowRadius = loadResource(params.mShadowRadiusId);
+ 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;
@@ -248,8 +249,8 @@
.build();
}
- final int captionHeight = loadResource(params.mCaptionHeightId);
- final int captionWidth = loadResource(params.mCaptionWidthId);
+ 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 @@
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
@@ -309,13 +311,6 @@
.setCrop(mTaskSurface, mTaskSurfaceCrop);
}
- private int loadResource(int resourceId) {
- if (resourceId == Resources.ID_NULL) {
- return 0;
- }
- return mDecorWindowContext.getResources().getDimensionPixelSize(resourceId);
- }
-
/**
* Obtains the {@link Display} instance for the display ID in {@link #mTaskInfo} if it exists or
* registers {@link #mOnDisplaysChangedListener} if it doesn't.
@@ -374,33 +369,18 @@
releaseViews();
}
- static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
- int mWidth;
- int mHeight;
- T mRootView;
-
- void reset() {
- mWidth = 0;
- mHeight = 0;
- mRootView = null;
+ private static int loadDimensionPixelSize(Resources resources, int resourceId) {
+ if (resourceId == Resources.ID_NULL) {
+ return 0;
}
+ return resources.getDimensionPixelSize(resourceId);
}
- private static class CaptionWindowManager extends WindowlessWindowManager {
- CaptionWindowManager(Configuration config, SurfaceControl rootSurface) {
- super(config, rootSurface, null /* hostInputToken */);
+ private static float loadDimension(Resources resources, int resourceId) {
+ if (resourceId == Resources.ID_NULL) {
+ return 0;
}
-
- @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);
- }
+ return resources.getDimension(resourceId);
}
static class RelayoutParams{
@@ -433,6 +413,23 @@
mOutsetLeftId = Resources.ID_NULL;
mOutsetRightId = Resources.ID_NULL;
}
-
}
-}
\ No newline at end of file
+
+ static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
+ int mWidth;
+ int mHeight;
+ T mRootView;
+
+ void reset() {
+ mWidth = 0;
+ mHeight = 0;
+ mRootView = null;
+ }
+ }
+
+ interface SurfaceControlViewHostFactory {
+ default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
+ return new SurfaceControlViewHost(c, d, wmm);
+ }
+ }
+}
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 0000000..8949a75
--- /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 077e9ca..2e328b0 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 @@
mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
mActivityTaskManager, mContext,
mContentResolver);
+ mController.setEnableUAnimation(true);
mShellInit.init();
mEventTime = 0;
mShellExecutor.flushAll();
@@ -245,10 +246,10 @@
// 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 @@
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 @@
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 @@
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 @@
// 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 0000000..3aefc3f
--- /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 c850a3b..79b520c 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_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.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 @@
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 9b28d11..aaa5c8a 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 @@
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 @@
@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 @@
@Test
fun addActiveTask_sameTaskDoesNotNotify() {
val listener = TestListener()
- repo.addListener(listener)
+ repo.addActiveTaskListener(listener)
repo.addActiveTask(1)
repo.addActiveTask(1)
@@ -58,7 +59,7 @@
@Test
fun addActiveTask_multipleTasksAddedNotifiesForEach() {
val listener = TestListener()
- repo.addListener(listener)
+ repo.addActiveTaskListener(listener)
repo.addActiveTask(1)
repo.addActiveTask(2)
@@ -68,7 +69,7 @@
@Test
fun removeActiveTask_listenerNotifiedAndTaskNotActive() {
val listener = TestListener()
- repo.addListener(listener)
+ repo.addActiveTaskListener(listener)
repo.addActiveTask(1)
repo.removeActiveTask(1)
@@ -80,7 +81,7 @@
@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 @@
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 103c8da..4d37e5d 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,12 +50,13 @@
import android.window.WindowContainerTransaction;
import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.wm.shell.R;
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;
@@ -145,8 +146,11 @@
// 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;
- mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle,
- R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle);
+ 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);
@@ -196,13 +200,11 @@
// 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;
-// int outsetLeftId = R.dimen.split_divider_bar_width;
-// int outsetTopId = R.dimen.gestures_onehanded_drag_threshold;
-// int outsetRightId = R.dimen.freeform_resize_handle;
-// int outsetBottomId = R.dimen.bubble_dismiss_target_padding_x;
-// mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
- mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle,
- R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle);
+ 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);
@@ -211,8 +213,8 @@
verify(decorContainerSurfaceBuilder).setParent(taskSurface);
verify(decorContainerSurfaceBuilder).setContainerLayer();
verify(mMockSurfaceControlStartT).setTrustedOverlay(decorContainerSurface, true);
- verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -60, -60);
- verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 420, 220);
+ verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -20, -40);
+ verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 380, 220);
verify(taskBackgroundSurfaceBuilder).setParent(taskSurface);
verify(taskBackgroundSurfaceBuilder).setEffectLayer();
@@ -225,36 +227,34 @@
verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
verify(captionContainerSurfaceBuilder).setContainerLayer();
- verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -6, -156);
- verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 432);
+ verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -46, 8);
+ verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64);
verify(mMockSurfaceControlStartT).show(captionContainerSurface);
verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
verify(mMockSurfaceControlViewHost)
.setView(same(mMockView),
- argThat(lp -> lp.height == 432
+ argThat(lp -> lp.height == 64
&& 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, 516),
+ new Rect(100, 300, 400, 332),
new int[] { InsetsState.ITYPE_CAPTION_BAR });
}
verify(mMockSurfaceControlFinishT)
.setPosition(taskSurface, TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y);
verify(mMockSurfaceControlFinishT)
- .setCrop(taskSurface, new Rect(-60, -60, 360, 160));
+ .setCrop(taskSurface, new Rect(-20, -40, 360, 180));
verify(mMockSurfaceControlStartT)
.show(taskSurface);
- assertEquals(420, mRelayoutResult.mWidth);
+ assertEquals(380, mRelayoutResult.mWidth);
assertEquals(220, mRelayoutResult.mHeight);
-
-
}
@Test
@@ -293,8 +293,11 @@
// 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;
- mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle,
- R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle);
+ 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);
@@ -365,7 +368,8 @@
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))),
@@ -417,12 +421,10 @@
@Override
void relayout(ActivityManager.RunningTaskInfo taskInfo) {
-
mRelayoutParams.mLayoutResId = 0;
- mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_width;
- mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width;
- mRelayoutParams.mShadowRadiusId =
- R.dimen.freeform_decor_shadow_unfocused_thickness;
+ 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/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml b/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml
index 18b8ca9..77dfbee 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/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java
index 39977df..f969a63 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 @@
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/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 808ea9e..6d375ac 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 @@
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/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index ca36fa4..fdfad2b 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.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 @@
*/
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 @@
* 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 @@
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 @@
* 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 @@
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 @@
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 @@
* 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 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)
@@ -917,7 +865,7 @@
}
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 8ce372d..40a5e97 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 @@
*/
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 @@
* 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 @@
): 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 f79b328..5f1bb83 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 @@
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 d427a57..0448c81 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 @@
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 @@
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 @@
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 0000000..ecee598
--- /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 8457312..cf66ff6 100644
--- a/packages/SystemUI/checks/Android.bp
+++ b/packages/SystemUI/checks/Android.bp
@@ -40,6 +40,10 @@
"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 4eeeb85..4b9aa13 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 @@
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 @@
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/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
index d4c55c0..141dd05 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,185 +18,22 @@
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")
@NonNull
private fun indentedJava(@NonNull @Language("JAVA") source: String) = java(source).indented()
-internal val commonSettingsCode =
- """
-public static float getFloat(ContentResolver cr, String name) { return 0.0f; }
-public static long getLong(ContentResolver cr, String name) {
- return 0L;
-}
-public static int getInt(ContentResolver cr, String name) {
- return 0;
-}
-public static String getString(ContentResolver cr, String name) {
- return "";
-}
-public static float getFloat(ContentResolver cr, String name, float def) {
- return 0.0f;
-}
-public static long getLong(ContentResolver cr, String name, long def) {
- return 0L;
-}
-public static int getInt(ContentResolver cr, String name, int def) {
- return 0;
-}
-public static String getString(ContentResolver cr, String name, String def) {
- return "";
-}
-public static boolean putFloat(ContentResolver cr, String name, float value) {
- return true;
-}
-public static boolean putLong(ContentResolver cr, String name, long value) {
- return true;
-}
-public static boolean putInt(ContentResolver cr, String name, int value) {
- return true;
-}
-public static boolean putFloat(ContentResolver cr, String name) {
- return true;
-}
-public static boolean putString(ContentResolver cr, String name, String value) {
- return true;
-}
-"""
-
/*
* This file contains stubs of framework APIs and System UI classes for testing purposes only. The
* stubs are not used in the lint detectors themselves.
*/
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;
@@ -208,47 +45,4 @@
}
"""
),
- 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 {
-}
-"""
- ),
- indentedJava(
- """
-package android.provider;
-
-public class Settings {
- public static final class Global {
- public static final String UNLOCK_SOUND = "unlock_sound";
- """ +
- commonSettingsCode +
- """
- }
- public static final class Secure {
- """ +
- commonSettingsCode +
- """
- }
- public static final class System {
- """ +
- commonSettingsCode +
- """
- }
-}
-"""
- ),
)
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 090ddf8..c632636 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
@@ -51,12 +51,12 @@
.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
"""
)
@@ -67,7 +67,7 @@
lint()
.files(
TestFiles.java(
- """
+ """
import android.graphics.Bitmap;
public class TestClass {
@@ -76,8 +76,7 @@
}
}
"""
- )
- .indented(),
+ ),
*stubs
)
.issues(SoftwareBitmapDetector.ISSUE)
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
index 2183b38..3f93f07 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt
@@ -3,9 +3,42 @@
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
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 065c314..50c3d7e 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.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 @@
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 @@
}
/** 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 @@
}
}
- 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 @@
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 a850238..da612a9 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 b3dd955..dee0f5c 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 @@
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 @@
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 @@
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 @@
return huePopulation
}
}
-}
\ No newline at end of file
+}
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 1e74c3d..89f5c2c 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 @@
/** 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 @@
/** 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/res-keyguard/drawable/media_squiggly_progress.xml b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml
similarity index 71%
copy from packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
copy to packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml
index 9e61236..de0e526 100644
--- a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
+++ b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml
@@ -12,6 +12,9 @@
~ 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
-->
-<com.android.systemui.media.SquigglyProgress />
\ No newline at end of file
+<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 3ad7c8c..d64587d 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 16a1d94..647abee 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 d5e84f9..d5552f6 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 be52c44..533e5a2 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 adb57b6..81ce7d3 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 cbfb325..443f666 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 6ec1061..e125697 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 13d6613..f0d1ef2 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 616d31a..e1af3ece 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 366a7f4..0b4417a 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 c20be5d..4851579 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 f1c00a9..4705b4d9 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 709407c..284eaeb 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 a44658c..6b4f607 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 331c355..85238df 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 c19b357..18befed 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 1d6ec82..65b84486 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 2b78f96..588f1b5 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 e1c2532..08fc8d6 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 2b78f96..588f1b5 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 2b78f96..588f1b5 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 9052e4f..a23aeb0 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 9dc054a..c71a678 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 f9f0452..c6ee698 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 dceb78e..071ede8 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 8431268..9b8e65b 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 37bb260..3583f1e 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 f8cec42..a0ac6df 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 077fe11..66fd7c0 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 45dadc1..ec00ba3 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 4fbdd67..a3f8e86 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 6caac8a..c97fe01 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 627576e..1283004 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 8b1b504..7a14a80 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 6b75e72..a4fbf53 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 3412026..086eeb9 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 1afb791..b43a032 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 6abdc82..8bad961 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 9fed5f7..186177ff 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 b5b1c53..aab4206 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 afe0159..1a4fb0b 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 b32caa7..123cc39 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 d6d5bcd..8daca5c 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 00bfe05..73f507c 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 80a98e6..c279cea 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 b952f1b..4c058ed 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 485337d..7c7099e 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 17584b5..5a6df42 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 a066a66..4d98fd1 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 d371a4b..2660a06 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 ef22564..77e1b50 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 63a542a..e62b435 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 71c913f..f2cc5ab 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 6ac13bd..1454b20 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 453afc3..a6d1af9 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 1cc46b1..5617a11 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 5310a730..0ad9e95 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 534164b..196b74a 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 08e226d4..747b3bb 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 3cdd264..75f7a89 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 409f727..bf1a359a 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 52bc982..c49149b 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 b934826..3d60e8c 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 a67bfb0..0a94349 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 b934826..3d60e8c 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 5ee67d91..547224e 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 2b8f8d6..f1945ad 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 4e911de..e5862c3 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 f2d68e3..efe4ec8 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 772308f..52726c2 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 c758462..a0a5594 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 e6fe853..e634fdcb5 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 fa241d9..fc9beb1 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 791bceb..bcab24b 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 271657d..88d5760 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 f62e667..3a0111a 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 62a83bc..14a65a07 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 524ea47..7936058 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 54aaae3..e520762 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 6144c1c..613181d 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 4e77841..a122f85 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 afaf746..2cc9724 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 1d6cfa8..e7c9295 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 8c8507e..d37d645 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 c331a92..9dbb8f2 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 1e1bec3..ebb88e1 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 c8f78ea..57e56f7 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 b1d3375..a25ab51 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 46f6ab2..0a55cf7 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
new file mode 100644
index 0000000..3dd997f
--- /dev/null
+++ b/packages/SystemUI/res/drawable-hdpi/textfield_default_filled.9.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable-mdpi/textfield_default_filled.9.png b/packages/SystemUI/res/drawable-mdpi/textfield_default_filled.9.png
new file mode 100644
index 0000000..80aba01
--- /dev/null
+++ b/packages/SystemUI/res/drawable-mdpi/textfield_default_filled.9.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable-xhdpi/textfield_default_filled.9.png b/packages/SystemUI/res/drawable-xhdpi/textfield_default_filled.9.png
new file mode 100644
index 0000000..b3f89ed
--- /dev/null
+++ b/packages/SystemUI/res/drawable-xhdpi/textfield_default_filled.9.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable-xxhdpi/textfield_default_filled.9.png b/packages/SystemUI/res/drawable-xxhdpi/textfield_default_filled.9.png
new file mode 100644
index 0000000..efa2cb9
--- /dev/null
+++ b/packages/SystemUI/res/drawable-xxhdpi/textfield_default_filled.9.png
Binary files differ
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 0000000..cca34d4
--- /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
similarity index 91%
rename from packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
rename to packages/SystemUI/res/drawable/media_squiggly_progress.xml
index 9e61236..9cd3f62 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/qs_media_background.xml b/packages/SystemUI/res/drawable/qs_media_background.xml
index 6ed3a0ae..217656da 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 b2647c1..849349a 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 da76c8d..3bcc37a 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/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/error"
- android:layout_width="match_parent"
+ <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 19a85fe..a3dd334 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"
+ 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/title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- style="@style/TextAppearance.AuthCredential.Title"/>
-
- <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"/>
+ android:layout_gravity="center_horizontal|bottom"/>
- <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>
-
- </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 0ff1db2..774b335f 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:layout_height="match_parent">
+
+ <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="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <TextView
+ android:id="@+id/subtitle"
+ style="?subTitleTextAppearance"
+ android:layout_below="@id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <TextView
+ android:id="@+id/description"
+ style="?descriptionTextAppearance"
+ android:layout_below="@id/subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/auth_credential_input"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
android:orientation="vertical">
- <LinearLayout
- android:id="@+id/auth_credential_header"
- style="@style/AuthCredentialHeaderStyle"
- android:layout_width="match_parent"
+ <ImeAwareEditText
+ android:id="@+id/lockPassword"
+ style="?passwordTextAppearance"
+ android:layout_width="208dp"
android:layout_height="wrap_content"
- android:layout_alignParentTop="true">
+ android:layout_gravity="center_horizontal"
+ android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii"
+ android:inputType="textPassword"
+ android:minHeight="48dp" />
- <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" />
-
- <TextView
- android:id="@+id/description"
- style="@style/TextAppearance.AuthNonBioCredential.Description"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
- </LinearLayout>
-
- <LinearLayout
+ <TextView
+ android:id="@+id/error"
+ style="?errorTextAppearance"
+ android:layout_gravity="center_horizontal"
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" />
- <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" />
-
- <TextView
- android:id="@+id/error"
- style="@style/TextAppearance.AuthNonBioCredential.Error"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
-
- </LinearLayout>
-
- </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 dada981..4af9970 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">
+ android:layout_height="wrap_content">
- <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"
+ style="?headerIconStyle"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:contentDescription="@null"/>
- <ImageView
- android:id="@+id/icon"
- android:layout_width="48dp"
- android:layout_height="48dp"
- 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/title"
- style="@style/TextAppearance.AuthNonBioCredential.Title"
- 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/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" />
- </LinearLayout>
-
- <LinearLayout
- 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">
-
- <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
- 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>
-
+ <TextView
+ android:id="@+id/description"
+ style="?descriptionTextAppearance"
+ android:layout_below="@id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
</RelativeLayout>
+ <FrameLayout
+ android:id="@+id/auth_credential_container"
+ style="?containerStyle"
+ android:layout_width="match_parent"
+ 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"
+ 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 4da7711..bc97e51 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/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml
index 50d3cc4..715c869 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/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index 753f941..159323a 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"
@@ -148,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 8919198..aefd998 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 0000000..8148d3d
--- /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 0000000..771de08
--- /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 0000000..f9ed67d
--- /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 0000000..78d299c
--- /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 bbe4b6c..f5fc7ee5 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 9a71995..df0659d 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/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index cf100cb..93926ef9 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -950,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/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9b9111f..212c77b5 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 b5011df..e76887b 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -197,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">
@@ -210,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>
@@ -224,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>
@@ -275,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>
@@ -301,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/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
index 2e391c7..49cc483 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 @@
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 @@
/**
* 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 @@
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 f59a320..91fd6a6 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -62,7 +62,6 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
},
- java_version: "1.8",
min_sdk_version: "current",
plugins: ["dagger2-compiler"],
}
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 860a5da..236aa66 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.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 @@
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 @@
override fun onAttachedToWindow() {
super.onAttachedToWindow()
+ logBuffer?.log(tag, DEBUG, "onAttachedToWindow")
refreshFormat()
}
@@ -150,27 +148,39 @@
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 @@
} 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 @@
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 @@
}
fun animateAppearOnLockscreen() {
- lastAnimationCall = "${getTimestamp()} call=animateAppearOnLockscreen"
+ logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen")
setTextStyle(
weight = dozingWeight,
textSize = -1f,
@@ -248,7 +263,7 @@
if (isAnimationEnabled && textAnimator == null) {
return
}
- lastAnimationCall = "${getTimestamp()} call=animateFoldAppear"
+ logBuffer?.log(tag, DEBUG, "animateFoldAppear")
setTextStyle(
weight = lockScreenWeightInternal,
textSize = -1f,
@@ -275,7 +290,7 @@
// 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 @@
}
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 @@
)
}
- 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 @@
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 @@
onAnimationEnd = onAnimationEnd
)
textAnimator?.glyphFilter = glyphFilter
+ if (color != null && !isAnimationEnabled) {
+ setTextColor(color)
+ }
}
}
}
@@ -394,9 +432,12 @@
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 @@
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 @@
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 @@
if (!clockView12Skel.contains("a")) {
sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' }
}
+
sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel)
sCacheKey = key
}
@@ -458,5 +611,36 @@
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 e3c21cc..cd27263 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
@@ -21,7 +21,6 @@
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
@@ -31,7 +30,6 @@
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
@@ -43,13 +41,6 @@
val handler: Handler,
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()
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 b887951..da1d233 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 @@
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.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 @@
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 @@
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 @@
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 72f8b7b..40c8774 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 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 @@
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 @@
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 @@
* 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 @@
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/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
index ce337bb..fd41cb06 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 @@
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 @@
@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 5d6598d..8a25096 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 @@
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 @@
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 f2742b7..766266d 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 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 @@
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 @@
? "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 5cca4a6..8bddf21 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 @@
* 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 @@
* 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 09cf7c5..37e706a 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 @@
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 @@
}
}
};
- remoteAnimationAdapter.onAnimationStart(transit, appsCompat, wallpapersCompat,
- nonAppsCompat, animationFinishedCallback);
+ remoteAnimationAdapter.onAnimationStart(transit, apps, wallpapers,
+ nonApps, animationFinishedCallback);
}
@Override
@@ -121,12 +115,12 @@
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 @@
}
}
// 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 @@
}
// 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 0076292..5809c81 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 2d6bef5..8d1768c 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 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 final 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 final 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 = app.rotationChange;
-
- 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,21 +63,10 @@
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);
- target.setRotationChange(rotationChange);
- return target;
- }
-
/**
* Almost a copy of Transitions#setupStartState.
* TODO: remove when there is proper cross-process transaction sync.
@@ -206,54 +138,61 @@
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;
}
/**
@@ -262,35 +201,20 @@
* @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;
+ });
}
/**
@@ -301,38 +225,22 @@
* @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<>();
-
- 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);
- }
- out.add(targetCompat);
- }
-
- return out.toArray(new RemoteAnimationTargetCompat[out.size()]);
+ return wrap(info, t, leashMap, (change, taskInfo) -> (taskInfo == null)
+ && wallpapers == ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0));
}
- /**
- * @see SurfaceControl#release()
- */
- public void release() {
- if (leash != null) {
- leash.release();
+ 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++) {
+ TransitionInfo.Change change = info.getChanges().get(i);
+ if (filter.test(change, change.getTaskInfo())) {
+ out.add(newTarget(change, info.getChanges().size() - i, info, t, leashMap));
+ }
}
- 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 f679225..d6655a7 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_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.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 @@
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 @@
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 @@
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/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
index 5277e40..450784e 100644
--- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
+++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
@@ -70,7 +70,7 @@
}
override fun setMessage(msg: CharSequence?) {
- if (msg == textAboutToShow || msg == text) {
+ if ((msg == textAboutToShow && msg != null) || msg == text) {
return
}
textAboutToShow = msg
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 910955a..40a96b0 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -30,11 +30,14 @@
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.log.LogBuffer
import com.android.systemui.shared.regionsampling.RegionSamplingInstance
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
@@ -66,12 +69,14 @@
private val context: Context,
@Main private val mainExecutor: Executor,
@Background private val bgExecutor: Executor,
+ @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)
}
@@ -217,8 +222,11 @@
disposableHandle = parent.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
listenForDozing(this)
- listenForDozeAmount(this)
- listenForDozeAmountTransition(this)
+ if (featureFlags.isEnabled(DOZING_MIGRATION_1)) {
+ listenForDozeAmountTransition(this)
+ } else {
+ listenForDozeAmount(this)
+ }
}
}
}
@@ -261,10 +269,9 @@
@VisibleForTesting
internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
return scope.launch {
- keyguardTransitionInteractor.aodToLockscreenTransition.collect {
- // Would eventually run this:
- // dozeAmount = it.value
- // clock?.animations?.doze(dozeAmount)
+ keyguardTransitionInteractor.dozeAmountTransition.collect {
+ dozeAmount = it.value
+ clock?.animations?.doze(dozeAmount)
}
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index d03ef98..8ebad6c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -127,7 +127,7 @@
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 202134b..35eecdf 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -40,6 +40,7 @@
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;
@@ -404,5 +405,10 @@
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/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index bcd1a1e..81305f9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -219,13 +219,16 @@
};
- 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 @@
}
}
};
- 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 e9f06ed..7849747 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.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 @@
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 46e187e..cb1330d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -2366,11 +2366,13 @@
* @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 @@
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 @@
* 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 @@
* 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 @@
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 c41b752..fe7c70a 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_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 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.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 java.io.PrintWriter;
import java.util.Objects;
+import java.util.function.Consumer;
import javax.inject.Inject;
@@ -101,6 +109,9 @@
@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 @@
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 @@
@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 @@
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 @@
@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 @@
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 @@
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
similarity index 86%
rename from packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java
rename to packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java
index c4be1ba535..72a44bd 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 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 0000000..f43f559
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard.dagger;
+
+import android.content.Context;
+import android.os.Handler;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Main;
+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) {
+ return new ClockRegistry(context, pluginManager, handler, defaultClockProvider);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 82b32cf..2f79e30 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -51,7 +51,7 @@
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 @@
{ "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 @@
{ "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 @@
}, { "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 @@
}, { "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 @@
{ "SubInfo:$str1" })
}
- fun logTimeFormatChanged(newTimeFormat: String) {
+ fun logTimeFormatChanged(newTimeFormat: String?) {
logBuffer.log(TAG, DEBUG,
{ str1 = newTimeFormat },
{ "handleTimeFormatUpdate timeFormat=$str1" })
@@ -338,18 +338,18 @@
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/Dumpable.java b/packages/SystemUI/src/com/android/systemui/Dumpable.java
index 6525951..73fdce6 100644
--- a/packages/SystemUI/src/com/android/systemui/Dumpable.java
+++ b/packages/SystemUI/src/com/android/systemui/Dumpable.java
@@ -30,7 +30,6 @@
/**
* 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 c595586..3e0fa45 100644
--- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
@@ -19,6 +19,7 @@
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 @@
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 @@
}
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)
}
- }
-
- override fun updateVisOnUpdateCutout(): Boolean {
- return false // instead, we always update the visibility whenever face scanning starts/ends
+ if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE) {
+ drawCameraProtection(canvas)
+ }
}
override fun enableShowProtection(show: Boolean) {
- val showScanningAnimNow = keyguardUpdateMonitor.isFaceScanning && show
+ val showScanningAnimNow = keyguardUpdateMonitor.isFaceDetectionRunning && show
if (showScanningAnimNow == showScanningAnim) {
return
}
@@ -152,91 +126,26 @@
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 @@
}
}
})
- 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 @@
// 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 @@
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 0000000..4c3a7ff
--- /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/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
index 7bcba3c..50e0399 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
@@ -121,6 +121,6 @@
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 e74d810..80c6c48 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 @@
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 @@
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 @@
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 @@
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 @@
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 d43e5d9..c015a21 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 @@
}
});
mUdfpsController.setAuthControllerUpdateUdfpsLocation(this::updateUdfpsLocation);
+ mUdfpsController.setUdfpsDisplayMode(new UdfpsDisplayMode(mContext, mExecution,
+ this));
mUdfpsBounds = mUdfpsProps.get(0).getLocation().getRect();
}
@@ -626,17 +628,6 @@
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 @@
} 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 0892612..76cd3f4 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.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 @@
@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 @@
}
return true;
});
+
+ setOnApplyWindowInsetsListener(this);
}
@Override
@@ -127,4 +147,92 @@
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 11498db..f9e44a0 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 @@
@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 4fa835e..5958e6a 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.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 @@
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 @@
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 a7648bf..b49d452 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 @@
@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 @@
// 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 @@
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 @@
@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 @@
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 @@
// 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 @@
}
/**
- * 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 66a521c..7d01096 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.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.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 @@
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 @@
@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 @@
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 @@
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 @@
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 @@
overlayParams.scaleFactor
)
}
- BiometricOverlayConstants.REASON_AUTH_KEYGUARD -> {
+ REASON_AUTH_KEYGUARD -> {
UdfpsKeyguardViewController(
view.addUdfpsView(R.layout.udfps_keyguard_view),
statusBarStateController,
@@ -216,7 +249,7 @@
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 @@
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.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 0000000..e9de7cc
--- /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/BroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java
index 9b7d498..8e062bd 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.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/classifier/FalsingCollector.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java
index 3871248..858bac3 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 @@
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 28aac05..0b7d6ab 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 @@
}
@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 f5f9655..da3d293 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.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.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 @@
ProximitySensor proximitySensor,
StatusBarStateController statusBarStateController,
KeyguardStateController keyguardStateController,
+ ShadeExpansionStateManager shadeExpansionStateManager,
BatteryController batteryController,
DockManager dockManager,
@Main DelayableExecutor mainExecutor,
@@ -157,6 +161,8 @@
mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback);
+ shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged);
+
mBatteryController.addCallback(mBatteryListener);
mDockManager.addListener(mDockEventListener);
}
@@ -193,8 +199,8 @@
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/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index bfb27a4..9f338d1 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -459,7 +459,7 @@
anim.start();
}
- private void hideImmediate() {
+ void hideImmediate() {
// Note this may be called multiple times if multiple dismissal events happen at the same
// time.
mTimeoutHandler.cancelTimeout();
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 bebade0..08e8293 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 @@
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 5d0e08f..4a56932 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 @@
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/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index d7638d6..7e31626 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 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;
@@ -120,7 +121,8 @@
BiometricsModule.class,
BouncerViewModule.class,
ClipboardOverlayModule.class,
- ClockModule.class,
+ ClockInfoModule.class,
+ ClockRegistryModule.class,
CoroutinesModule.class,
DreamModule.class,
ControlsModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index 5fdd198..976afd4 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.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 @@
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 @@
}
fun shouldShowFaceScanningAnim(): Boolean {
- return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceScanning
+ return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceDetectionRunning
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 2e51b51..b69afeb 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 @@
/**
* 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 @@
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 0e1bfba..18c8e01 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -25,6 +25,7 @@
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 @@
})
}
- 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 @@
"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/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index da6c163..9a091e7 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 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.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 @@
* 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 @@
}
DozeSensors(
- Context context,
AsyncSensorManager sensorManager,
DozeParameters dozeParameters,
AmbientDisplayConfiguration config,
@@ -157,7 +151,6 @@
AuthController authController,
DevicePostureController devicePostureController
) {
- mContext = context;
mSensorManager = sensorManager;
mConfig = config;
mWakeLock = wakeLock;
@@ -605,10 +598,7 @@
// 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 @@
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 @@
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 @@
&& !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 @@
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/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index ef454ff..32cb1c0 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 @@
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 @@
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 @@
// 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 @@
.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/DreamMediaEntryComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
index c07d402..1166c2f 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.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 478f861..609bd76 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.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.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 @@
/**
* 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 @@
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 @@
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 @@
"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 @@
}
}
+ 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,
@@ -267,6 +300,7 @@
}
}
}
+ PROTO -> pArgs.proto = true
"-t", "--tail" -> {
pArgs.tailLength = readArgument(iterator, arg) {
it.toInt()
@@ -278,6 +312,9 @@
"-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")
}
@@ -314,7 +351,7 @@
const val PRIORITY_ARG_CRITICAL = "CRITICAL"
const val PRIORITY_ARG_HIGH = "HIGH"
const val PRIORITY_ARG_NORMAL = "NORMAL"
- const val PROTO = "--sysui_proto"
+ const val PROTO = "--proto"
}
}
@@ -338,6 +375,7 @@
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 dbca651..ae78089 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
@@ -18,6 +18,8 @@
import android.util.ArrayMap
import com.android.systemui.Dumpable
+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
@@ -90,7 +92,7 @@
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 @@
}
}
+ @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 @@
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 @@
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/SystemUIAuxiliaryDumpService.java b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
index 0a41a56..da983ab 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 @@
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 0000000..cd8c08a
--- /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 c39a9ca4..0000000
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ /dev/null
@@ -1,355 +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 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);
-
- public static final UnreleasedFlag STABILITY_INDEX_FIX = new UnreleasedFlag(114, true);
-
- public static final UnreleasedFlag SEMI_STABLE_SORT = new UnreleasedFlag(115, true);
-
- // next id: 116
-
- /***************************************/
- // 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);
-
- // 1700 - clipboard
- public static final UnreleasedFlag CLIPBOARD_OVERLAY_REFACTOR = new UnreleasedFlag(1700);
-
- // 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 0000000..06b7614
--- /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 da5819a..3ef5499 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.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 @@
*
* @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 @@
mDialog.dismiss();
mDialog = null;
} else {
- handleShow(view);
+ handleShow(expandable);
}
}
@@ -495,7 +497,7 @@
}
}
- protected void handleShow(@Nullable View view) {
+ protected void handleShow(@Nullable Expandable expandable) {
awakenIfNecessary();
mDialog = createDialog();
prepareDialog();
@@ -507,10 +509,12 @@
// 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 @@
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 @@
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 a363270..c8370b4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -401,6 +401,11 @@
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.
*/
@@ -946,8 +951,7 @@
}
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 +1183,9 @@
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() {
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 7d4db37..2af9318 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 @@
/** 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/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 59bb22786..7409aec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -24,6 +24,8 @@
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
@@ -34,4 +36,17 @@
) {
/** 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/log/dagger/KeyguardClockLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
new file mode 100644
index 0000000..0645236
--- /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/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 92a7b51..9adb855 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -43,7 +43,7 @@
@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 @@
/**
* 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 @@
/**
* 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 @@
}
/**
+ * 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 90ced02..af43347 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
@@ -26,7 +26,7 @@
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 e5ac3e2..f4dac6e 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
@@ -26,7 +26,7 @@
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/MediaTimeoutListenerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
index 99ec05b..0c2cd92 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
@@ -26,7 +26,7 @@
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/MediaViewLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
index 8c904ea..5b7f4bb 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
@@ -26,7 +26,7 @@
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/media/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt
deleted file mode 100644
index 556560c..0000000
--- 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 5977ed0..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ /dev/null
@@ -1,1193 +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 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/MediaCarouselControllerLogger.kt
deleted file mode 100644
index d40624b..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
+++ /dev/null
@@ -1,76 +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 com.android.systemui.dagger.SysUISingleton
-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
-) {
- /**
- * 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: " +
- "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 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 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/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
deleted file mode 100644
index e0b6d1f..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ /dev/null
@@ -1,1243 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.ValueAnimator
-import android.annotation.IntDef
-import android.content.Context
-import android.content.res.Configuration
-import android.database.ContentObserver
-import android.graphics.Rect
-import android.net.Uri
-import android.os.Handler
-import android.os.UserHandle
-import android.provider.Settings
-import android.util.Log
-import android.util.MathUtils
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewGroupOverlay
-import androidx.annotation.VisibleForTesting
-import com.android.keyguard.KeyguardViewController
-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.dreams.DreamOverlayStateController
-import com.android.systemui.keyguard.WakefulnessLifecycle
-import com.android.systemui.media.dream.MediaDreamComplication
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.shade.NotifPanelEvents
-import com.android.systemui.statusbar.CrossFadeHelper
-import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator
-import com.android.systemui.statusbar.phone.KeyguardBypassController
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.LargeScreenUtils
-import com.android.systemui.util.animation.UniqueObjectHostView
-import com.android.systemui.util.settings.SecureSettings
-import com.android.systemui.util.traceSection
-import javax.inject.Inject
-
-private val TAG: String = MediaHierarchyManager::class.java.simpleName
-
-/**
- * Similarly to isShown but also excludes views that have 0 alpha
- */
-val View.isShownNotFaded: Boolean
- get() {
- var current: View = this
- while (true) {
- if (current.visibility != View.VISIBLE) {
- return false
- }
- if (current.alpha == 0.0f) {
- return false
- }
- val parent = current.parent ?: return false // We are not attached to the view root
- if (parent !is View) {
- // we reached the viewroot, hurray
- return true
- }
- current = parent
- }
- }
-
-/**
- * This manager is responsible for placement of the unique media view between the different hosts
- * and animate the positions of the views to achieve seamless transitions.
- */
-@SysUISingleton
-class MediaHierarchyManager @Inject constructor(
- private val context: Context,
- private val statusBarStateController: SysuiStatusBarStateController,
- private val keyguardStateController: KeyguardStateController,
- private val bypassController: KeyguardBypassController,
- private val mediaCarouselController: MediaCarouselController,
- private val keyguardViewController: KeyguardViewController,
- private val dreamOverlayStateController: DreamOverlayStateController,
- configurationController: ConfigurationController,
- wakefulnessLifecycle: WakefulnessLifecycle,
- panelEventsEvents: NotifPanelEvents,
- private val secureSettings: SecureSettings,
- @Main private val handler: Handler,
-) {
-
- /**
- * 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)
-
- /**
- * Whether we "skip" QQS during panel expansion.
- *
- * This means that when expanding the panel we go directly to QS. Also when we are on QS and
- * start closing the panel, it fully collapses instead of going to QQS.
- */
- private var skipQqsOnExpansion: Boolean = false
-
- /**
- * 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.
- */
- private var rootOverlay: ViewGroupOverlay? = null
-
- private var rootView: View? = null
- private var currentBounds = Rect()
- private var animationStartBounds: Rect = Rect()
-
- private var animationStartClipping = Rect()
- private var currentClipping = Rect()
- private var targetClipping = Rect()
-
- /**
- * The cross fade progress at the start of the animation. 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.
- */
- private var animationStartCrossFadeProgress = 0.0f
-
- /**
- * 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 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)
- }
- interpolateBounds(animationStartBounds, targetBounds, boundsProgress,
- result = currentBounds)
- resolveClipping(currentClipping)
- applyState(currentBounds, currentAlpha, clipBounds = currentClipping)
- }
- addListener(object : AnimatorListenerAdapter() {
- private var cancelled: Boolean = false
-
- override fun onAnimationCancel(animation: Animator?) {
- cancelled = true
- animationPending = false
- rootView?.removeCallbacks(startAnimation)
- }
-
- override fun onAnimationEnd(animation: Animator?) {
- isCrossFadeAnimatorRunning = false
- if (!cancelled) {
- applyTargetStateIfNotAnimating()
- }
- }
-
- override fun onAnimationStart(animation: Animator?) {
- cancelled = false
- animationPending = false
- }
- })
- }
-
- private fun resolveClipping(result: Rect) {
- if (animationStartClipping.isEmpty) result.set(targetClipping)
- else if (targetClipping.isEmpty) result.set(animationStartClipping)
- else result.setIntersect(animationStartClipping, targetClipping)
- }
-
- 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.
- */
- @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].
- */
- @MediaLocation
- private var currentAttachmentLocation = -1
-
- private var inSplitShade = false
-
- /**
- * 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?
- */
- private var animationPending: Boolean = false
- private val startAnimation: Runnable = Runnable { animator.start() }
-
- /**
- * The expansion of quick settings
- */
- var qsExpansion: Float = 0.0f
- set(value) {
- if (field != value) {
- field = value
- updateDesiredLocation()
- if (getQSTransformationProgress() >= 0) {
- updateTargetState()
- applyTargetStateIfNotAnimating()
- }
- }
- }
-
- /**
- * Is quick setting expanded?
- */
- var qsExpanded: Boolean = false
- set(value) {
- if (field != value) {
- field = value
- mediaCarouselController.mediaCarouselScrollHandler.qsExpanded = value
- }
- // qs is expanded on LS shade and HS shade
- if (value && (isLockScreenShadeVisibleToUser() || isHomeScreenShadeVisibleToUser())) {
- mediaCarouselController.logSmartspaceImpression(value)
- }
- mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
- }
-
- /**
- * distance that the full shade transition takes in order for media to fully transition to the
- * shade
- */
- 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.
- */
- private var fullShadeTransitionProgress = 0f
- set(value) {
- if (field == value) {
- return
- }
- field = value
- if (bypassController.bypassEnabled || statusbarState != StatusBarState.KEYGUARD) {
- // No need to do all the calculations / updates below if we're not on the lockscreen
- // or if we're bypassing.
- return
- }
- updateDesiredLocation(forceNoAnimation = isCurrentlyFading())
- if (value >= 0) {
- updateTargetState()
- // Setting the alpha directly, as the below call will use it to update the alpha
- carouselAlpha = calculateAlphaFromCrossFade(field)
- applyTargetStateIfNotAnimating()
- }
- }
-
- /**
- * Is there currently a cross-fade animation running driven by an animator?
- */
- private var isCrossFadeAnimatorRunning = false
-
- /**
- * Are we currently transitionioning from the lockscreen to the full shade
- * [StatusBarState.SHADE_LOCKED] or [StatusBarState.SHADE]. Once the user has dragged down and
- * the transition starts, this will no longer return true.
- */
- private val isTransitioningToFullShade: Boolean
- 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
- * shade. 0.0f means we're not transitioning yet.
- */
- fun setTransitionToFullShadeAmount(value: Float) {
- // If we're transitioning starting on the shade_locked, we don't want any delay and rather
- // have it aligned with the rest of the animation
- val progress = MathUtils.saturate(value / distanceForFullShadeTransition)
- fullShadeTransitionProgress = progress
- }
-
- /**
- * Returns the amount of translationY of the media container, during the current guided
- * transformation, if running. If there is no guided transformation running, it will return 0.
- */
- fun getGuidedTransformationTranslationY(): Int {
- if (!isCurrentlyInGuidedTransformation()) {
- return -1
- }
- val startHost = getHost(previousLocation) ?: return 0
- return targetBounds.top - startHost.currentBounds.top
- }
-
- /**
- * Is the shade currently collapsing from the expanded qs? If we're on the lockscreen and in qs,
- * we wouldn't want to transition in that case.
- */
- var collapsingShadeFromQS: Boolean = false
- set(value) {
- if (field != value) {
- field = value
- updateDesiredLocation(forceNoAnimation = true)
- }
- }
-
- /**
- * Are location changes currently blocked?
- */
- private val blockLocationChanges: Boolean
- get() {
- return goingToSleep || dozeAnimationRunning
- }
-
- /**
- * Are we currently going to sleep
- */
- private var goingToSleep: Boolean = false
- set(value) {
- if (field != value) {
- field = value
- if (!value) {
- updateDesiredLocation()
- }
- }
- }
-
- /**
- * Are we currently fullyAwake
- */
- private var fullyAwake: Boolean = false
- set(value) {
- if (field != value) {
- field = value
- if (value) {
- updateDesiredLocation(forceNoAnimation = true)
- }
- }
- }
-
- /**
- * Is the doze animation currently Running
- */
- private var dozeAnimationRunning: Boolean = false
- private set(value) {
- if (field != value) {
- field = value
- if (!value) {
- updateDesiredLocation()
- }
- }
- }
-
- /**
- * Is the dream overlay currently active
- */
- private var dreamOverlayActive: Boolean = false
- private set(value) {
- if (field != value) {
- field = value
- updateDesiredLocation(forceNoAnimation = true)
- }
- }
-
- /**
- * Is the dream media complication currently active
- */
- private var dreamMediaComplicationActive: Boolean = false
- private set(value) {
- if (field != value) {
- field = value
- updateDesiredLocation(forceNoAnimation = 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.
- */
- private var carouselAlpha: Float = 1.0f
- set(value) {
- if (field == value) {
- return
- }
- field = value
- CrossFadeHelper.fadeIn(mediaFrame, value)
- }
-
- /**
- * 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.
- */
- private fun calculateAlphaFromCrossFade(crossFadeProgress: Float): Float {
- if (crossFadeProgress <= 0.5f) {
- return 1.0f - crossFadeProgress / 0.5f
- } else {
- return (crossFadeProgress - 0.5f) / 0.5f
- }
- }
-
- 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)
- }
- mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
- }
-
- override fun onDozeAmountChanged(linear: Float, eased: Float) {
- dozeAnimationRunning = linear != 0.0f && linear != 1.0f
- }
-
- 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 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
- }
- }
-
- override fun onStateChanged() {
- dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it }
- }
- })
-
- wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
- override fun onFinishedGoingToSleep() {
- goingToSleep = false
- }
-
- override fun onStartedGoingToSleep() {
- goingToSleep = true
- fullyAwake = false
- }
-
- override fun onFinishedWakingUp() {
- goingToSleep = false
- fullyAwake = true
- }
-
- override fun onStartedWakingUp() {
- goingToSleep = false
- }
- })
-
- mediaCarouselController.updateUserVisibility = {
- mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
- }
- mediaCarouselController.updateHostVisibility = {
- mediaHosts.forEach {
- it?.updateViewVisibility()
- }
- }
-
- 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 =
- secureSettings.getBoolForUser(
- Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
- true,
- UserHandle.USER_CURRENT
- )
- }
- }
- }
- secureSettings.registerContentObserverForUser(
- Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
- settingsObserver,
- UserHandle.USER_ALL)
- }
-
- private fun updateConfiguration() {
- 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.
- *
- * @return the hostView associated with this location
- */
- fun register(mediaObject: MediaHost): UniqueObjectHostView {
- val viewHost = createUniqueObjectHost()
- mediaObject.hostView = viewHost
- mediaObject.addVisibilityChangeListener {
- // If QQS changes visibility, we need to force an update to ensure the transition
- // goes into the correct state
- val stateUpdate = mediaObject.location == LOCATION_QQS
-
- // Never animate because of a visibility change, only state changes should do that
- updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate)
- }
- mediaHosts[mediaObject.location] = mediaObject
- if (mediaObject.location == desiredLocation) {
- // In case we are overriding a view that is already visible, make sure we attach it
- // to this new host view in the below call
- desiredLocation = -1
- }
- if (mediaObject.location == currentAttachmentLocation) {
- currentAttachmentLocation = -1
- }
- updateDesiredLocation()
- return viewHost
- }
-
- /**
- * 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.removeOnAttachStateChangeListener(this)
- }
-
- override fun onViewDetachedFromWindow(p0: View?) {
- }
- })
- return viewHost
- }
-
- /**
- * Updates the location that the view should be in. If it changes, an animation may be triggered
- * going from the old desired location to the new one.
- *
- * @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
- }
- }
- 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)
- }
- 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 {
- // 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
- }
- 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()
- }
- }
-
- private fun shouldAnimateTransition(
- @MediaLocation currentLocation: Int,
- @MediaLocation previousLocation: Int
- ): Boolean {
- if (isCurrentlyInGuidedTransformation()) {
- return false
- }
- if (skipQqsOnExpansion) {
- return false
- }
- // 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) {
- return false
- }
-
- if (currentLocation == LOCATION_QQS &&
- previousLocation == LOCATION_LOCKSCREEN &&
- (statusBarStateController.leaveOpenOnKeyguardHide() ||
- 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)) {
- // We're always fading from lockscreen to keyguard in situations where the player
- // is already fully hidden
- return false
- }
- return mediaFrame.isShownNotFaded || animator.isRunning || animationPending
- }
-
- private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
- val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
- animator.apply {
- duration = animDuration
- startDelay = delay
- }
- }
-
- private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> {
- var animDuration = 200L
- 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) {
- delay = keyguardStateController.keyguardFadingAwayDelay
- }
- animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong()
- } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
- animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
- }
- return animDuration to delay
- }
-
- private fun applyTargetStateIfNotAnimating() {
- if (!animator.isRunning) {
- // Let's immediately apply the target state (which is interpolated) if there is
- // no animation running. Otherwise the animation update will already update
- // the location
- applyState(targetBounds, carouselAlpha, clipBounds = targetClipping)
- }
- }
-
- /**
- * 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) {
- 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
- // be undefined
- if (!endHost.visible) {
- endHost = starthost
- } else if (!starthost.visible) {
- starthost = endHost
- }
- val newBounds = endHost.currentBounds
- val previousBounds = starthost.currentBounds
- targetBounds = interpolateBounds(previousBounds, newBounds, progress)
- targetClipping = endHost.currentClipping
- } else if (endHost != null) {
- val bounds = endHost.currentBounds
- targetBounds.set(bounds)
- targetClipping = endHost.currentClipping
- }
- }
-
- private fun interpolateBounds(
- startBounds: Rect,
- endBounds: Rect,
- 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 resultBounds = result ?: Rect()
- resultBounds.set(left, top, right, bottom)
- return resultBounds
- }
-
- /** @return true if this transformation is guided by an external progress like a finger */
- fun isCurrentlyInGuidedTransformation(): Boolean {
- return hasValidStartAndEndLocations() &&
- getTransformationProgress() >= 0 &&
- areGuidedTransitionHostsVisible()
- }
-
- private fun hasValidStartAndEndLocations(): Boolean {
- return previousLocation != -1 && desiredLocation != -1
- }
-
- /**
- * Calculate the transformation type for the current animation
- */
- @VisibleForTesting
- @TransformationType
- fun calculateTransformationType(): Int {
- if (isTransitioningToFullShade) {
- if (inSplitShade && areGuidedTransitionHostsVisible()) {
- return TRANSFORMATION_TYPE_TRANSITION
- }
- return TRANSFORMATION_TYPE_FADE
- }
- 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
- }
- if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
- // animating between ls and qqs should fade when dragging down via e.g. expand button
- return TRANSFORMATION_TYPE_FADE
- }
- return TRANSFORMATION_TYPE_TRANSITION
- }
-
- private fun areGuidedTransitionHostsVisible(): Boolean {
- return getHost(previousLocation)?.visible == true &&
- getHost(desiredLocation)?.visible == true
- }
-
- /**
- * @return the current transformation progress if we're in a guided transformation and -1
- * otherwise
- */
- private fun getTransformationProgress(): Float {
- if (skipQqsOnExpansion) {
- return -1.0f
- }
- val progress = getQSTransformationProgress()
- if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) {
- return progress
- }
- if (isTransitioningToFullShade) {
- return fullShadeTransitionProgress
- }
- return -1.0f
- }
-
- private fun getQSTransformationProgress(): Float {
- val currentHost = getHost(desiredLocation)
- val previousHost = getHost(previousLocation)
- if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) {
- if (previousHost?.location == LOCATION_QQS) {
- if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) {
- return qsExpansion
- }
- }
- }
- return -1.0f
- }
-
- private fun getHost(@MediaLocation location: Int): MediaHost? {
- if (location < 0) {
- return null
- }
- return mediaHosts[location]
- }
-
- private fun cancelAnimationAndApplyDesiredState() {
- animator.cancel()
- getHost(desiredLocation)?.let {
- applyState(it.currentBounds, alpha = 1.0f, immediately = true)
- }
- }
-
- /**
- * 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(
- currentBounds.left,
- currentBounds.top,
- currentBounds.right,
- 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
- }
- }
- 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)
-
- // 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}")
- }
- }
- }
- 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.
- */
- private fun resolveLocationForFading(): Int {
- if (isCrossFadeAnimatorRunning) {
- // When animating between two hosts with a fade, let's keep ourselves in the old
- // location for the first half, and then switch over to the end location
- if (animationCrossFadeProgress > 0.5 || previousLocation == -1) {
- return crossFadeAnimationEndLocation
- } else {
- return crossFadeAnimationStartLocation
- }
- }
- return desiredLocation
- }
-
- private fun isTransitionRunning(): Boolean {
- return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
- animator.isRunning || animationPending
- }
-
- @MediaLocation
- private fun calculateLocation(): Int {
- if (blockLocationChanges) {
- // 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
- }
- // 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) {
- return LOCATION_QS
- }
- 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) {
- // 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
- return LOCATION_LOCKSCREEN
- }
- if (skipQqsOnExpansion) {
- // When doing an immediate expand or collapse, we want to keep it in QS.
- return LOCATION_QS
- }
- return location
- }
-
- private fun isSplitShadeExpanding(): Boolean {
- return inSplitShade && isTransitioningToFullShade
- }
-
- /**
- * Are we currently transforming to the full shade and already in QQS
- */
- private fun isTransformingToFullShadeAndInQQS(): Boolean {
- if (!isTransitioningToFullShade) {
- return false
- }
- if (inSplitShade) {
- // Split shade doesn't use QQS.
- return false
- }
- return fullShadeTransitionProgress > 0.5f
- }
-
- /**
- * Is the current transformationType fading
- */
- private fun isCurrentlyFading(): Boolean {
- if (isSplitShadeExpanding()) {
- // Split shade always uses transition instead of fade.
- return false
- }
- if (isTransitioningToFullShade) {
- return true
- }
- return isCrossFadeAnimatorRunning
- }
-
- /**
- * Returns true when the media card could be visible to the user if existed.
- */
- private fun isVisibleToUser(): Boolean {
- return isLockScreenVisibleToUser() || isLockScreenShadeVisibleToUser() ||
- isHomeScreenShadeVisibleToUser()
- }
-
- private fun isLockScreenVisibleToUser(): Boolean {
- return !statusBarStateController.isDozing &&
- !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))
- }
-
- private fun isHomeScreenShadeVisibleToUser(): Boolean {
- return !statusBarStateController.isDozing &&
- statusBarStateController.state == StatusBarState.SHADE &&
- statusBarStateController.isExpanded
- }
-
- companion object {
- /**
- * Attached in expanded quick settings
- */
- const val LOCATION_QS = 0
-
- /**
- * Attached in the collapsed QS
- */
- const val LOCATION_QQS = 1
-
- /**
- * Attached on the lock screen
- */
- const val LOCATION_LOCKSCREEN = 2
-
- /**
- * Attached on the dream overlay
- */
- const val LOCATION_DREAM_OVERLAY = 3
-
- /**
- * Attached at the root of the hierarchy in an overlay
- */
- const val IN_OVERLAY = -1000
-
- /**
- * The default transformation type where the hosts transform into each other using a direct
- * transition
- */
- const val TRANSFORMATION_TYPE_TRANSITION = 0
-
- /**
- * A transformation type where content fades from one place to another instead of
- * transitioning
- */
- const val TRANSFORMATION_TYPE_FADE = 1
- }
-}
-private val EMPTY_RECT = Rect()
-
-@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])
-@Retention(AnnotationRetention.SOURCE)
-annotation class MediaLocation
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
deleted file mode 100644
index aea2934..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.util.animation.MeasurementOutput
-import com.android.systemui.util.traceSection
-import javax.inject.Inject
-
-/**
- * A class responsible for managing all media host states of the various host locations and
- * coordinating the heights among different players. This class can be used to get the most up to
- * date state for any location.
- */
-@SysUISingleton
-class MediaHostStatesManager @Inject constructor() {
-
- private val callbacks: MutableSet<Callback> = mutableSetOf()
- private val controllers: MutableSet<MediaViewController> = mutableSetOf()
-
- /**
- * The overall sizes of the carousel. This is needed to make sure all players in the carousel
- * have equal size.
- */
- val carouselSizes: MutableMap<Int, MeasurementOutput> = mutableMapOf()
-
- /**
- * 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.
- */
- 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)
- }
- }
- }
-
- /**
- * 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
- }
- }
- }
- carouselSizes[location] = result
- return result
- }
-
- /**
- * 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
- */
- 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
- */
- fun addController(controller: MediaViewController) {
- controllers.add(controller)
- }
-
- /**
- * Notify the manager about the removal of a controller.
- */
- fun removeController(controller: MediaViewController) {
- controllers.remove(controller)
- }
-
- interface Callback {
- /**
- * Notify the callbacks that a media state for a host has changed, and that the
- * corresponding view states should be updated and applied
- */
- fun onHostStateChanged(@MediaLocation location: Int, mediaHostState: MediaHostState)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index 1ac2a07..be357ee 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 @@
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/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt
deleted file mode 100644
index 00273bc..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-package com.android.systemui.media
-
-import android.content.Context
-import android.os.SystemClock
-import android.util.AttributeSet
-import android.view.InputDevice
-import android.view.MotionEvent
-import android.view.ViewGroup
-import android.widget.HorizontalScrollView
-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
- */
-class MediaScrollView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0
-)
- : HorizontalScrollView(context, attrs, defStyleAttr) {
-
- lateinit var contentContainer: ViewGroup
- private set
- var touchListener: Gefingerpoken? = null
-
- /**
- * The target value of the translation X animation. Only valid if the physicsAnimator is running
- */
- var animationTargetX = 0.0f
-
- /**
- * 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
- }
-
- /**
- * 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.
- */
- 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.
- */
- var relativeScrollX: Int
- get() = transformScrollX(scrollX)
- set(value) {
- scrollX = transformScrollX(value)
- }
-
- /**
- * 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
- val oldY: Int = mScrollY
- mScrollX = x
- mScrollY = y
- invalidateParentCaches()
- onScrollChanged(mScrollX, mScrollY, oldX, oldY)
- if (!awakenScrollBars()) {
- postInvalidateOnAnimation()
- }
- }
- }
-
- override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
- var intercept = false
- 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)
- }
- return super.onTouchEvent(ev) || touch
- }
-
- override fun onFinishInflate() {
- super.onFinishInflate()
- contentContainer = getChildAt(0) as ViewGroup
- }
-
- override fun overScrollBy(
- deltaX: Int,
- deltaY: Int,
- scrollX: Int,
- scrollY: Int,
- scrollRangeX: Int,
- scrollRangeY: Int,
- maxOverScrollX: Int,
- maxOverScrollY: Int,
- isTouchEvent: Boolean
- ): Boolean {
- if (getContentTranslation() != 0.0f) {
- // When we're dismissing we ignore all the scrolling
- return false
- }
- return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
- scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent)
- }
-
- /**
- * 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)
- 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/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
deleted file mode 100644
index 8c9e2d8..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
+++ /dev/null
@@ -1,161 +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.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"
- }
- )
-}
\ 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/MediaViewController.kt
deleted file mode 100644
index faa7aae..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
+++ /dev/null
@@ -1,615 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.media
-
-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.statusbar.policy.ConfigurationController
-import com.android.systemui.util.animation.MeasurementOutput
-import com.android.systemui.util.animation.TransitionLayout
-import com.android.systemui.util.animation.TransitionLayoutController
-import com.android.systemui.util.animation.TransitionViewState
-import com.android.systemui.util.traceSection
-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(
- private val context: Context,
- private val configurationController: ConfigurationController,
- private val mediaHostStatesManager: MediaHostStatesManager,
- private val logger: MediaViewLogger
-) {
-
- /**
- * Indicating that the media view controller is for a notification-based player,
- * session-based player, or recommendation
- */
- enum class TYPE {
- PLAYER, RECOMMENDATION
- }
-
- companion object {
- @JvmField
- val GUTS_ANIMATION_DURATION = 500L
- val controlIds = setOf(
- R.id.media_progress_bar,
- R.id.actionNext,
- R.id.actionPrev,
- 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 detailIds = setOf(
- R.id.header_title,
- R.id.header_artist,
- R.id.actionPlayPause,
- )
- }
-
- /**
- * 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
- private val layoutController = TransitionLayoutController()
- private var animationDelay: Long = 0
- private var animationDuration: Long = 0
- private var animateNextStateChange: Boolean = false
- private val measurement = MeasurementOutput(0, 0)
- private var type: TYPE = TYPE.PLAYER
-
- /**
- * 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
-
- /**
- * 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
- */
- private var currentTransitionProgress: Float = 1.0f
-
- /**
- * A temporary state used to store intermediate measurements.
- */
- private val tmpState = TransitionViewState()
-
- /**
- * A temporary state used to store intermediate measurements.
- */
- private val tmpState2 = TransitionViewState()
-
- /**
- * A temporary state used to store intermediate measurements.
- */
- private val tmpState3 = TransitionViewState()
-
- /**
- * 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
- */
- 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
- */
- var currentHeight: Int = 0
-
- /**
- * 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
- */
- 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 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)
- }
- }
- }
-
- /**
- * The expanded constraint set used to render a expanded player. If it is modified, make sure
- * to call [refreshState]
- */
- val collapsedLayout = ConstraintSet()
-
- /**
- * The expanded constraint set used to render a collapsed player. If it is modified, make sure
- * to call [refreshState]
- */
- val expandedLayout = ConstraintSet()
-
- /**
- * Whether the guts are visible for the associated player.
- */
- var isGutsVisible = false
- private set
-
- init {
- mediaHostStatesManager.addController(this)
- layoutController.sizeChangedListener = { width: Int, height: Int ->
- currentWidth = width
- currentHeight = height
- sizeChangedListener.invoke()
- }
- configurationController.addCallback(configurationListener)
- }
-
- /**
- * Notify this controller that the view has been removed and all listeners should be destroyed
- */
- fun onDestroy() {
- mediaHostStatesManager.removeController(this)
- configurationController.removeCallback(configurationListener)
- }
-
- /**
- * Show guts with an animated transition.
- */
- fun openGuts() {
- if (isGutsVisible) return
- isGutsVisible = true
- animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
- setCurrentState(currentStartLocation,
- currentEndLocation,
- currentTransitionProgress,
- applyImmediately = false)
- }
-
- /**
- * Close the guts for the associated player.
- *
- * @param immediate if `false`, it will animate the transition.
- */
- @JvmOverloads
- fun closeGuts(immediate: Boolean = false) {
- if (!isGutsVisible) return
- isGutsVisible = false
- if (!immediate) {
- animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
- }
- setCurrentState(currentStartLocation,
- currentEndLocation,
- currentTransitionProgress,
- applyImmediately = immediate)
- }
-
- private fun ensureAllMeasurements() {
- val mediaStates = mediaHostStatesManager.mediaHostStates
- for (entry in mediaStates) {
- obtainViewState(entry.value)
- }
- }
-
- /**
- * Get the constraintSet for a given expansion
- */
- private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
- 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 gutsIds = GutsViewHolder.ids
- controlsIds.forEach { id ->
- viewState.widgetStates.get(id)?.let { state ->
- // Make sure to use the unmodified state if guts are not visible.
- state.alpha = if (isGutsVisible) 0f else state.alpha
- state.gone = if (isGutsVisible) true else state.gone
- }
- }
- gutsIds.forEach { id ->
- viewState.widgetStates.get(id)?.let { state ->
- // Make sure to use the unmodified state if guts are visible
- state.alpha = if (isGutsVisible) state.alpha else 0f
- state.gone = if (isGutsVisible) state.gone else true
- }
- }
- }
-
- /**
- * Apply squishFraction to a copy of viewState such that the cached version is untouched.
- */
- internal fun squishViewState(
- viewState: TransitionViewState,
- squishFraction: Float
- ): TransitionViewState {
- val squishedViewState = viewState.copy()
- squishedViewState.height = (squishedViewState.height * squishFraction).toInt()
- controlIds.forEach { id ->
- squishedViewState.widgetStates.get(id)?.let { state ->
- state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION)
- }
- }
-
- detailIds.forEach { id ->
- squishedViewState.widgetStates.get(id)?.let { state ->
- state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION)
- }
- }
-
- RecommendationViewHolder.mediaContainersIds.forEach { id ->
- squishedViewState.widgetStates.get(id)?.let { state ->
- state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION)
- }
- }
-
- RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id ->
- squishedViewState.widgetStates.get(id)?.let { state ->
- state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION)
- }
- }
-
- return squishedViewState
- }
-
- /**
- * 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? {
- if (state == null || state.measurementInput == null) {
- return null
- }
- // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey
- var cacheKey = getKey(state, isGutsVisible, tmpKey)
- val viewState = viewStates[cacheKey]
- if (viewState != null) {
- // we already have cached this measurement, let's continue
- if (state.squishFraction <= 1f) {
- return squishViewState(viewState, state.squishFraction)
- }
- return viewState
- }
- // Copy the key since this might call recursively into it and we're using tmpKey
- cacheKey = cacheKey.copy()
- val result: TransitionViewState?
-
- if (transitionLayout == null) {
- return null
- }
- // Let's create a new measurement
- if (state.expansion == 0.0f || state.expansion == 1.0f) {
- result = transitionLayout!!.calculateViewState(
- state.measurementInput!!,
- constraintSetForExpansion(state.expansion),
- TransitionViewState())
-
- setGutsViewState(result)
- // We don't want to cache interpolated or null states as this could quickly fill up
- // our cache. We only cache the start and the end states since the interpolation
- // is cheap
- viewStates[cacheKey] = result
- } else {
- // This is an interpolated state
- val startState = state.copy().also { it.expansion = 0.0f }
-
- // Given that we have a measurement and a view, let's get (guaranteed) viewstates
- // from the start and end state and interpolate them
- 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)
- }
- if (state.squishFraction <= 1f) {
- return squishViewState(result, state.squishFraction)
- }
- return result
- }
-
- private fun getKey(
- state: MediaHostState,
- guts: Boolean,
- result: CacheKey
- ): CacheKey {
- result.apply {
- heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0
- widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0
- expansion = state.expansion
- gutsVisible = guts
- }
- return result
- }
-
- /**
- * 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(
- startLocation = currentStartLocation,
- endLocation = currentEndLocation,
- transitionProgress = currentTransitionProgress,
- 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.
- */
- 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
- * locations.
- */
- fun setCurrentState(
- @MediaLocation startLocation: Int,
- @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)
-
- 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 {
- result = layoutController.getInterpolatedState(startViewState, endViewState,
- transitionProgress, tmpState)
- }
- logger.logMediaSize("setCurrentState", result.width, result.height)
- layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
- animationDelay)
- }
-
- private fun updateViewStateToCarouselSize(
- viewState: TransitionViewState?,
- location: Int,
- outState: TransitionViewState
- ): TransitionViewState? {
- val result = viewState?.copy(outState) ?: return null
- val overrideSize = mediaHostStatesManager.carouselSizes[location]
- overrideSize?.let {
- // To be safe we're using a maximum here. The override size should always be set
- // properly though.
- result.height = Math.max(it.measuredHeight, result.height)
- result.width = Math.max(it.measuredWidth, result.width)
- }
- logger.logMediaSize("update to carousel", result.width, result.height)
- return result
- }
-
- private fun updateMediaViewControllerType(type: TYPE) {
- this.type = type
-
- // These XML resources contain ConstraintSets that will apply to this player type's layout
- when (type) {
- TYPE.PLAYER -> {
- collapsedLayout.load(context, R.xml.media_session_collapsed)
- expandedLayout.load(context, R.xml.media_session_expanded)
- }
- TYPE.RECOMMENDATION -> {
- collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
- expandedLayout.load(context, R.xml.media_recommendation_expanded)
- }
- }
- refreshState()
- }
-
- /**
- * 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
- * [MediaHost.visible]
- * @return State require for executing a transition, and also the respective [MediaHost].
- */
- private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? {
- val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null
- return obtainViewState(mediaHostState)
- }
-
- /**
- * Notify that the location is changing right now and a [setCurrentState] change is imminent.
- * This updates the width the view will me measured with.
- */
- fun onLocationPreChange(@MediaLocation newLocation: Int) {
- obtainViewStateForLocation(newLocation)?.let {
- layoutController.setMeasureState(it)
- }
- }
-
- /**
- * 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
- }
- setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress,
- applyImmediately = true)
- }
-}
-
-/**
- * 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,
- var expansion: Float = 0.0f,
- var gutsVisible: Boolean = false
-)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
deleted file mode 100644
index 51c658c..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
+++ /dev/null
@@ -1,63 +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 com.android.systemui.dagger.SysUISingleton
-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
- */
-@SysUISingleton
-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"
- }
- )
- }
-
- fun logMediaLocation(reason: String, startLocation: Int, endLocation: Int) {
- buffer.log(
- 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/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
deleted file mode 100644
index 8ae75fc..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.TextView
-import com.android.systemui.R
-import com.android.systemui.util.animation.TransitionLayout
-
-private const val TAG = "RecommendationViewHolder"
-
-/** ViewHolder for a Smartspace media recommendation. */
-class RecommendationViewHolder private constructor(itemView: View) {
-
- val recommendations = itemView as TransitionLayout
-
- // 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 gutsViewHolder = GutsViewHolder(itemView)
-
- init {
- (recommendations.background as IlluminationDrawable).let { background ->
- mediaCoverContainers.forEach { background.registerLightSource(it) }
- background.registerLightSource(gutsViewHolder.cancel)
- background.registerLightSource(gutsViewHolder.dismiss)
- background.registerLightSource(gutsViewHolder.settings)
- }
- }
-
- fun marquee(start: Boolean, delay: Long) {
- gutsViewHolder.marquee(start, delay, TAG)
- }
-
- companion object {
- /**
- * Creates a RecommendationViewHolder.
- *
- * @param inflater LayoutInflater to use to inflate the layout.
- * @param parent Parent of inflated view.
- */
- @JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup):
- RecommendationViewHolder {
- val itemView =
- inflater.inflate(
- R.layout.media_smartspace_recommendations,
- parent,
- 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.
- itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
- return RecommendationViewHolder(itemView)
- }
-
- // 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 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
- )
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
deleted file mode 100644
index a9c5c61..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
+++ /dev/null
@@ -1,74 +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.content.ComponentName
-import com.android.systemui.dagger.SysUISingleton
-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
-) {
- /** 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" }
- )
-
- /** 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" }
- )
-
- /**
- * 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.
- */
- 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/GutsViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/GutsViewHolder.kt
similarity index 91%
rename from packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/GutsViewHolder.kt
index 73240b5..5315067 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.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 @@
}
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
similarity index 67%
rename from packages/SystemUI/src/com/android/systemui/media/MediaData.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
index 5b2cda0..f006442 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 @@
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 @@
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 @@
}
}
-/**
- * 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 @@
/** 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 @@
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
similarity index 82%
rename from packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
index fc9515c..2511324 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.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 @@
// 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 @@
}
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 @@
* @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 @@
}
}
- val controlsIds = setOf(
+ val controlsIds =
+ setOf(
R.id.icon,
R.id.app_name,
R.id.header_title,
@@ -142,27 +131,23 @@
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 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 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
similarity index 68%
rename from packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt
index 121021f..37d956b 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 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 @@
}
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 @@
}
}
- 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
similarity index 74%
rename from packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
index 0f78a1e..bba5f35 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 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 @@
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 @@
}
/** 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 @@
}
_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 @@
}
}
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)) {
+ 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()
- } else {
- checkIfPollingNeeded()
}
}
-
- override fun onSessionDestroyed() {
- clearController()
- }
- }
private var cancel: Runnable? = null
/** Indicates if the seek interaction is considered a false guesture. */
@@ -121,12 +122,13 @@
/** 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 @@
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 @@
* @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 @@
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 @@
* 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 @@
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 @@
// 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 @@
/**
* 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 @@
// 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/controls/models/recommendation/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
new file mode 100644
index 0000000..1a10b18
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.models.recommendation
+
+import android.view.LayoutInflater
+import android.view.View
+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"
+
+/** ViewHolder for a Smartspace media recommendation. */
+class RecommendationViewHolder private constructor(itemView: View) {
+
+ val recommendations = itemView as TransitionLayout
+
+ // 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 gutsViewHolder = GutsViewHolder(itemView)
+
+ init {
+ (recommendations.background as IlluminationDrawable).let { background ->
+ mediaCoverContainers.forEach { background.registerLightSource(it) }
+ background.registerLightSource(gutsViewHolder.cancel)
+ background.registerLightSource(gutsViewHolder.dismiss)
+ background.registerLightSource(gutsViewHolder.settings)
+ }
+ }
+
+ fun marquee(start: Boolean, delay: Long) {
+ gutsViewHolder.marquee(start, delay, TAG)
+ }
+
+ companion object {
+ /**
+ * Creates a RecommendationViewHolder.
+ *
+ * @param inflater LayoutInflater to use to inflate the layout.
+ * @param parent Parent of inflated view.
+ */
+ @JvmStatic
+ fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder {
+ val itemView =
+ inflater.inflate(
+ R.layout.media_smartspace_recommendations,
+ parent,
+ 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.
+ itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
+ return RecommendationViewHolder(itemView)
+ }
+
+ // 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 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
+ )
+ }
+}
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
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
index c8f17d9..1df42c6 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.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 @@
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
similarity index 71%
rename from packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataProvider.kt
index 140a1fe..a7ed69a 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 @@
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
similarity index 88%
rename from packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt
index 94a0835..ff763d8 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
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatest.kt
index 311973a..789ef40 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 @@
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 @@
*/
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 @@
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
similarity index 71%
rename from packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
index e0c8d66..45b319b 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.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 @@
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 @@
* 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 @@
* 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
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 @@
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
similarity index 68%
rename from packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
index 896fb47..14dd990 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.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 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 @@
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 @@
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 @@
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 @@
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)
+ ) : 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)
+ 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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
}
/**
- * 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 @@
}
}
- /**
- * 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 @@
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 @@
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 @@
}
/**
- * 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
}
}
- /**
- * 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 @@
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 @@
* @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 @@
}
// 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 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 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 @@
* @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 @@
}
}
- /**
- * 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 @@
)
}
- /**
- * 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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
}
}
- /**
- * 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 @@
): 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 @@
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 @@
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
similarity index 70%
rename from packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
index b3a4ddf..6a512be 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.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 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 @@
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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
@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 @@
// 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 @@
* [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
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
index 3179296..ab93b29 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.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 @@
* 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 @@
// 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 @@
// 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 @@
}
}
- /**
- * 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 @@
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 @@
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
- backgroundExecutor.execute {
- dispatchSmartspaceMediaDataLoaded(key, data)
- }
+ backgroundExecutor.execute { dispatchSmartspaceMediaDataLoaded(key, data) }
}
override fun onMediaDataRemoved(key: String) {
@@ -160,9 +160,7 @@
}
override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
- backgroundExecutor.execute {
- dispatchSmartspaceMediaDataRemoved(key, immediately)
- }
+ backgroundExecutor.execute { dispatchSmartspaceMediaDataRemoved(key, immediately) }
}
private fun dispatchMediaDataLoaded(
@@ -177,9 +175,7 @@
}
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 @@
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
similarity index 81%
rename from packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
index 93a29ef..7f5c82f 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 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 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 @@
* 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 0000000..8f3f054
--- /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
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaBrowserFactory.java
index aca033e..00620b5 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
similarity index 72%
rename from packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
index cc06b6c..4891297 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.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_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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
}
- /**
- * Load controls for resuming media, if available
- */
+ /** Load controls for resuming media, if available */
private fun loadMediaResumptionControls() {
if (!useMediaResumption) {
return
@@ -204,9 +226,7 @@
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 @@
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 @@
mediaBrowser = null
}
},
- componentName)
+ componentName
+ )
mediaBrowser?.testConnection()
}
@@ -285,9 +307,7 @@
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 @@
}
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
similarity index 99%
rename from packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java
index 40a5653..3493b24 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 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
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java
index 3d1380b..c558227 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/controls/resume/ResumeMediaBrowserLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt
new file mode 100644
index 0000000..335ce1d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.resume
+
+import android.content.ComponentName
+import com.android.systemui.dagger.SysUISingleton
+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) {
+ /** 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" }
+ )
+
+ /** 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" }
+ )
+
+ /**
+ * 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.
+ */
+ 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
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/AnimationBindHandler.kt
index 013683e..d2793bc 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 @@
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 @@
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 0000000..61ef2f1
--- /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
similarity index 72%
rename from packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt
index 121ddd4..9f86cd8 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 @@
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 @@
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 @@
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 @@
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 @@
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
similarity index 79%
rename from packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
index 32600fb..899148b 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 @@
* 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 @@
) {
init {
- statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
- override fun onStateChanged(newState: Int) {
- refreshMediaPosition()
- }
- })
- 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 =
- secureSettings.getBoolForUser(
- Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
- true,
- UserHandle.USER_CURRENT
- )
+ statusBarStateController.addCallback(
+ object : StatusBarStateController.StateListener {
+ override fun onStateChanged(newState: Int) {
refreshMediaPosition()
}
}
- }
+ )
+ 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 =
+ secureSettings.getBoolForUser(
+ Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+ true,
+ UserHandle.USER_CURRENT
+ )
+ 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 @@
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 @@
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 @@
}
}
- /**
- * 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 @@
}
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 @@
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
similarity index 64%
rename from packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt
index 711cb36..dd5c2bf 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 0000000..e38c1ba
--- /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/controls/ui/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
new file mode 100644
index 0000000..eed1bd7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
@@ -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.systemui.media.controls.ui
+
+import com.android.systemui.dagger.SysUISingleton
+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) {
+ /**
+ * 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: " +
+ "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 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 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
similarity index 67%
rename from packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
index a776897..36b2eda 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.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 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 @@
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 @@
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 @@
// 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)
+ /** 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 onScroll(
+ down: MotionEvent?,
+ lastMotion: MotionEvent?,
+ distanceX: Float,
+ distanceY: Float
+ ) = onScroll(down!!, lastMotion!!, distanceX)
- override fun onDown(e: MotionEvent?): Boolean {
- if (falsingProtectionNeeded) {
- falsingCollector.onNotificationStartDismissing()
+ 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 @@
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)
+ 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 {
- -settingsTranslation
+ if (contentTranslation > 0) {
+ settingsTranslation
+ } else {
+ scrollView.width - settingsTranslation - settingsButton.width
+ }
}
- } else {
- 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
// 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 @@
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 @@
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 @@
}
/**
- * 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 @@
}
}
- /**
- * 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 @@
}
}
- /**
- * Reset the MediaScrollView to the start.
- */
+ /** Reset the MediaScrollView to the start. */
fun scrollToStart() {
scrollView.relativeScrollX = 0
}
@@ -581,21 +566,22 @@
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
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/media/MediaColorSchemes.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt
index 208766d..82abf9b 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
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index fba51dd..18ecadb 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.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 @@
"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 @@
});
}
+ /**
+ * Clean up seekbar and controller when panel is destroyed
+ */
public void onDestroy() {
if (mSeekBarObserver != null) {
mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
@@ -651,7 +667,7 @@
return;
}
- CharSequence contentDescription;
+ CharSequence contentDescription;
if (mMediaViewController.isGutsVisible()) {
contentDescription =
mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText();
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
new file mode 100644
index 0000000..6b46d8f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
@@ -0,0 +1,1272 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.annotation.IntDef
+import android.content.Context
+import android.content.res.Configuration
+import android.database.ContentObserver
+import android.graphics.Rect
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
+import android.util.Log
+import android.util.MathUtils
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroupOverlay
+import androidx.annotation.VisibleForTesting
+import com.android.keyguard.KeyguardViewController
+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.dreams.DreamOverlayStateController
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.media.dream.MediaDreamComplication
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.NotifPanelEvents
+import com.android.systemui.statusbar.CrossFadeHelper
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.LargeScreenUtils
+import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.traceSection
+import javax.inject.Inject
+
+private val TAG: String = MediaHierarchyManager::class.java.simpleName
+
+/** Similarly to isShown but also excludes views that have 0 alpha */
+val View.isShownNotFaded: Boolean
+ get() {
+ var current: View = this
+ while (true) {
+ if (current.visibility != View.VISIBLE) {
+ return false
+ }
+ if (current.alpha == 0.0f) {
+ return false
+ }
+ val parent = current.parent ?: return false // We are not attached to the view root
+ if (parent !is View) {
+ // we reached the viewroot, hurray
+ return true
+ }
+ current = parent
+ }
+ }
+
+/**
+ * This manager is responsible for placement of the unique media view between the different hosts
+ * and animate the positions of the views to achieve seamless transitions.
+ */
+@SysUISingleton
+class MediaHierarchyManager
+@Inject
+constructor(
+ private val context: Context,
+ private val statusBarStateController: SysuiStatusBarStateController,
+ private val keyguardStateController: KeyguardStateController,
+ private val bypassController: KeyguardBypassController,
+ private val mediaCarouselController: MediaCarouselController,
+ private val keyguardViewController: KeyguardViewController,
+ private val dreamOverlayStateController: DreamOverlayStateController,
+ configurationController: ConfigurationController,
+ wakefulnessLifecycle: WakefulnessLifecycle,
+ panelEventsEvents: NotifPanelEvents,
+ private val secureSettings: SecureSettings,
+ @Main private val handler: Handler,
+) {
+
+ /** 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)
+
+ /**
+ * Whether we "skip" QQS during panel expansion.
+ *
+ * This means that when expanding the panel we go directly to QS. Also when we are on QS and
+ * start closing the panel, it fully collapses instead of going to QQS.
+ */
+ private var skipQqsOnExpansion: Boolean = false
+
+ /**
+ * 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.
+ */
+ private var rootOverlay: ViewGroupOverlay? = null
+
+ private var rootView: View? = null
+ private var currentBounds = Rect()
+ private var animationStartBounds: Rect = Rect()
+
+ private var animationStartClipping = Rect()
+ private var currentClipping = Rect()
+ private var targetClipping = Rect()
+
+ /**
+ * The cross fade progress at the start of the animation. 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.
+ */
+ private var animationStartCrossFadeProgress = 0.0f
+
+ /** 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 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)
+ }
+ interpolateBounds(
+ animationStartBounds,
+ targetBounds,
+ boundsProgress,
+ result = currentBounds
+ )
+ resolveClipping(currentClipping)
+ applyState(currentBounds, currentAlpha, clipBounds = currentClipping)
+ }
+ addListener(
+ object : AnimatorListenerAdapter() {
+ private var cancelled: Boolean = false
+
+ override fun onAnimationCancel(animation: Animator?) {
+ cancelled = true
+ animationPending = false
+ rootView?.removeCallbacks(startAnimation)
+ }
+
+ override fun onAnimationEnd(animation: Animator?) {
+ isCrossFadeAnimatorRunning = false
+ if (!cancelled) {
+ applyTargetStateIfNotAnimating()
+ }
+ }
+
+ override fun onAnimationStart(animation: Animator?) {
+ cancelled = false
+ animationPending = false
+ }
+ }
+ )
+ }
+
+ private fun resolveClipping(result: Rect) {
+ if (animationStartClipping.isEmpty) result.set(targetClipping)
+ else if (targetClipping.isEmpty) result.set(animationStartClipping)
+ else result.setIntersect(animationStartClipping, targetClipping)
+ }
+
+ 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.
+ */
+ @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].
+ */
+ @MediaLocation private var currentAttachmentLocation = -1
+
+ private var inSplitShade = false
+
+ /** 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? */
+ private var animationPending: Boolean = false
+ private val startAnimation: Runnable = Runnable { animator.start() }
+
+ /** The expansion of quick settings */
+ var qsExpansion: Float = 0.0f
+ set(value) {
+ if (field != value) {
+ field = value
+ updateDesiredLocation()
+ if (getQSTransformationProgress() >= 0) {
+ updateTargetState()
+ applyTargetStateIfNotAnimating()
+ }
+ }
+ }
+
+ /** Is quick setting expanded? */
+ var qsExpanded: Boolean = false
+ set(value) {
+ if (field != value) {
+ field = value
+ mediaCarouselController.mediaCarouselScrollHandler.qsExpanded = value
+ }
+ // qs is expanded on LS shade and HS shade
+ if (value && (isLockScreenShadeVisibleToUser() || isHomeScreenShadeVisibleToUser())) {
+ mediaCarouselController.logSmartspaceImpression(value)
+ }
+ mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
+ }
+
+ /**
+ * distance that the full shade transition takes in order for media to fully transition to the
+ * shade
+ */
+ 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.
+ */
+ private var fullShadeTransitionProgress = 0f
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ if (bypassController.bypassEnabled || statusbarState != StatusBarState.KEYGUARD) {
+ // No need to do all the calculations / updates below if we're not on the lockscreen
+ // or if we're bypassing.
+ return
+ }
+ updateDesiredLocation(forceNoAnimation = isCurrentlyFading())
+ if (value >= 0) {
+ updateTargetState()
+ // Setting the alpha directly, as the below call will use it to update the alpha
+ carouselAlpha = calculateAlphaFromCrossFade(field)
+ applyTargetStateIfNotAnimating()
+ }
+ }
+
+ /** Is there currently a cross-fade animation running driven by an animator? */
+ private var isCrossFadeAnimatorRunning = false
+
+ /**
+ * Are we currently transitionioning from the lockscreen to the full shade
+ * [StatusBarState.SHADE_LOCKED] or [StatusBarState.SHADE]. Once the user has dragged down and
+ * the transition starts, this will no longer return true.
+ */
+ private val isTransitioningToFullShade: Boolean
+ 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
+ * shade. 0.0f means we're not transitioning yet.
+ */
+ fun setTransitionToFullShadeAmount(value: Float) {
+ // If we're transitioning starting on the shade_locked, we don't want any delay and rather
+ // have it aligned with the rest of the animation
+ val progress = MathUtils.saturate(value / distanceForFullShadeTransition)
+ fullShadeTransitionProgress = progress
+ }
+
+ /**
+ * Returns the amount of translationY of the media container, during the current guided
+ * transformation, if running. If there is no guided transformation running, it will return 0.
+ */
+ fun getGuidedTransformationTranslationY(): Int {
+ if (!isCurrentlyInGuidedTransformation()) {
+ return -1
+ }
+ val startHost = getHost(previousLocation) ?: return 0
+ return targetBounds.top - startHost.currentBounds.top
+ }
+
+ /**
+ * Is the shade currently collapsing from the expanded qs? If we're on the lockscreen and in qs,
+ * we wouldn't want to transition in that case.
+ */
+ var collapsingShadeFromQS: Boolean = false
+ set(value) {
+ if (field != value) {
+ field = value
+ updateDesiredLocation(forceNoAnimation = true)
+ }
+ }
+
+ /** Are location changes currently blocked? */
+ private val blockLocationChanges: Boolean
+ get() {
+ return goingToSleep || dozeAnimationRunning
+ }
+
+ /** Are we currently going to sleep */
+ private var goingToSleep: Boolean = false
+ set(value) {
+ if (field != value) {
+ field = value
+ if (!value) {
+ updateDesiredLocation()
+ }
+ }
+ }
+
+ /** Are we currently fullyAwake */
+ private var fullyAwake: Boolean = false
+ set(value) {
+ if (field != value) {
+ field = value
+ if (value) {
+ updateDesiredLocation(forceNoAnimation = true)
+ }
+ }
+ }
+
+ /** Is the doze animation currently Running */
+ private var dozeAnimationRunning: Boolean = false
+ private set(value) {
+ if (field != value) {
+ field = value
+ if (!value) {
+ updateDesiredLocation()
+ }
+ }
+ }
+
+ /** Is the dream overlay currently active */
+ private var dreamOverlayActive: Boolean = false
+ private set(value) {
+ if (field != value) {
+ field = value
+ updateDesiredLocation(forceNoAnimation = true)
+ }
+ }
+
+ /** Is the dream media complication currently active */
+ private var dreamMediaComplicationActive: Boolean = false
+ private set(value) {
+ if (field != value) {
+ field = value
+ updateDesiredLocation(forceNoAnimation = 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. */
+ private var carouselAlpha: Float = 1.0f
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ CrossFadeHelper.fadeIn(mediaFrame, value)
+ }
+
+ /**
+ * 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.
+ */
+ private fun calculateAlphaFromCrossFade(crossFadeProgress: Float): Float {
+ if (crossFadeProgress <= 0.5f) {
+ return 1.0f - crossFadeProgress / 0.5f
+ } else {
+ return (crossFadeProgress - 0.5f) / 0.5f
+ }
+ }
+
+ 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)
+ }
+ mediaCarouselController.mediaCarouselScrollHandler.visibleToUser =
+ isVisibleToUser()
+ }
+
+ override fun onDozeAmountChanged(linear: Float, eased: Float) {
+ dozeAnimationRunning = linear != 0.0f && linear != 1.0f
+ }
+
+ 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 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
+ }
+ }
+
+ override fun onStateChanged() {
+ dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it }
+ }
+ }
+ )
+
+ wakefulnessLifecycle.addObserver(
+ object : WakefulnessLifecycle.Observer {
+ override fun onFinishedGoingToSleep() {
+ goingToSleep = false
+ }
+
+ override fun onStartedGoingToSleep() {
+ goingToSleep = true
+ fullyAwake = false
+ }
+
+ override fun onFinishedWakingUp() {
+ goingToSleep = false
+ fullyAwake = true
+ }
+
+ override fun onStartedWakingUp() {
+ goingToSleep = false
+ }
+ }
+ )
+
+ mediaCarouselController.updateUserVisibility = {
+ mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
+ }
+ mediaCarouselController.updateHostVisibility = {
+ mediaHosts.forEach { it?.updateViewVisibility() }
+ }
+
+ 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 =
+ secureSettings.getBoolForUser(
+ Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+ true,
+ UserHandle.USER_CURRENT
+ )
+ }
+ }
+ }
+ secureSettings.registerContentObserverForUser(
+ Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+ settingsObserver,
+ UserHandle.USER_ALL
+ )
+ }
+
+ private fun updateConfiguration() {
+ 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.
+ *
+ * @return the hostView associated with this location
+ */
+ fun register(mediaObject: MediaHost): UniqueObjectHostView {
+ val viewHost = createUniqueObjectHost()
+ mediaObject.hostView = viewHost
+ mediaObject.addVisibilityChangeListener {
+ // If QQS changes visibility, we need to force an update to ensure the transition
+ // goes into the correct state
+ val stateUpdate = mediaObject.location == LOCATION_QQS
+
+ // Never animate because of a visibility change, only state changes should do that
+ updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate)
+ }
+ mediaHosts[mediaObject.location] = mediaObject
+ if (mediaObject.location == desiredLocation) {
+ // In case we are overriding a view that is already visible, make sure we attach it
+ // to this new host view in the below call
+ desiredLocation = -1
+ }
+ if (mediaObject.location == currentAttachmentLocation) {
+ currentAttachmentLocation = -1
+ }
+ updateDesiredLocation()
+ return viewHost
+ }
+
+ /** 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.removeOnAttachStateChangeListener(this)
+ }
+
+ override fun onViewDetachedFromWindow(p0: View?) {}
+ }
+ )
+ return viewHost
+ }
+
+ /**
+ * Updates the location that the view should be in. If it changes, an animation may be triggered
+ * going from the old desired location to the new one.
+ *
+ * @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
+ }
+ }
+ 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)
+ }
+ 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 {
+ // 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
+ }
+ 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()
+ }
+ }
+
+ private fun shouldAnimateTransition(
+ @MediaLocation currentLocation: Int,
+ @MediaLocation previousLocation: Int
+ ): Boolean {
+ if (isCurrentlyInGuidedTransformation()) {
+ return false
+ }
+ if (skipQqsOnExpansion) {
+ return false
+ }
+ // 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
+ ) {
+ return false
+ }
+
+ if (
+ currentLocation == LOCATION_QQS &&
+ previousLocation == LOCATION_LOCKSCREEN &&
+ (statusBarStateController.leaveOpenOnKeyguardHide() ||
+ 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)
+ ) {
+ // We're always fading from lockscreen to keyguard in situations where the player
+ // is already fully hidden
+ return false
+ }
+ return mediaFrame.isShownNotFaded || animator.isRunning || animationPending
+ }
+
+ private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
+ val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
+ animator.apply {
+ duration = animDuration
+ startDelay = delay
+ }
+ }
+
+ private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> {
+ var animDuration = 200L
+ 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
+ ) {
+ delay = keyguardStateController.keyguardFadingAwayDelay
+ }
+ animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong()
+ } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
+ animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
+ }
+ return animDuration to delay
+ }
+
+ private fun applyTargetStateIfNotAnimating() {
+ if (!animator.isRunning) {
+ // Let's immediately apply the target state (which is interpolated) if there is
+ // no animation running. Otherwise the animation update will already update
+ // the location
+ applyState(targetBounds, carouselAlpha, clipBounds = targetClipping)
+ }
+ }
+
+ /** 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
+ ) {
+ 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
+ // be undefined
+ if (!endHost.visible) {
+ endHost = starthost
+ } else if (!starthost.visible) {
+ starthost = endHost
+ }
+ val newBounds = endHost.currentBounds
+ val previousBounds = starthost.currentBounds
+ targetBounds = interpolateBounds(previousBounds, newBounds, progress)
+ targetClipping = endHost.currentClipping
+ } else if (endHost != null) {
+ val bounds = endHost.currentBounds
+ targetBounds.set(bounds)
+ targetClipping = endHost.currentClipping
+ }
+ }
+
+ private fun interpolateBounds(
+ startBounds: Rect,
+ endBounds: Rect,
+ 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 resultBounds = result ?: Rect()
+ resultBounds.set(left, top, right, bottom)
+ return resultBounds
+ }
+
+ /** @return true if this transformation is guided by an external progress like a finger */
+ fun isCurrentlyInGuidedTransformation(): Boolean {
+ return hasValidStartAndEndLocations() &&
+ getTransformationProgress() >= 0 &&
+ areGuidedTransitionHostsVisible()
+ }
+
+ private fun hasValidStartAndEndLocations(): Boolean {
+ return previousLocation != -1 && desiredLocation != -1
+ }
+
+ /** Calculate the transformation type for the current animation */
+ @VisibleForTesting
+ @TransformationType
+ fun calculateTransformationType(): Int {
+ if (isTransitioningToFullShade) {
+ if (inSplitShade && areGuidedTransitionHostsVisible()) {
+ return TRANSFORMATION_TYPE_TRANSITION
+ }
+ return TRANSFORMATION_TYPE_FADE
+ }
+ 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
+ }
+ if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
+ // animating between ls and qqs should fade when dragging down via e.g. expand button
+ return TRANSFORMATION_TYPE_FADE
+ }
+ return TRANSFORMATION_TYPE_TRANSITION
+ }
+
+ private fun areGuidedTransitionHostsVisible(): Boolean {
+ return getHost(previousLocation)?.visible == true &&
+ getHost(desiredLocation)?.visible == true
+ }
+
+ /**
+ * @return the current transformation progress if we're in a guided transformation and -1
+ * otherwise
+ */
+ private fun getTransformationProgress(): Float {
+ if (skipQqsOnExpansion) {
+ return -1.0f
+ }
+ val progress = getQSTransformationProgress()
+ if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) {
+ return progress
+ }
+ if (isTransitioningToFullShade) {
+ return fullShadeTransitionProgress
+ }
+ return -1.0f
+ }
+
+ private fun getQSTransformationProgress(): Float {
+ val currentHost = getHost(desiredLocation)
+ val previousHost = getHost(previousLocation)
+ if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) {
+ if (previousHost?.location == LOCATION_QQS) {
+ if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) {
+ return qsExpansion
+ }
+ }
+ }
+ return -1.0f
+ }
+
+ private fun getHost(@MediaLocation location: Int): MediaHost? {
+ if (location < 0) {
+ return null
+ }
+ return mediaHosts[location]
+ }
+
+ private fun cancelAnimationAndApplyDesiredState() {
+ animator.cancel()
+ getHost(desiredLocation)?.let {
+ applyState(it.currentBounds, alpha = 1.0f, immediately = true)
+ }
+ }
+
+ /** 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(
+ currentBounds.left,
+ currentBounds.top,
+ currentBounds.right,
+ 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
+ }
+ }
+ 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)
+
+ // 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}"
+ )
+ }
+ }
+ }
+ 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.
+ */
+ private fun resolveLocationForFading(): Int {
+ if (isCrossFadeAnimatorRunning) {
+ // When animating between two hosts with a fade, let's keep ourselves in the old
+ // location for the first half, and then switch over to the end location
+ if (animationCrossFadeProgress > 0.5 || previousLocation == -1) {
+ return crossFadeAnimationEndLocation
+ } else {
+ return crossFadeAnimationStartLocation
+ }
+ }
+ return desiredLocation
+ }
+
+ private fun isTransitionRunning(): Boolean {
+ return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
+ animator.isRunning ||
+ animationPending
+ }
+
+ @MediaLocation
+ private fun calculateLocation(): Int {
+ if (blockLocationChanges) {
+ // 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
+ }
+ // 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
+ ) {
+ return LOCATION_QS
+ }
+ 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
+ ) {
+ // 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
+ return LOCATION_LOCKSCREEN
+ }
+ if (skipQqsOnExpansion) {
+ // When doing an immediate expand or collapse, we want to keep it in QS.
+ return LOCATION_QS
+ }
+ return location
+ }
+
+ private fun isSplitShadeExpanding(): Boolean {
+ return inSplitShade && isTransitioningToFullShade
+ }
+
+ /** Are we currently transforming to the full shade and already in QQS */
+ private fun isTransformingToFullShadeAndInQQS(): Boolean {
+ if (!isTransitioningToFullShade) {
+ return false
+ }
+ if (inSplitShade) {
+ // Split shade doesn't use QQS.
+ return false
+ }
+ return fullShadeTransitionProgress > 0.5f
+ }
+
+ /** Is the current transformationType fading */
+ private fun isCurrentlyFading(): Boolean {
+ if (isSplitShadeExpanding()) {
+ // Split shade always uses transition instead of fade.
+ return false
+ }
+ if (isTransitioningToFullShade) {
+ return true
+ }
+ return isCrossFadeAnimatorRunning
+ }
+
+ /** Returns true when the media card could be visible to the user if existed. */
+ private fun isVisibleToUser(): Boolean {
+ return isLockScreenVisibleToUser() ||
+ isLockScreenShadeVisibleToUser() ||
+ isHomeScreenShadeVisibleToUser()
+ }
+
+ private fun isLockScreenVisibleToUser(): Boolean {
+ return !statusBarStateController.isDozing &&
+ !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))
+ }
+
+ private fun isHomeScreenShadeVisibleToUser(): Boolean {
+ return !statusBarStateController.isDozing &&
+ statusBarStateController.state == StatusBarState.SHADE &&
+ statusBarStateController.isExpanded
+ }
+
+ companion object {
+ /** Attached in expanded quick settings */
+ const val LOCATION_QS = 0
+
+ /** Attached in the collapsed QS */
+ const val LOCATION_QQS = 1
+
+ /** Attached on the lock screen */
+ const val LOCATION_LOCKSCREEN = 2
+
+ /** Attached on the dream overlay */
+ const val LOCATION_DREAM_OVERLAY = 3
+
+ /** Attached at the root of the hierarchy in an overlay */
+ const val IN_OVERLAY = -1000
+
+ /**
+ * The default transformation type where the hosts transform into each other using a direct
+ * transition
+ */
+ const val TRANSFORMATION_TYPE_TRANSITION = 0
+
+ /**
+ * A transformation type where content fades from one place to another instead of
+ * transitioning
+ */
+ const val TRANSFORMATION_TYPE_FADE = 1
+ }
+}
+
+private val EMPTY_RECT = Rect()
+
+@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
+ ]
+)
+@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
similarity index 62%
rename from packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
index 8645922..455b7de 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 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 @@
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 @@
*/
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) {
+ 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 onSmartspaceMediaDataLoaded(
- key: String,
- data: SmartspaceMediaData,
- shouldPrioritize: Boolean
- ) {
- updateViewVisibility()
- }
-
- override fun onMediaDataRemoved(key: String) {
- 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 @@
}
/**
- * 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 @@
// 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 @@
* 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 @@
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 @@
}
/**
- * 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 @@
}
/**
- * 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/controls/ui/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHostStatesManager.kt
new file mode 100644
index 0000000..ae3ce33
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHostStatesManager.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.animation.MeasurementOutput
+import com.android.systemui.util.traceSection
+import javax.inject.Inject
+
+/**
+ * A class responsible for managing all media host states of the various host locations and
+ * coordinating the heights among different players. This class can be used to get the most up to
+ * date state for any location.
+ */
+@SysUISingleton
+class MediaHostStatesManager @Inject constructor() {
+
+ private val callbacks: MutableSet<Callback> = mutableSetOf()
+ private val controllers: MutableSet<MediaViewController> = mutableSetOf()
+
+ /**
+ * The overall sizes of the carousel. This is needed to make sure all players in the carousel
+ * have equal size.
+ */
+ val carouselSizes: MutableMap<Int, MeasurementOutput> = mutableMapOf()
+
+ /** 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.
+ */
+ 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)
+ }
+ }
+ }
+
+ /**
+ * 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
+ }
+ }
+ }
+ carouselSizes[location] = result
+ return result
+ }
+
+ /** 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 */
+ 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
+ */
+ fun addController(controller: MediaViewController) {
+ controllers.add(controller)
+ }
+
+ /** Notify the manager about the removal of a controller. */
+ fun removeController(controller: MediaViewController) {
+ controllers.remove(controller)
+ }
+
+ interface Callback {
+ /**
+ * Notify the callbacks that a media state for a host has changed, and that the
+ * corresponding view states should be updated and applied
+ */
+ fun onHostStateChanged(@MediaLocation location: Int, mediaHostState: MediaHostState)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt
new file mode 100644
index 0000000..0e07465
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt
@@ -0,0 +1,145 @@
+/*
+ * 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
+import android.util.AttributeSet
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.ViewGroup
+import android.widget.HorizontalScrollView
+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
+ */
+class MediaScrollView
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
+ HorizontalScrollView(context, attrs, defStyleAttr) {
+
+ lateinit var contentContainer: ViewGroup
+ private set
+ var touchListener: Gefingerpoken? = null
+
+ /**
+ * The target value of the translation X animation. Only valid if the physicsAnimator is running
+ */
+ var animationTargetX = 0.0f
+
+ /**
+ * 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
+ }
+
+ /**
+ * 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.
+ */
+ 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. */
+ var relativeScrollX: Int
+ get() = transformScrollX(scrollX)
+ set(value) {
+ scrollX = transformScrollX(value)
+ }
+
+ /** 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
+ val oldY: Int = mScrollY
+ mScrollX = x
+ mScrollY = y
+ invalidateParentCaches()
+ onScrollChanged(mScrollX, mScrollY, oldX, oldY)
+ if (!awakenScrollBars()) {
+ postInvalidateOnAnimation()
+ }
+ }
+ }
+
+ override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+ var intercept = false
+ 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) }
+ return super.onTouchEvent(ev) || touch
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ contentContainer = getChildAt(0) as ViewGroup
+ }
+
+ override fun overScrollBy(
+ deltaX: Int,
+ deltaY: Int,
+ scrollX: Int,
+ scrollY: Int,
+ scrollRangeX: Int,
+ scrollRangeY: Int,
+ maxOverScrollX: Int,
+ maxOverScrollY: Int,
+ isTouchEvent: Boolean
+ ): Boolean {
+ if (getContentTranslation() != 0.0f) {
+ // When we're dismissing we ignore all the scrolling
+ return false
+ }
+ return super.overScrollBy(
+ deltaX,
+ deltaY,
+ scrollX,
+ scrollY,
+ scrollRangeX,
+ scrollRangeY,
+ maxOverScrollX,
+ maxOverScrollY,
+ isTouchEvent
+ )
+ }
+
+ /** 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)
+ event.source = InputDevice.SOURCE_TOUCHSCREEN
+ super.onTouchEvent(event)
+ event.recycle()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
new file mode 100644
index 0000000..4bf3031
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -0,0 +1,607 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.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.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
+import com.android.systemui.util.animation.TransitionLayoutController
+import com.android.systemui.util.animation.TransitionViewState
+import com.android.systemui.util.traceSection
+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(
+ private val context: Context,
+ private val configurationController: ConfigurationController,
+ private val mediaHostStatesManager: MediaHostStatesManager,
+ private val logger: MediaViewLogger
+) {
+
+ /**
+ * Indicating that the media view controller is for a notification-based player, session-based
+ * player, or recommendation
+ */
+ enum class TYPE {
+ PLAYER,
+ RECOMMENDATION
+ }
+
+ companion object {
+ @JvmField val GUTS_ANIMATION_DURATION = 500L
+ val controlIds =
+ setOf(
+ R.id.media_progress_bar,
+ R.id.actionNext,
+ R.id.actionPrev,
+ 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 detailIds =
+ setOf(
+ R.id.header_title,
+ R.id.header_artist,
+ R.id.actionPlayPause,
+ )
+ }
+
+ /** 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
+ private val layoutController = TransitionLayoutController()
+ private var animationDelay: Long = 0
+ private var animationDuration: Long = 0
+ private var animateNextStateChange: Boolean = false
+ private val measurement = MeasurementOutput(0, 0)
+ private var type: TYPE = TYPE.PLAYER
+
+ /** 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
+
+ /** 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 */
+ private var currentTransitionProgress: Float = 1.0f
+
+ /** A temporary state used to store intermediate measurements. */
+ private val tmpState = TransitionViewState()
+
+ /** A temporary state used to store intermediate measurements. */
+ private val tmpState2 = TransitionViewState()
+
+ /** A temporary state used to store intermediate measurements. */
+ private val tmpState3 = TransitionViewState()
+
+ /** 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
+ */
+ 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
+ */
+ var currentHeight: Int = 0
+
+ /** 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 */
+ 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 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
+ )
+ }
+ }
+ }
+
+ /**
+ * The expanded constraint set used to render a expanded player. If it is modified, make sure to
+ * call [refreshState]
+ */
+ val collapsedLayout = ConstraintSet()
+
+ /**
+ * The expanded constraint set used to render a collapsed player. If it is modified, make sure
+ * to call [refreshState]
+ */
+ val expandedLayout = ConstraintSet()
+
+ /** Whether the guts are visible for the associated player. */
+ var isGutsVisible = false
+ private set
+
+ init {
+ mediaHostStatesManager.addController(this)
+ layoutController.sizeChangedListener = { width: Int, height: Int ->
+ currentWidth = width
+ currentHeight = height
+ sizeChangedListener.invoke()
+ }
+ configurationController.addCallback(configurationListener)
+ }
+
+ /**
+ * Notify this controller that the view has been removed and all listeners should be destroyed
+ */
+ fun onDestroy() {
+ mediaHostStatesManager.removeController(this)
+ configurationController.removeCallback(configurationListener)
+ }
+
+ /** Show guts with an animated transition. */
+ fun openGuts() {
+ if (isGutsVisible) return
+ isGutsVisible = true
+ animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
+ setCurrentState(
+ currentStartLocation,
+ currentEndLocation,
+ currentTransitionProgress,
+ applyImmediately = false
+ )
+ }
+
+ /**
+ * Close the guts for the associated player.
+ *
+ * @param immediate if `false`, it will animate the transition.
+ */
+ @JvmOverloads
+ fun closeGuts(immediate: Boolean = false) {
+ if (!isGutsVisible) return
+ isGutsVisible = false
+ if (!immediate) {
+ animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
+ }
+ setCurrentState(
+ currentStartLocation,
+ currentEndLocation,
+ currentTransitionProgress,
+ applyImmediately = immediate
+ )
+ }
+
+ private fun ensureAllMeasurements() {
+ val mediaStates = mediaHostStatesManager.mediaHostStates
+ for (entry in mediaStates) {
+ obtainViewState(entry.value)
+ }
+ }
+
+ /** Get the constraintSet for a given expansion */
+ private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
+ 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 gutsIds = GutsViewHolder.ids
+ controlsIds.forEach { id ->
+ viewState.widgetStates.get(id)?.let { state ->
+ // Make sure to use the unmodified state if guts are not visible.
+ state.alpha = if (isGutsVisible) 0f else state.alpha
+ state.gone = if (isGutsVisible) true else state.gone
+ }
+ }
+ gutsIds.forEach { id ->
+ viewState.widgetStates.get(id)?.let { state ->
+ // Make sure to use the unmodified state if guts are visible
+ state.alpha = if (isGutsVisible) state.alpha else 0f
+ state.gone = if (isGutsVisible) state.gone else true
+ }
+ }
+ }
+
+ /** Apply squishFraction to a copy of viewState such that the cached version is untouched. */
+ internal fun squishViewState(
+ viewState: TransitionViewState,
+ squishFraction: Float
+ ): TransitionViewState {
+ val squishedViewState = viewState.copy()
+ squishedViewState.height = (squishedViewState.height * squishFraction).toInt()
+ controlIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION)
+ }
+ }
+
+ detailIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION)
+ }
+ }
+
+ RecommendationViewHolder.mediaContainersIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION)
+ }
+ }
+
+ RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION)
+ }
+ }
+
+ return squishedViewState
+ }
+
+ /**
+ * 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? {
+ if (state == null || state.measurementInput == null) {
+ return null
+ }
+ // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey
+ var cacheKey = getKey(state, isGutsVisible, tmpKey)
+ val viewState = viewStates[cacheKey]
+ if (viewState != null) {
+ // we already have cached this measurement, let's continue
+ if (state.squishFraction <= 1f) {
+ return squishViewState(viewState, state.squishFraction)
+ }
+ return viewState
+ }
+ // Copy the key since this might call recursively into it and we're using tmpKey
+ cacheKey = cacheKey.copy()
+ val result: TransitionViewState?
+
+ if (transitionLayout == null) {
+ return null
+ }
+ // Let's create a new measurement
+ if (state.expansion == 0.0f || state.expansion == 1.0f) {
+ result =
+ transitionLayout!!.calculateViewState(
+ state.measurementInput!!,
+ constraintSetForExpansion(state.expansion),
+ TransitionViewState()
+ )
+
+ setGutsViewState(result)
+ // We don't want to cache interpolated or null states as this could quickly fill up
+ // our cache. We only cache the start and the end states since the interpolation
+ // is cheap
+ viewStates[cacheKey] = result
+ } else {
+ // This is an interpolated state
+ val startState = state.copy().also { it.expansion = 0.0f }
+
+ // Given that we have a measurement and a view, let's get (guaranteed) viewstates
+ // from the start and end state and interpolate them
+ 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)
+ }
+ if (state.squishFraction <= 1f) {
+ return squishViewState(result, state.squishFraction)
+ }
+ return result
+ }
+
+ private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey {
+ result.apply {
+ heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0
+ widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0
+ expansion = state.expansion
+ gutsVisible = guts
+ }
+ return result
+ }
+
+ /**
+ * 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(
+ startLocation = currentStartLocation,
+ endLocation = currentEndLocation,
+ transitionProgress = currentTransitionProgress,
+ 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.
+ */
+ 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
+ * locations.
+ */
+ fun setCurrentState(
+ @MediaLocation startLocation: Int,
+ @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)
+
+ 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 {
+ result =
+ layoutController.getInterpolatedState(
+ startViewState,
+ endViewState,
+ transitionProgress,
+ tmpState
+ )
+ }
+ logger.logMediaSize("setCurrentState", result.width, result.height)
+ layoutController.setState(
+ result,
+ applyImmediately,
+ shouldAnimate,
+ animationDuration,
+ animationDelay
+ )
+ }
+
+ private fun updateViewStateToCarouselSize(
+ viewState: TransitionViewState?,
+ location: Int,
+ outState: TransitionViewState
+ ): TransitionViewState? {
+ val result = viewState?.copy(outState) ?: return null
+ val overrideSize = mediaHostStatesManager.carouselSizes[location]
+ overrideSize?.let {
+ // To be safe we're using a maximum here. The override size should always be set
+ // properly though.
+ result.height = Math.max(it.measuredHeight, result.height)
+ result.width = Math.max(it.measuredWidth, result.width)
+ }
+ logger.logMediaSize("update to carousel", result.width, result.height)
+ return result
+ }
+
+ private fun updateMediaViewControllerType(type: TYPE) {
+ this.type = type
+
+ // These XML resources contain ConstraintSets that will apply to this player type's layout
+ when (type) {
+ TYPE.PLAYER -> {
+ collapsedLayout.load(context, R.xml.media_session_collapsed)
+ expandedLayout.load(context, R.xml.media_session_expanded)
+ }
+ TYPE.RECOMMENDATION -> {
+ collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
+ expandedLayout.load(context, R.xml.media_recommendation_expanded)
+ }
+ }
+ refreshState()
+ }
+
+ /**
+ * 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
+ * [MediaHost.visible]
+ * @return State require for executing a transition, and also the respective [MediaHost].
+ */
+ private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? {
+ val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null
+ return obtainViewState(mediaHostState)
+ }
+
+ /**
+ * Notify that the location is changing right now and a [setCurrentState] change is imminent.
+ * This updates the width the view will me measured with.
+ */
+ fun onLocationPreChange(@MediaLocation newLocation: Int) {
+ obtainViewStateForLocation(newLocation)?.let { layoutController.setMeasureState(it) }
+ }
+
+ /** 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
+ }
+ setCurrentState(
+ currentStartLocation,
+ currentEndLocation,
+ currentTransitionProgress,
+ applyImmediately = true
+ )
+ }
+}
+
+/** 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,
+ var expansion: Float = 0.0f,
+ var gutsVisible: Boolean = false
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt
new file mode 100644
index 0000000..fdac33a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt
@@ -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.systemui.media.controls.ui
+
+import com.android.systemui.dagger.SysUISingleton
+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 */
+@SysUISingleton
+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" }
+ )
+ }
+
+ fun logMediaLocation(reason: String, startLocation: Int, endLocation: Int) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = reason
+ int1 = startLocation
+ int2 = endLocation
+ },
+ { "location ($str1): $int1 -> $int2" }
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MetadataAnimationHandler.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/MetadataAnimationHandler.kt
index 48f4a16..1cdcf5e 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 @@
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
similarity index 73%
rename from packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt
index 6bc94cd..e9b2cf2 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.java
index ed3e109..6caf5c2 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
similarity index 80%
rename from packages/SystemUI/src/com/android/systemui/media/MediaDataUtils.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
index b8185b9..bcfceaa 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
similarity index 88%
rename from packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFeatureFlag.kt
index 75eb33d..91dac6f 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
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index b85ae48..8d4931a 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 @@
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
similarity index 66%
rename from packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
index 0baf01e..3ad8c21 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.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 @@
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 @@
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 @@
}
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 @@
}
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 @@
}
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
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/media/SmallHash.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/util/SmallHash.java
index de7aac6..97483a6 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 e15e2d3..3e5d337 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -19,11 +19,11 @@
import com.android.systemui.dagger.SysUISingleton;
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;
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 65c5bc7..69b5698 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 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 91e7b49..20e8ae6 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.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 ffcc1f7..e260894 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.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/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
index c3de94f..0a60437 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.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 @@
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/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index c24b030..6e596ee 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 @@
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 @@
* @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 @@
val uiEvent: UiEventLogger.UiEventEnum,
@StringRes val stringResId: Int?,
val transferStatus: TransferStatus,
+ val endItem: SenderEndItem?,
val timeout: Long = DEFAULT_TIMEOUT_MILLIS
) {
/**
@@ -58,6 +55,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
}
+/** 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 224303a..edf759d 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
@@ -20,14 +20,20 @@
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.ChipbarEndItem
+import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
import com.android.systemui.temporarydisplay.chipbar.SENDER_TAG
import javax.inject.Inject
@@ -107,7 +113,90 @@
chipbarCoordinator.removeView(removalReason)
} else {
displayedState = chipState
- chipbarCoordinator.displayView(ChipSenderInfo(chipState, routeInfo, undoCallback))
+ 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 f15720d..b963809 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/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 482a139..bb2b441 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.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 @@
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 @@
override fun shouldUpdateFooterVisibility() = dialog == null
- override fun showDialog(viewLaunchedFrom: View?) {
+ override fun showDialog(expandable: Expandable?) {
synchronized(lock) {
if (dialog == null) {
@@ -403,16 +404,18 @@
}
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 9d64781..a9943e8 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.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 @@
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 7511278e..b1b9dd7 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 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 @@
@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 0fe3d16..20f1a8e 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.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/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index f6db775..abc0ade 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.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 2727c83..2a80de0 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.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 67bf300..6c1e956 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 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 @@
// 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 ae6ed20..67bc769 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.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 @@
}
/** 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 @@
}
}
- 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 @@
? 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 ac46c85..f37d668 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.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.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.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 @@
* 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 @@
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 9739974..6aabe3b 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.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 0000000..2c8a5a4
--- /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/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
index cf9b41c..9ba3501 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.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 @@
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 @@
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,
+ expandable: Expandable?,
+ ) {
+ qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, expandable)
+ if (expandable != null) {
+ DevicePolicyEventLogger.createEvent(
+ FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED
+ )
+ .write()
+ }
}
- override fun showDeviceMonitoringDialog(quickSettingsContext: Context) {
- qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, /* view= */ null)
+ override fun showForegroundServicesDialog(expandable: Expandable) {
+ fgsManagerController.showDialog(expandable)
}
- override fun showForegroundServicesDialog(view: View) {
- fgsManagerController.showDialog(view)
- }
-
- 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 @@
)
}
- 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 dd1ffcc..3e39c8e 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.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 @@
launch {
viewModel.security.collect { security ->
if (previousSecurity != security) {
- bindSecurity(securityHolder, security)
+ bindSecurity(view.context, securityHolder, security)
previousSecurity = security
}
}
@@ -159,6 +160,7 @@
}
private fun bindSecurity(
+ quickSettingsContext: Context,
securityHolder: TextButtonViewHolder,
security: FooterActionsSecurityButtonViewModel?,
) {
@@ -171,9 +173,12 @@
// 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 @@
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 @@
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 @@
}
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 9b5f683..8d819da 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 @@
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 98b53cb..ff8130d 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 @@
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 98ab129..3450505 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 d3c06f6..dee6fad 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 @@
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 @@
*/
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/proto/tiles.proto b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto
new file mode 100644
index 0000000..2a61033
--- /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/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
index bdcc6b0..314252b 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.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 @@
* 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 @@
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/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index 26cbcbf..1b9cdd4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -767,7 +767,9 @@
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 +780,7 @@
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 +792,7 @@
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,
@@ -1064,6 +1068,12 @@
}
}
+ private void prepareSharedTransition() {
+ mPendingSharedTransition = true;
+ // fade out non-preview UI
+ createScreenshotFadeDismissAnimation().start();
+ }
+
ValueAnimator createScreenshotFadeDismissAnimation() {
ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
alphaAnim.addUpdateListener(animation -> {
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 6e9f859..d5a3954 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.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.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 @@
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/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index c1afd59..ddb57f7 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.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.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 @@
private final SysUiState mSysUiState;
private final NotificationShadeDepthController mDepthController;
+ private final NavigationBarController mNavigationBarController;
private final int mDisplayId;
private KeyguardIndicationController mKeyguardIndicationController;
@@ -858,6 +864,7 @@
PrivacyDotViewController privacyDotViewController,
TapAgainViewController tapAgainViewController,
NavigationModeController navigationModeController,
+ NavigationBarController navigationBarController,
FragmentService fragmentService,
ContentResolver contentResolver,
RecordingController recordingController,
@@ -951,6 +958,7 @@
mNotificationsQSContainerController = notificationsQSContainerController;
mNotificationListContainer = notificationListContainer;
mNotificationStackSizeCalculator = notificationStackSizeCalculator;
+ mNavigationBarController = navigationBarController;
mKeyguardBottomAreaViewControllerProvider = keyguardBottomAreaViewControllerProvider;
mNotificationsQSContainerController.init();
mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
@@ -1440,6 +1448,16 @@
mMaxAllowedKeyguardNotifications = maxAllowed;
}
+ @VisibleForTesting
+ boolean getClosing() {
+ return mClosing;
+ }
+
+ @VisibleForTesting
+ boolean getIsFlinging() {
+ return mIsFlinging;
+ }
+
private void updateMaxDisplayedNotifications(boolean recompute) {
if (recompute) {
setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1));
@@ -1667,9 +1685,40 @@
// 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);
@@ -2637,12 +2686,16 @@
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);
}
}
@@ -3684,6 +3737,11 @@
setListening(true);
}
+ @VisibleForTesting
+ void setTouchSlopExceeded(boolean isTouchSlopExceeded) {
+ mTouchSlopExceeded = isTouchSlopExceeded;
+ }
+
public void setOverExpansion(float overExpansion) {
if (overExpansion == mOverExpansion) {
return;
@@ -3875,20 +3933,19 @@
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.
- mUpdateMonitor.requestFaceAuth(true,
+ boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true,
FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
- mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT,
- 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */);
- mLockscreenGestureLogger
- .log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT);
- if (!mUpdateMonitor.isFaceDetectionRunning()) {
- startUnlockHintAnimation();
- }
- if (mUpdateMonitor.isFaceEnrolled()) {
+ 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 */);
+ mLockscreenGestureLogger
+ .log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT);
+ startUnlockHintAnimation();
}
}
return true;
@@ -4166,8 +4223,8 @@
/**
* 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;
@@ -4307,35 +4364,35 @@
/**
* 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();
}
/**
@@ -4695,8 +4752,10 @@
/**
* 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) {
@@ -4742,6 +4801,7 @@
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();
@@ -4861,10 +4921,12 @@
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;
@@ -4912,7 +4974,7 @@
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));
@@ -5103,7 +5165,7 @@
/**
* 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) {
@@ -5139,7 +5201,8 @@
*/
public void updatePanelExpansionAndVisibility() {
mShadeExpansionStateManager.onPanelExpansionChanged(
- mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
+ mExpandedFraction, isExpanded(),
+ mTracking, mExpansionDragDownAmountPx);
updateVisibility();
}
@@ -5900,7 +5963,7 @@
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,
@@ -6099,7 +6162,7 @@
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;
}
@@ -6256,4 +6319,54 @@
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 1d92105..66a22f4 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 @@
DumpManager dumpManager,
KeyguardStateController keyguardStateController,
ScreenOffAnimationController screenOffAnimationController,
- AuthController authController) {
+ AuthController authController,
+ ShadeExpansionStateManager shadeExpansionStateManager) {
mContext = context;
mWindowManager = windowManager;
mActivityManager = activityManager;
@@ -156,6 +157,7 @@
.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 @@
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 d6f0de8..73c6d50 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 @@
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 @@
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 @@
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 @@
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 f617d47..7bba74a 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 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 @@
@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 @@
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 @@
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/ShadeQsExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt
new file mode 100644
index 0000000..14882b9
--- /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/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 04621168..e6d7e41 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.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 @@
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 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 @@
}
@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 886ad68..5fb5002 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.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 8006931..a2e4536 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.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 4be5a1a..ced725e 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.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 0c9e1ec..e21acb7 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 @@
/** 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 f961984..87ef92a 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.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 @@
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 @@
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 @@
// 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 8222c9d..c630feb 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.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 @@
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 @@
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/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 11e3d17..eacb18e 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.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 @@
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 d88f07c..737b481 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.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_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 @@
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 @@
updateStatusBarState()
}
})
+
+ shadeExpansionStateManager.addQsExpansionListener { isQsExpanded ->
+ dlog("setQsExpanded $isQsExpanded")
+ synchronized(lock) {
+ nextViewState = nextViewState.copy(qsExpanded = isQsExpanded)
+ }
+ }
}
fun setUiExecutor(e: DelayableExecutor) {
@@ -138,13 +146,6 @@
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/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 36b8333..2734511 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -28,12 +28,7 @@
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)
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 553826d..0d35fdc 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 @@
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 @@
// 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 @@
left = location[0],
right = location[0] + notification.width,
topCornerRadius = topCornerRadius,
- bottomCornerRadius = notification.currentBackgroundRadiusBottom
+ bottomCornerRadius = notification.bottomCornerRadius
)
params.startTranslationZ = notification.translationZ
@@ -97,8 +97,8 @@
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 7242506..d97b712 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 @@
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.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 @@
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 @@
}
init {
+ dumpManager.registerDumpable(this)
mHeadsUpManager.addListener(this)
statusBarStateController.addCallback(this)
addListener(object : WakeUpListener {
@@ -248,13 +255,14 @@
// 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 @@
// 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 @@
*/
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 @@
*/
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 @@
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 0000000..ed7f648
--- /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/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 2887f97..df35c9e 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 @@
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/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 8f3eb4f..8a31ed9 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 @@
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 @@
@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 @@
}
// 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 @@
isAlerting = false,
isBinding = false,
)
+
+ // Record the last updated time for this key
+ setUpdateTime(entry, mSystemClock.currentTimeMillis())
}
/**
@@ -419,6 +428,9 @@
cancelHeadsUpBind(posted.entry)
}
}
+
+ // Update last updated time for this entry
+ setUpdateTime(entry, mSystemClock.currentTimeMillis())
}
/**
@@ -426,6 +438,7 @@
*/
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 @@
// 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 @@
(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 @@
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/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
index 2480ff6..0be4bde 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 6e76691..d2db622 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
@@ -407,7 +407,10 @@
mLogger.logGroupInflationTookTooLong(group);
return false;
}
- if (mInflatingNotifs.contains(group.getSummary())) {
+ // 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 (group.getSummary() != null && !isInflated(group.getSummary())) {
mLogger.logDelayingGroupRelease(group, group.getSummary());
return true;
}
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 b2cb23b..a5278c3d 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.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 @@
*/
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 8e646a3..8eef3f3 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.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.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 @@
*/
@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 @@
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 @@
) : NotificationsController {
override fun initialize(
+ centralSurfaces: CentralSurfaces,
presenter: NotificationPresenter,
listContainer: NotificationListContainer,
stackController: NotifStackController,
@@ -92,8 +96,8 @@
notificationRowBinder.setNotificationClicker(
clickerBuilder.build(
- Optional.of(
- centralSurfaces.get()), bubblesOptional, notificationActivityStarter))
+ Optional.ofNullable(centralSurfaces), bubblesOptional,
+ notificationActivityStarter))
notificationRowBinder.setUpWithPresenter(
presenter,
listContainer,
@@ -109,7 +113,8 @@
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 744166d..14856da 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.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 @@
) : NotificationsController {
override fun initialize(
+ centralSurfaces: CentralSurfaces,
presenter: NotificationPresenter,
listContainer: NotificationListContainer,
stackController: NotifStackController,
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 c5a6921..c4f5a3a 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.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 @@
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 @@
NotificationInterruptLogger logger,
@Main Handler mainHandler,
NotifPipelineFlags flags,
- KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) {
+ KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
+ UiEventLogger uiEventLogger) {
mContentResolver = contentResolver;
mPowerManager = powerManager;
mDreamManager = dreamManager;
@@ -97,6 +122,7 @@
mLogger = logger;
mFlags = flags;
mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider;
+ mUiEventLogger = uiEventLogger;
ContentObserver headsUpObserver = new ContentObserver(mainHandler) {
@Override
public void onChange(boolean selfChange) {
@@ -203,7 +229,9 @@
// 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 @@
// 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 832a739..0380fff 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 @@
/** 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 @@
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 0000000..7d39e18
--- /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 958978e..c09cc43 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 @@
) : 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 @@
}
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)
- )
- 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
+ /** 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
+ )
+ }
+
+ // 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,
+ )
+
+ 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
+ }
+
+ 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 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()
- }
+ /** 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,
+ )
+
+ 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()
}
- /**
- * 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
+ 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 0000000..a0bee15
--- /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/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 755e3e1..d29298a 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 @@
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 087dc71..9e7717c 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.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 @@
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 @@
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 @@
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 @@
}
}
- /** 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 @@
/**
* 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 @@
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 @@
/**
* 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 @@
}
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 @@
/**
* @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 @@
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 @@
}
}
- /** 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 @@
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 @@
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 void setContentBackground(int customBackgroundColor, boolean animate,
- NotificationContentView notificationContentView) {
+ NotificationContentView notificationContentView) {
if (getShowingLayout() == notificationContentView) {
setTintColor(customBackgroundColor, animate);
}
@@ -1637,7 +1676,9 @@
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 @@
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 @@
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 @@
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 @@
mChildrenContainer.setIsLowPriority(mIsLowPriority);
mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this);
mChildrenContainer.onNotificationUpdated();
+ mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
mTranslateableViews.add(mChildrenContainer);
});
@@ -1796,6 +1842,7 @@
/**
* 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 @@
/**
* 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 @@
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 Animator getTranslateViewAnimator(final float leftTarget,
- AnimatorUpdateListener listener) {
+ AnimatorUpdateListener listener) {
if (mTranslateAnim != null) {
mTranslateAnim.cancel();
}
@@ -2115,7 +2171,7 @@
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 @@
}
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 @@
/**
* 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 @@
/**
* @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 @@
@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 @@
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 @@
/**
* @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 @@
}
@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 @@
}
}
- /** 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 @@
* 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 @@
}
}
- /** 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 @@
* 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 @@
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 @@
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 @@
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 @@
}
@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 @@
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 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 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 @@
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 a493a67..842526e 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 @@
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 d58fe3b..4fde5d0 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.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 @@
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 @@
}
};
+ @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 @@
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 @@
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 @@
@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 @@
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 @@
}
}
- 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 @@
/**
* 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 38f0c55..955d7c1 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.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 @@
/**
* 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 @@
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 @@
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 void setExtraWidthForClipping(float extraWidthForClipping) {
mExtraWidthForClipping = extraWidthForClipping;
- updateClipping();
}
public float getHeaderVisibleAmount() {
@@ -844,22 +853,6 @@
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/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 4c69304..c534860 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.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 8de0365..277ad8e 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 @@
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 @@
}
}
+ @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/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index 7a65436..f13e48d 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 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 @@
/**
* 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 @@
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 @@
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 @@
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 @@
}
}
- /** 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 @@
// 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 @@
}
@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/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 7b23a56..26f0ad9 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.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.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.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 @@
* 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
/**
* 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 @@
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 @@
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 {
@@ -461,7 +488,9 @@
return mAttachedChildren;
}
- /** To be called any time the rows have been updated */
+ /**
+ * 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
@@ -475,7 +504,6 @@
}
/**
- *
* @return the intrinsic size of this children container, i.e the natural fully expanded state
*/
public int getIntrinsicHeight() {
@@ -485,7 +513,7 @@
/**
* @return the intrinsic height with a number of children given
- * in @param maxAllowedVisibleChildren
+ * in @param maxAllowedVisibleChildren
*/
private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
if (showingAsLowPriority()) {
@@ -539,7 +567,8 @@
/**
* 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) {
@@ -655,14 +684,17 @@
* 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;
@@ -690,13 +722,15 @@
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();
@@ -768,17 +802,73 @@
}
}
+ @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();
@@ -1102,7 +1192,8 @@
* 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);
@@ -1112,10 +1203,13 @@
* 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) {
@@ -1274,16 +1368,19 @@
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;
}
}
@@ -1293,7 +1390,9 @@
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);
@@ -1325,4 +1424,26 @@
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 2015c87..6810055 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.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 @@
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 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 @@
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 @@
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 @@
}
void setViewsAffectedBySwipe(
- ExpandableView viewBefore,
+ Roundable viewBefore,
ExpandableView viewSwiped,
- ExpandableView viewAfter) {
+ Roundable viewAfter) {
final boolean animate = true;
+ final SourceType source = SourceType.OnDismissAnimation;
- ExpandableView oldViewBefore = mViewBeforeSwipedView;
+ // 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);
+
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 @@
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 @@
&& 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 == 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);
}
- if (view.showingPulsing() && mRoundForPulsingViews) {
- return 1.0f;
- }
- 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 @@
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 @@
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/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index 91a2813..a1b77ac 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.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 2272411..df705c5 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
@@ -1188,7 +1188,7 @@
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;
@@ -1658,7 +1658,7 @@
// 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;
@@ -1691,6 +1691,10 @@
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]);
@@ -2276,7 +2280,7 @@
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++;
}
@@ -2496,7 +2500,7 @@
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;
@@ -2509,7 +2513,7 @@
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;
@@ -2523,7 +2527,7 @@
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) {
@@ -2882,7 +2886,7 @@
}
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) {
@@ -2936,7 +2940,7 @@
}
mAmbientState.setLastVisibleBackgroundChild(lastChild);
// TODO: Refactor SectionManager and put the RoundnessManager there.
- mController.getNoticationRoundessManager().updateRoundedChildren(mSections);
+ mController.getNotificationRoundnessManager().updateRoundedChildren(mSections);
mAnimateBottomOnLayout = false;
invalidate();
}
@@ -3968,7 +3972,7 @@
@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);
@@ -3981,7 +3985,7 @@
// 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());
@@ -4230,7 +4234,7 @@
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 @@
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 @@
// 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 void setClearAllInProgress(boolean clearAllInProgress) {
mClearAllInProgress = clearAllInProgress;
mAmbientState.setClearAllInProgress(clearAllInProgress);
- mController.getNoticationRoundessManager().setClearAllInProgress(clearAllInProgress);
+ mController.getNotificationRoundnessManager().setClearAllInProgress(clearAllInProgress);
}
boolean getClearAllInProgress() {
@@ -4555,7 +4559,7 @@
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 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) {
@@ -5052,7 +5056,7 @@
pw.println();
for (int i = 0; i < childCount; i++) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
child.dump(pw, args);
pw.println();
}
@@ -5341,7 +5345,7 @@
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;
}
@@ -5380,7 +5384,7 @@
public void setController(
NotificationStackScrollLayoutController notificationStackScrollLayoutController) {
mController = notificationStackScrollLayoutController;
- mController.getNoticationRoundessManager().setAnimatedChildren(mChildrenToAddAnimated);
+ mController.getNotificationRoundnessManager().setAnimatedChildren(mChildrenToAddAnimated);
}
void addSwipedOutView(View v) {
@@ -5391,31 +5395,22 @@
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);
@@ -5426,7 +5421,7 @@
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 5c09d61..e1337826 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.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 @@
private int mBarState;
private HeadsUpAppearanceController mHeadsUpAppearanceController;
private final FeatureFlags mFeatureFlags;
+ private final NotificationTargetsHelper mNotificationTargetsHelper;
private View mLongPressedView;
@@ -642,7 +643,8 @@
StackStateLogger stackLogger,
NotificationStackScrollLogger logger,
NotificationStackSizeCalculator notificationStackSizeCalculator,
- FeatureFlags featureFlags) {
+ FeatureFlags featureFlags,
+ NotificationTargetsHelper notificationTargetsHelper) {
mStackStateLogger = stackLogger;
mLogger = logger;
mAllowLongPress = allowLongPress;
@@ -679,6 +681,7 @@
mRemoteInputManager = remoteInputManager;
mShadeController = shadeController;
mFeatureFlags = featureFlags;
+ mNotificationTargetsHelper = notificationTargetsHelper;
updateResources();
}
@@ -1380,7 +1383,7 @@
return mView.calculateGapHeight(previousView, child, count);
}
- NotificationRoundnessManager getNoticationRoundessManager() {
+ NotificationRoundnessManager getNotificationRoundnessManager() {
return mNotificationRoundnessManager;
}
@@ -1537,6 +1540,10 @@
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/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
new file mode 100644
index 0000000..991a14b
--- /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 0502159..eea1d911 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.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 @@
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/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index fa7bfae..169c907 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 @@
@Override
void startActivity(Intent intent, boolean dismissShade, Callback callback);
- void setQsExpanded(boolean expanded);
-
boolean isWakeUpComingFromTouch();
boolean isFalsingThresholdNeeded();
@@ -455,6 +453,9 @@
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 604e146..db8aac95 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 @@
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 @@
// 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 @@
mStackScrollerController.setNotificationActivityStarter(mNotificationActivityStarter);
mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);
mNotificationsController.initialize(
+ this,
mPresenter,
mNotifListContainer,
mStackScrollerController.getNotifStackController(),
@@ -1790,18 +1795,6 @@
}
@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;
}
@@ -3580,6 +3573,13 @@
}
}
+ @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 b987f68..b965ac9 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.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 @@
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 @@
statusBarStateController: StatusBarStateController,
lockscreenUserManager: NotificationLockscreenUserManager,
keyguardStateController: KeyguardStateController,
+ shadeExpansionStateManager: ShadeExpansionStateManager,
dumpManager: DumpManager
) {
this.mKeyguardStateController = keyguardStateController
@@ -132,6 +127,14 @@
}
})
+ 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 @@
): 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 @@
altBouncerShowing -> true
statusBarStateController.state != StatusBarState.KEYGUARD -> false
launchingAffordance -> false
- isPulseExpanding || qSExpanded -> false
+ isPulseExpanding || qsExpanded -> false
else -> true
}
}
@@ -214,7 +217,7 @@
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/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
index 00c3e8f..5e2a7c8 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 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 @@
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/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index ece7ee0..86f6ff8 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 @@
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 @@
@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 @@
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 @@
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 e106b9e..31e960a 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 @@
*/
@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 @@
@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 3a1c03d..ccb5d88 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.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 @@
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() {
@@ -327,7 +326,7 @@
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 @@
private void setDozing(boolean dozing) {
if (mDozing != dozing) {
mDozing = dozing;
- if (dozing || mBouncer.needsFullscreenBouncer()
+ if (dozing || needsFullscreenBouncer()
|| mKeyguardStateController.isOccluded()) {
reset(dozing /* hideBouncerWhenShowing */);
}
@@ -1082,7 +1081,7 @@
* @return whether a back press can be handled right now.
*/
public boolean canHandleBackPressed() {
- return mBouncer.isShowing();
+ return bouncerIsShowing();
}
/**
@@ -1095,7 +1094,7 @@
mCentralSurfaces.endAffordanceLaunch();
// The second condition is for SIM card locked bouncer
- if (bouncerIsScrimmed() && needsFullscreenBouncer()) {
+ if (bouncerIsScrimmed() && !needsFullscreenBouncer()) {
hideBouncer(false);
updateStates();
} else {
@@ -1125,8 +1124,8 @@
}
public boolean isFullscreenBouncer() {
- if (mBouncerViewDelegate != null) {
- return mBouncerViewDelegate.isFullScreenBouncer();
+ if (mBouncerView.getDelegate() != null) {
+ return mBouncerView.getDelegate().isFullScreenBouncer();
}
return mBouncer != null && mBouncer.isFullscreenBouncer();
}
@@ -1285,15 +1284,15 @@
}
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);
}
@@ -1302,8 +1301,8 @@
* @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/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 64ca2705..a1e0c50 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 @@
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/userswitcher/StatusBarUserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
index 0d52f46..e498ae4 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 @@
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 @@
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 9b8b643..06cd12d 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 @@
/** 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)
+
+ /** 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 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)
-
- /**
- * 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 0000000..7aa5ee1
--- /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 0000000..3e9b2c2
--- /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 0000000..fe30c01
--- /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 06d5542..2aaa085 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,6 +16,8 @@
package com.android.systemui.statusbar.pipeline.dagger
+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.MobileSubscriptionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
@@ -30,6 +32,9 @@
@Module
abstract class StatusBarPipelineModule {
@Binds
+ abstract fun airplaneModeRepository(impl: AirplaneModeRepositoryImpl): AirplaneModeRepository
+
+ @Binds
abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
@Binds
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 681cf72..93448c1d 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.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 @@
/** 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 @@
merge(wifiNetworkChangeEvents, wifiStateChangeEvents)
.mapLatest { wifiManager.isWifiEnabled }
.distinctUntilChanged()
- .logOutputChange(logger, "enabled")
+ .logInputChange(logger, "enabled")
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(),
@@ -111,6 +113,39 @@
)
}
+ 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 04b17ed..3a3e611 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 @@
/** 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 273be63..25537b9 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 @@
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 @@
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 40f948f..95ab251 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 @@
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,
+ isAirplaneSpacerVisible: Flow<Boolean>,
) :
LocationBasedWifiViewModel(
statusBarPipelineFlags,
@@ -40,4 +41,5 @@
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 9642ac4..86535d6 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 @@
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,
+ isAirplaneSpacerVisible: Flow<Boolean>,
) :
LocationBasedWifiViewModel(
statusBarPipelineFlags,
@@ -37,4 +38,5 @@
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 e23f8c7..7cbdf5d 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 @@
/** 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 0ddf90e..fd54c5f 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 @@
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,
+ isAirplaneSpacerVisible: Flow<Boolean>,
) :
LocationBasedWifiViewModel(
statusBarPipelineFlags,
@@ -37,4 +38,5 @@
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 ebbd77b..89b96b7 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_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 @@
class WifiViewModel
@Inject
constructor(
+ airplaneModeViewModel: AirplaneModeViewModel,
connectivityConstants: ConnectivityConstants,
private val context: Context,
logger: ConnectivityPipelineLogger,
@@ -124,9 +126,10 @@
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 @@
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 @@
}
.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 @@
isActivityInViewVisible,
isActivityOutViewVisible,
isActivityContainerVisible,
+ isAirplaneSpacerVisible,
)
/** A view model for the status bar on keyguard. */
@@ -193,6 +204,7 @@
isActivityInViewVisible,
isActivityOutViewVisible,
isActivityContainerVisible,
+ isAirplaneSpacerVisible,
)
/** A view model for the status bar in quick settings. */
@@ -203,6 +215,7 @@
isActivityInViewVisible,
isActivityOutViewVisible,
isActivityContainerVisible,
+ isAirplaneSpacerVisible,
)
companion object {
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 dc73d1f..f63d652 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.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 @@
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 da6d455..dd400b3 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.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.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 @@
*/
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 @@
// 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 @@
}
}
+ @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 @@
}
@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 @@
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 d5d904c..f0a50de 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 @@
* 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
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 1a25e4d..cd7bd2d 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 @@
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,25 +26,25 @@
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.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
@@ -78,11 +77,11 @@
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,
@@ -104,15 +103,13 @@
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)
@@ -120,47 +117,57 @@
}
}
- // 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()
+ (newInfo.endItem == ChipbarEndItem.Loading).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()
+ // 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,
@@ -175,7 +182,7 @@
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,
@@ -197,13 +204,5 @@
}
}
-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 0000000..57fde87
--- /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 3d56f23..3ecb15b 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.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 @@
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 @@
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 @@
// 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/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
index 7f1195b..7da2d47 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 @@
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 ee785b6..088cd93 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 @@
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 d768b6d..b16dc54 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 @@
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 }
}
}
}
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 0d5c64b..dda78aa 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
@@ -429,6 +429,7 @@
isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
onExitGuestUser = this::exitGuestUser,
+ dialogShower = dialogShower,
)
)
return
@@ -443,6 +444,7 @@
isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
onExitGuestUser = this::exitGuestUser,
+ dialogShower = dialogShower,
)
)
return
@@ -477,6 +479,7 @@
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 08d7c5a..177356e 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 @@
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 938417f..968af59 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.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 @@
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 @@
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 f7e19c0..e921720 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 @@
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
@@ -71,37 +73,58 @@
}
}
- currentDialog =
+ val (dialog, dialogCuj) =
when (request) {
is ShowDialogRequestModel.ShowAddUserDialog ->
- AddUserDialog(
- context = context.get(),
- userHandle = request.userHandle,
- isKeyguardShowing = request.isKeyguardShowing,
- showEphemeralMessage = request.showEphemeralMessage,
- falsingManager = falsingManager.get(),
- broadcastSender = broadcastSender.get(),
- dialogLaunchAnimator = dialogLaunchAnimator.get(),
+ 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.get(),
- request.isGuest,
+ Pair(
+ UserCreatingDialog(
+ context.get(),
+ request.isGuest,
+ ),
+ null,
)
is ShowDialogRequestModel.ShowExitGuestDialog ->
- 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,
+ 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()
+ if (request.dialogShower != null && dialogCuj != null) {
+ request.dialogShower?.showDialog(dialog, dialogCuj)
+ } else {
+ dialog.show()
+ }
+
interactor.get().onDialogShown()
}
}
@@ -120,4 +143,9 @@
}
}
}
+
+ 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/util/condition/Condition.java b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
index ecb365f..2c317dd 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 @@
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 4824f67..cb430ba 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 @@
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 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 0000000..9653985
--- /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 0000000..b7166d9
--- /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 fbc6a58..309f168 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_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.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 @@
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 @@
Optional<SplitScreen> splitScreenOptional,
Optional<OneHanded> oneHandedOptional,
Optional<FloatingTasks> floatingTasksOptional,
+ Optional<DesktopMode> desktopMode,
CommandQueue commandQueue,
ConfigurationController configurationController,
KeyguardStateController keyguardStateController,
@@ -194,6 +199,7 @@
mPipOptional = pipOptional;
mSplitScreenOptional = splitScreenOptional;
mOneHandedOptional = oneHandedOptional;
+ mDesktopModeOptional = desktopMode;
mWakefulnessLifecycle = wakefulnessLifecycle;
mProtoTracer = protoTracer;
mUserTracker = userTracker;
@@ -219,6 +225,7 @@
mPipOptional.ifPresent(this::initPip);
mSplitScreenOptional.ifPresent(this::initSplitScreen);
mOneHandedOptional.ifPresent(this::initOneHanded);
+ mDesktopModeOptional.ifPresent(this::initDesktopMode);
}
@VisibleForTesting
@@ -326,6 +333,16 @@
});
}
+ 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 ba28045..1b404a8 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 9e460a5..112d73d2 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
index 9d6aff2..7b9b39f 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt
@@ -66,4 +66,13 @@
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 03efd06..1c3656d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -32,6 +32,7 @@
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.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.mockito.any
@@ -82,7 +83,7 @@
@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
@@ -109,6 +110,7 @@
context,
mainExecutor,
bgExecutor,
+ logBuffer,
featureFlags
)
underTest.clock = clock
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index bb03a47..627d738 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 android.content.res.Resources;
import android.database.ContentObserver;
+import android.graphics.Rect;
import android.net.Uri;
import android.os.UserHandle;
import android.provider.Settings;
@@ -45,6 +46,7 @@
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;
@@ -262,6 +264,19 @@
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));
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 48e8239..b885d54 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 @@
@Captor
private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback;
+ @Captor
+ private ArgumentCaptor<KeyguardSecurityContainer.SwipeListener> mSwipeListenerArgumentCaptor;
private Configuration mConfiguration;
@@ -475,6 +477,64 @@
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 4dcaa7c..c94c97c 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 @@
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 784e7dd..ebfb4d4 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 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 @@
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 0000000..ae8f419
--- /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 0000000..da40595
--- /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 0000000..d2c54b4
--- /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/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index d52612b..e8c760c 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.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 @@
}
@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 baeabc5..c85334d 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.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 @@
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 @@
}
@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 @@
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 @@
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 @@
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 8923ba8..28e13b8 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 @@
@Mock
private LatencyTracker mLatencyTracker;
private FakeExecutor mFgExecutor;
+ @Mock
+ private UdfpsDisplayMode mUdfpsDisplayMode;
// Stuff for configuring mocks
@Mock
@@ -258,7 +260,6 @@
mVibrator,
mUdfpsHapticsSimulator,
mUdfpsShell,
- Optional.of(mDisplayModeProvider),
mKeyguardStateController,
mDisplayManager,
mHandler,
@@ -275,6 +276,7 @@
verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
mScreenObserver = mScreenObserverCaptor.getValue();
mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, new UdfpsOverlayParams());
+ mUdfpsController.setUdfpsDisplayMode(mUdfpsDisplayMode);
}
@Test
@@ -659,7 +661,7 @@
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 0000000..7e35b26
--- /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 3e9cf1e..fa9c41a 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.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 @@
@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 @@
mFalsingCollector = new FalsingCollectorImpl(mFalsingDataProvider, mFalsingManager,
mKeyguardUpdateMonitor, mHistoryTracker, mProximitySensor,
- mStatusBarStateController, mKeyguardStateController, mBatteryController,
+ mStatusBarStateController, mKeyguardStateController, mShadeExpansionStateManager,
+ mBatteryController,
mDockManager, mFakeExecutor, mFakeSystemClock);
}
@@ -137,9 +141,9 @@
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/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index d96ca91..b7f1c1a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -39,6 +39,7 @@
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;
@@ -95,6 +96,11 @@
mCallbacks = mOverlayCallbacksCaptor.getValue();
}
+ @After
+ public void tearDown() {
+ mOverlayController.hideImmediate();
+ }
+
@Test
public void test_setClipData_nullData() {
ClipData clipData = null;
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 9ffc5a5..c40c187 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 @@
@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 @@
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/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 781dc15..6091d3a 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 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 @@
@Mock
private ProximityCheck mProximityCheck;
@Mock
+ private DozeLog mDozeLog;
+ @Mock
private AuthController mAuthController;
@Mock
private UiEventLogger mUiEventLogger;
@@ -127,7 +129,7 @@
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 @@
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 522b5b5..50f27ea 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.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 65b44a1..65ae90b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
@@ -19,11 +19,17 @@
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.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
@@ -31,9 +37,6 @@
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-import java.io.PrintWriter
-import java.io.StringWriter
-import javax.inject.Provider
@SmallTest
class DumpHandlerTest : SysuiTestCase() {
@@ -47,6 +50,8 @@
@Mock
private lateinit var pw: PrintWriter
+ @Mock
+ private lateinit var fd: FileDescriptor
@Mock
private lateinit var dumpable1: Dumpable
@@ -56,6 +61,11 @@
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
@@ -88,7 +98,7 @@
// 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)
@@ -107,7 +117,7 @@
// 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)
@@ -124,7 +134,7 @@
// 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)
@@ -145,7 +155,7 @@
// 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(
@@ -168,11 +178,35 @@
val spw = PrintWriter(stringWriter)
// When a config dump is requested
- dumpHandler.dump(spw, arrayOf("config"))
+ 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/flags/FlagCommandTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
index 4c61138..9628ee9 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 @@
}
@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/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
deleted file mode 100644
index cefd68d..0000000
--- 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/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
deleted file mode 100644
index 7e0be6d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
+++ /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.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 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/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
deleted file mode 100644
index 6e38d264..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-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.mock
-import org.mockito.junit.MockitoJUnit
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-public class MediaPlayerDataTest : SysuiTestCase() {
-
- @Mock
- private lateinit var playerIsPlaying: MediaControlPanel
- private var systemClock: FakeSystemClock = FakeSystemClock()
-
- @JvmField
- @Rule
- val mockito = MockitoJUnit.rule()
-
- companion object {
- val LOCAL = MediaData.PLAYBACK_LOCAL
- val REMOTE = MediaData.PLAYBACK_CAST_LOCAL
- val RESUMPTION = true
- val PLAYING = true
- val UNDETERMINED = null
- }
-
- @Before
- fun setup() {
- MediaPlayerData.clear()
- }
-
- @Test
- fun addPlayingThenRemote() {
- val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
-
- 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)
-
- val players = MediaPlayerData.players()
- assertThat(players).hasSize(2)
- assertThat(players).containsExactly(playerIsPlaying, playerIsRemote).inOrder()
- }
-
- @Test
- fun switchPlayersPlaying() {
- val playerIsPlaying1 = mock(MediaControlPanel::class.java)
- var dataIsPlaying1 = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
-
- val playerIsPlaying2 = mock(MediaControlPanel::class.java)
- var dataIsPlaying2 = createMediaData("app2", !PLAYING, LOCAL, !RESUMPTION)
-
- MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock,
- isSsReactivated = false)
- systemClock.advanceTime(1)
- 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)
- systemClock.advanceTime(1)
-
- MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock,
- isSsReactivated = false)
- systemClock.advanceTime(1)
-
- val players = MediaPlayerData.players()
- assertThat(players).hasSize(2)
- assertThat(players).containsExactly(playerIsPlaying2, playerIsPlaying1).inOrder()
- }
-
- @Test
- fun fullOrderTest() {
- val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
-
- val playerIsPlayingAndRemote = mock(MediaControlPanel::class.java)
- val dataIsPlayingAndRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
-
- val playerIsStoppedAndLocal = mock(MediaControlPanel::class.java)
- val dataIsStoppedAndLocal = createMediaData("app3", !PLAYING, LOCAL, !RESUMPTION)
-
- val playerIsStoppedAndRemote = mock(MediaControlPanel::class.java)
- val dataIsStoppedAndRemote = createMediaData("app4", !PLAYING, REMOTE, !RESUMPTION)
-
- val playerCanResume = mock(MediaControlPanel::class.java)
- val dataCanResume = createMediaData("app5", !PLAYING, LOCAL, RESUMPTION)
-
- val playerUndetermined = mock(MediaControlPanel::class.java)
- val dataUndetermined = createMediaData("app6", UNDETERMINED, LOCAL, RESUMPTION)
-
- MediaPlayerData.addMediaPlayer(
- "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)
- 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()
- }
-
- @Test
- fun testMoveMediaKeysAround() {
- val keyA = "a"
- val keyB = "b"
-
- val data = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
-
- assertThat(MediaPlayerData.players()).hasSize(0)
-
- MediaPlayerData.addMediaPlayer(keyA, data, playerIsPlaying, systemClock,
- isSsReactivated = false)
- systemClock.advanceTime(1)
-
- assertThat(MediaPlayerData.players()).hasSize(1)
- MediaPlayerData.addMediaPlayer(keyB, data, playerIsPlaying, systemClock,
- isSsReactivated = false)
- systemClock.advanceTime(1)
-
- assertThat(MediaPlayerData.players()).hasSize(2)
-
- MediaPlayerData.moveIfExists(keyA, keyB)
-
- assertThat(MediaPlayerData.players()).hasSize(1)
-
- assertThat(MediaPlayerData.getMediaPlayer(keyA)).isNull()
- assertThat(MediaPlayerData.getMediaPlayer(keyB)).isNotNull()
- }
-
- private fun createMediaData(
- app: String,
- isPlaying: Boolean?,
- location: Int,
- resumption: Boolean
- ) = 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/MediaTestUtils.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt
deleted file mode 100644
index 3d9ed5f..0000000
--- 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 ee32793..0000000
--- 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 0000000..3437365
--- /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 0000000..c829d4c
--- /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
similarity index 92%
rename from packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
index 9e9cda8..97b18e2 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 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.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 @@
@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 @@
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
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt
index 5973340..7cd8e74 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 @@
private lateinit var viewModel: SeekBarViewModel
private lateinit var fakeExecutor: FakeExecutor
- private val taskExecutor: TaskExecutor = object : TaskExecutor() {
- override fun executeOnDiskIO(runnable: Runnable) {
- runnable.run()
+ 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
+ }
}
- 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
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 @@
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 @@
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 @@
// 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 @@
@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 @@
@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 @@
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 @@
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 @@
@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 @@
// 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 @@
// 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 @@
// 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 @@
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 @@
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 @@
@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
similarity index 62%
rename from packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
index b5078bc..1d6e980 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 @@
@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 @@
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 @@
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 @@
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 @@
}
}
-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
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
index 04b93d7..4d2d0f0 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.reset;
import static org.mockito.Mockito.verify;
-import android.graphics.Color;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -34,6 +33,8 @@
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
similarity index 82%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
index 6468fe1..575b1c6 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.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 @@
@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 @@
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 @@
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 @@
`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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
// 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 @@
// 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 @@
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
similarity index 60%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
index f9c7d2d..11eb26b 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.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 @@
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()
}
- build()
- }
- metadataBuilder = MediaMetadata.Builder().apply {
- putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
- putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
- }
+ 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 @@
// 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 @@
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 @@
@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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
@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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
// 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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
// 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 @@
// 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 @@
// 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 @@
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 @@
@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 @@
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
similarity index 93%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
index 121c894..a45e9d9 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.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 @@
fakeFgExecutor = FakeExecutor(FakeSystemClock())
fakeBgExecutor = FakeExecutor(FakeSystemClock())
localBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager::class.java)
- manager = MediaDeviceManager(
+ manager =
+ MediaDeviceManager(
context,
controllerFactory,
lmmFactory,
@@ -120,7 +125,7 @@
fakeFgExecutor,
fakeBgExecutor,
dumpster
- )
+ )
manager.addListener(listener)
// Configure mocks.
@@ -134,11 +139,9 @@
// 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 @@
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 @@
@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 @@
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 @@
}
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 @@
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 @@
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
similarity index 84%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilterTest.kt
index 5586453..3099609 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.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.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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
index 823d4ae..344dffa 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.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.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 @@
@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 @@
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 @@
// 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 @@
// 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 @@
// 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 @@
@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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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 @@
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 @@
@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 @@
@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 @@
@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 @@
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 @@
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
similarity index 78%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt
index 83168cb..84fdfd7 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 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 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 @@
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 @@
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 @@
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 @@
// 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 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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
similarity index 91%
rename from packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt
index dafaa6b..a04cfd4 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.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 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 @@
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 @@
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 @@
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 @@
}
}
- /**
- * 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 @@
}
}
- /**
- * 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 @@
}
}
- /**
- * 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 @@
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
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/AnimationBindHandlerTest.kt
index e4cab18..99f56b1 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 @@
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
similarity index 90%
rename from packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt
index f56d42e..5bb74e5 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.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 @@
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
similarity index 84%
rename from packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerTest.kt
index c41fac7..2026006 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 @@
@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 @@
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 @@
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 @@
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 0000000..c8e8943
--- /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
similarity index 80%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index 7de5719..5843053 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.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 @@
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 @@
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 @@
// 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 @@
}
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 @@
action1.id,
action2.id,
action3.id,
- action4.id)
+ action4.id
+ )
}
whenever(viewHolder.player).thenReturn(view)
@@ -394,9 +416,7 @@
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 @@
.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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
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 @@
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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
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
similarity index 85%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
index 954b438..071604d 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 @@
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 @@
@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 @@
wakefulnessLifecycle,
notifPanelEvents,
settings,
- fakeHandler,)
+ fakeHandler,
+ )
verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
verify(statusBarStateController).addCallback(statusBarCallback.capture())
verify(dreamOverlayStateController).addCallback(dreamOverlayCallback.capture())
@@ -125,7 +126,7 @@
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 @@
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 @@
// 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 @@
val expectedTranslation = LOCKSCREEN_TOP - QS_TOP
assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY())
- .isEqualTo(expectedTranslation)
+ .isEqualTo(expectedTranslation)
}
@Test
@@ -343,27 +371,31 @@
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/controls/ui/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt
new file mode 100644
index 0000000..32b822d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.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
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+public class MediaPlayerDataTest : SysuiTestCase() {
+
+ @Mock private lateinit var playerIsPlaying: MediaControlPanel
+ private var systemClock: FakeSystemClock = FakeSystemClock()
+
+ @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+ companion object {
+ val LOCAL = MediaData.PLAYBACK_LOCAL
+ val REMOTE = MediaData.PLAYBACK_CAST_LOCAL
+ val RESUMPTION = true
+ val PLAYING = true
+ val UNDETERMINED = null
+ }
+
+ @Before
+ fun setup() {
+ MediaPlayerData.clear()
+ }
+
+ @Test
+ fun addPlayingThenRemote() {
+ val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
+
+ 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
+ )
+
+ val players = MediaPlayerData.players()
+ assertThat(players).hasSize(2)
+ assertThat(players).containsExactly(playerIsPlaying, playerIsRemote).inOrder()
+ }
+
+ @Test
+ fun switchPlayersPlaying() {
+ val playerIsPlaying1 = mock(MediaControlPanel::class.java)
+ var dataIsPlaying1 = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
+
+ val playerIsPlaying2 = mock(MediaControlPanel::class.java)
+ var dataIsPlaying2 = createMediaData("app2", !PLAYING, LOCAL, !RESUMPTION)
+
+ MediaPlayerData.addMediaPlayer(
+ "1",
+ dataIsPlaying1,
+ playerIsPlaying1,
+ systemClock,
+ isSsReactivated = false
+ )
+ systemClock.advanceTime(1)
+ 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
+ )
+ systemClock.advanceTime(1)
+
+ MediaPlayerData.addMediaPlayer(
+ "2",
+ dataIsPlaying2,
+ playerIsPlaying2,
+ systemClock,
+ isSsReactivated = false
+ )
+ systemClock.advanceTime(1)
+
+ val players = MediaPlayerData.players()
+ assertThat(players).hasSize(2)
+ assertThat(players).containsExactly(playerIsPlaying2, playerIsPlaying1).inOrder()
+ }
+
+ @Test
+ fun fullOrderTest() {
+ val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
+
+ val playerIsPlayingAndRemote = mock(MediaControlPanel::class.java)
+ val dataIsPlayingAndRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
+
+ val playerIsStoppedAndLocal = mock(MediaControlPanel::class.java)
+ val dataIsStoppedAndLocal = createMediaData("app3", !PLAYING, LOCAL, !RESUMPTION)
+
+ val playerIsStoppedAndRemote = mock(MediaControlPanel::class.java)
+ val dataIsStoppedAndRemote = createMediaData("app4", !PLAYING, REMOTE, !RESUMPTION)
+
+ val playerCanResume = mock(MediaControlPanel::class.java)
+ val dataCanResume = createMediaData("app5", !PLAYING, LOCAL, RESUMPTION)
+
+ val playerUndetermined = mock(MediaControlPanel::class.java)
+ val dataUndetermined = createMediaData("app6", UNDETERMINED, LOCAL, RESUMPTION)
+
+ MediaPlayerData.addMediaPlayer(
+ "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
+ )
+ 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()
+ }
+
+ @Test
+ fun testMoveMediaKeysAround() {
+ val keyA = "a"
+ val keyB = "b"
+
+ val data = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
+
+ assertThat(MediaPlayerData.players()).hasSize(0)
+
+ MediaPlayerData.addMediaPlayer(
+ keyA,
+ data,
+ playerIsPlaying,
+ systemClock,
+ isSsReactivated = false
+ )
+ systemClock.advanceTime(1)
+
+ assertThat(MediaPlayerData.players()).hasSize(1)
+ MediaPlayerData.addMediaPlayer(
+ keyB,
+ data,
+ playerIsPlaying,
+ systemClock,
+ isSsReactivated = false
+ )
+ systemClock.advanceTime(1)
+
+ assertThat(MediaPlayerData.players()).hasSize(2)
+
+ MediaPlayerData.moveIfExists(keyA, keyB)
+
+ assertThat(MediaPlayerData.players()).hasSize(1)
+
+ assertThat(MediaPlayerData.getMediaPlayer(keyA)).isNull()
+ assertThat(MediaPlayerData.getMediaPlayer(keyB)).isNotNull()
+ }
+
+ private fun createMediaData(
+ app: String,
+ isPlaying: Boolean?,
+ location: Int,
+ resumption: Boolean
+ ) =
+ 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
similarity index 91%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
index 622a512..6b76155 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 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
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MetadataAnimationHandlerTest.kt
index 311aa96..323b781 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.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 @@
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
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/SquigglyProgressTest.kt
index d087b0f..d6cff81 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 @@
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 29188da..ce885c0 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 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 af53016..ed928a3 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.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/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
index 7c83cb7..6a4c0f6 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 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 @@
}
@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 110bbb8..fdeb3f5 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.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 @@
@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
@@ -85,6 +101,18 @@
whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true)
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 @@
accessibilityManager,
configurationController,
powerManager,
- uiEventLogger,
falsingManager,
falsingCollector,
viewUtil,
+ vibratorHelper,
)
chipbarCoordinator.start()
@@ -149,10 +177,17 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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
@@ -407,53 +579,113 @@
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)
+ private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
- /** Helper method providing default parameters to not clutter up the tests. */
- private fun almostCloseToEndCast() =
- ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo)
+ private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error)
- /** Helper method providing default parameters to not clutter up the tests. */
- private fun transferToReceiverTriggered() =
- ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo)
+ private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button)
- /** 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 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 0badd861..1bc4719 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 @@
}
@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/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index d2c2d58..cd7a949 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.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 b847ad0..caf8321 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.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 e539705..3c867ab 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.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 1c686c6..5e9c1aa 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 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.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 @@
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 @@
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 @@
mFooter.refreshState();
TestableLooper.get(this).processAllMessages();
- assertEquals(View.GONE, mRootView.getVisibility());
+ assertEquals(View.GONE, mSecurityFooterView.getVisibility());
}
@Test
@@ -165,7 +171,7 @@
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 @@
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 @@
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 @@
mFooter.refreshState();
TestableLooper.get(this).processAllMessages();
- assertEquals(View.GONE, mRootView.getVisibility());
+ assertEquals(View.GONE, mSecurityFooterView.getVisibility());
}
@Test
@@ -227,8 +233,8 @@
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 @@
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 @@
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 @@
@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 @@
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 @@
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 3c58b6fc..c452872 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.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 @@
@Mock
private DumpManager mDumpManager;
@Mock
- private QSTile.State mMockState;
- @Mock
private CentralSurfaces mCentralSurfaces;
@Mock
private QSLogger mQSLogger;
@@ -195,7 +194,6 @@
}
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 @@
} 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 @@
@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 @@
.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 @@
@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 6af8e49..f53e997 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.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 0000000..629c663
--- /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 3c25807..2c2ddbb 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.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 @@
val underTest = utils.footerActionsInteractor(qsSecurityFooterUtils = qsSecurityFooterUtils)
val quickSettingsContext = mock<Context>()
- underTest.showDeviceMonitoringDialog(quickSettingsContext)
+
+ underTest.showDeviceMonitoringDialog(quickSettingsContext, null)
verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null)
- val view = mock<View>()
- whenever(view.context).thenReturn(quickSettingsContext)
- underTest.showDeviceMonitoringDialog(view)
- verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null)
+ val expandable = mock<Expandable>()
+ underTest.showDeviceMonitoringDialog(quickSettingsContext, expandable)
+ verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, expandable)
}
@Test
@@ -85,8 +85,8 @@
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 @@
.showOrHideDialog(
/* keyguardShowing= */ false,
/* isDeviceProvisioned= */ true,
- view,
+ expandable,
)
}
@@ -167,11 +167,11 @@
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 @@
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/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
index 9d908fd..0a34810 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.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.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 @@
@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 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
- `when`(launchView.context).thenReturn(mContext)
`when`(dialog.context).thenReturn(mContext)
controller = UserSwitchDialogController(
@@ -94,32 +94,34 @@
@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 @@
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 @@
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 0000000..1130bda
--- /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/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index d095add..02f28a2 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 @@
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;
@@ -104,10 +105,11 @@
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;
@@ -254,6 +256,7 @@
@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;
@@ -377,6 +380,7 @@
NotificationWakeUpCoordinator coordinator =
new NotificationWakeUpCoordinator(
+ mDumpManager,
mock(HeadsUpManagerPhone.class),
new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager,
mInteractionJankMonitor),
@@ -392,6 +396,7 @@
mConfigurationController,
mStatusBarStateController,
mFalsingManager,
+ mShadeExpansionStateManager,
mLockscreenShadeTransitionController,
new FalsingCollectorFake(),
mDumpManager);
@@ -430,6 +435,7 @@
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 =
@@ -473,6 +479,7 @@
mPrivacyDotViewController,
mTapAgainViewController,
mNavigationModeController,
+ mNavigationBarController,
mFragmentService,
mContentResolver,
mRecordingController,
@@ -757,6 +764,38 @@
}
@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);
@@ -1568,7 +1607,7 @@
mNotificationPanelViewController.mStatusBarStateListener;
statusBarStateListener.onStateChanged(KEYGUARD);
mNotificationPanelViewController.setDozing(false, false);
- when(mUpdateMonitor.isFaceDetectionRunning()).thenReturn(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);
@@ -1583,7 +1622,7 @@
mNotificationPanelViewController.mStatusBarStateListener;
statusBarStateListener.onStateChanged(KEYGUARD);
mNotificationPanelViewController.setDozing(false, false);
- when(mUpdateMonitor.isFaceDetectionRunning()).thenReturn(true);
+ 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);
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 12ef036..bdafc7d 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 @@
@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 @@
navigationModeController,
overviewProxyService,
largeScreenShadeHeaderController,
+ shadeExpansionStateManager,
featureFlags,
delayableExecutor
)
@@ -380,6 +383,7 @@
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 ad3d3d2..95cf9d6 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 @@
@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 @@
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 eb34561..cc45cf88 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 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 @@
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 @@
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/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
index cf5fa87..64dc956 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_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.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 @@
.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 @@
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 @@
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/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index 8643e86..3d11ced 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.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 44cbe51..fbb8ebf 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.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 @@
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 @@
fun setUp() {
whenever(expandableView.collapsedHeight).thenReturn(collapsedHeight)
- pulseExpansionHandler = PulseExpansionHandler(
+ pulseExpansionHandler =
+ PulseExpansionHandler(
mContext,
wakeUpCoordinator,
bypassController,
@@ -74,10 +77,11 @@
configurationController,
statusBarStateController,
falsingManager,
+ shadeExpansionStateManager,
lockscreenShadeTransitionController,
falsingCollector,
dumpManager
- )
+ )
}
@Test
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 4b458f5..dda7fad 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 @@
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 @@
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 851517e..3b05321 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 @@
}
@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/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 340bc96..3ff7639 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 @@
@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 @@
@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 @@
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 e1e5051..590c902 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 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 f4adf69..b6b0b77 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
@@ -181,7 +181,7 @@
@Test
public void testInflatesNewNotification() {
// WHEN there is a new notification
- mCollectionListener.onEntryAdded(mEntry);
+ mCollectionListener.onEntryInit(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
// THEN we inflate it
@@ -194,7 +194,7 @@
@Test
public void testRebindsInflatedNotificationsOnUpdate() {
// GIVEN an inflated notification
- mCollectionListener.onEntryAdded(mEntry);
+ mCollectionListener.onEntryInit(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
@@ -213,7 +213,7 @@
@Test
public void testEntrySmartReplyAdditionWillRebindViews() {
// GIVEN an inflated notification
- mCollectionListener.onEntryAdded(mEntry);
+ mCollectionListener.onEntryInit(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
@@ -232,7 +232,7 @@
@Test
public void testEntryChangedToMinimizedSectionWillRebindViews() {
// GIVEN an inflated notification
- mCollectionListener.onEntryAdded(mEntry);
+ mCollectionListener.onEntryInit(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any());
assertFalse(mParamsCaptor.getValue().isLowPriority());
@@ -254,7 +254,7 @@
public void testMinimizedEntryMovedIntoGroupWillRebindViews() {
// GIVEN an inflated, minimized notification
setSectionIsLowPriority(true);
- mCollectionListener.onEntryAdded(mEntry);
+ mCollectionListener.onEntryInit(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any());
assertTrue(mParamsCaptor.getValue().isLowPriority());
@@ -275,7 +275,7 @@
@Test
public void testEntryRankChangeWillNotRebindViews() {
// GIVEN an inflated notification
- mCollectionListener.onEntryAdded(mEntry);
+ mCollectionListener.onEntryInit(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
@@ -294,7 +294,7 @@
@Test
public void testDoesntFilterInflatedNotifs() {
// GIVEN an inflated notification
- mCollectionListener.onEntryAdded(mEntry);
+ mCollectionListener.onEntryInit(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
@@ -330,9 +330,9 @@
mCollectionListener.onEntryInit(entry);
}
- mCollectionListener.onEntryAdded(summary);
+ mCollectionListener.onEntryInit(summary);
for (NotificationEntry entry : children) {
- mCollectionListener.onEntryAdded(entry);
+ mCollectionListener.onEntryInit(entry);
}
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(groupEntry));
@@ -393,6 +393,70 @@
}
@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";
+ final int summaryId = 1;
+ final GroupEntry group = new GroupEntryBuilder()
+ .setKey(groupKey)
+ .setCreationTime(400)
+ .setSummary(getNotificationEntryBuilder().setId(summaryId).setImportance(1).build())
+ .addChild(getNotificationEntryBuilder().setId(2).build())
+ .addChild(getNotificationEntryBuilder().setId(3).build())
+ .build();
+ fireAddEvents(List.of(group));
+ final NotificationEntry summary = group.getSummary();
+ final NotificationEntry child0 = group.getChildren().get(0);
+ final NotificationEntry child1 = group.getChildren().get(1);
+ mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
+
+ // WHEN all of the children (but not the summary) finish inflating
+ mNotifInflater.invokeInflateCallbackForEntry(child0);
+ mNotifInflater.invokeInflateCallbackForEntry(child1);
+ mNotifInflater.invokeInflateCallbackForEntry(summary);
+
+ // WHEN the summary is updated and starts re-inflating
+ summary.setRanking(new RankingBuilder(summary.getRanking()).setImportance(4).build());
+ fireUpdateEvents(summary);
+ mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
+
+ // THEN the entire group is still not filtered out
+ assertFalse(mUninflatedFilter.shouldFilterOut(summary, 401));
+ assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401));
+ assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401));
+ }
+
+ @Test
public void testCompletedInflatedGroupsAreReleased() {
// GIVEN a newly-posted group with a summary and two children
final GroupEntry group = new GroupEntryBuilder()
@@ -412,7 +476,7 @@
mNotifInflater.invokeInflateCallbackForEntry(child1);
mNotifInflater.invokeInflateCallbackForEntry(summary);
- // THEN the entire group is still filtered out
+ // THEN the entire group is no longer filtered out
assertFalse(mUninflatedFilter.shouldFilterOut(summary, 401));
assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401));
assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401));
@@ -494,7 +558,11 @@
private void fireAddEvents(NotificationEntry entry) {
mCollectionListener.onEntryInit(entry);
- mCollectionListener.onEntryAdded(entry);
+ mCollectionListener.onEntryInit(entry);
+ }
+
+ private void fireUpdateEvents(NotificationEntry entry) {
+ mCollectionListener.onEntryUpdated(entry);
}
private static final String TEST_MESSAGE = "TEST_MESSAGE";
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 46f630b..ea311da 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 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 @@
NotifPipelineFlags mFlags;
@Mock
KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
+ UiEventLoggerFake mUiEventLoggerFake;
@Mock
PendingIntent mPendingIntent;
@@ -107,6 +110,8 @@
MockitoAnnotations.initMocks(this);
when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(false);
+ mUiEventLoggerFake = new UiEventLoggerFake();
+
mNotifInterruptionStateProvider =
new NotificationInterruptStateProviderImpl(
mContext.getContentResolver(),
@@ -120,7 +125,8 @@
mLogger,
mMockHandler,
mFlags,
- mKeyguardNotificationVisibilityProvider);
+ mKeyguardNotificationVisibilityProvider,
+ mUiEventLoggerFake);
mNotifInterruptionStateProvider.mUseHeadsUp = true;
}
@@ -442,6 +448,13 @@
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 @@
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
similarity index 83%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt
index 16e2441..f69839b 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 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 @@
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 @@
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 @@
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 @@
.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 @@
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 @@
.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 @@
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 @@
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 @@
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 @@
}
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 0000000..3a16fb3
--- /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 8375e7c..5394d88 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.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 81b8e98..0000000
--- 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 0000000..562b4df
--- /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 9abdeb9..421f918 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.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/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
index a95a49c..8c8b644 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
});
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 9d848e8..ecc0224 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 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 1c9b0be..90061b0 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.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 @@
@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 @@
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 35c8b61..91aecd8 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 @@
mStackScroller.setCentralSurfaces(mCentralSurfaces);
mStackScroller.setEmptyShadeView(mEmptyShadeView);
when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true);
- when(mStackScrollLayoutController.getNoticationRoundessManager())
+ when(mStackScrollLayoutController.getNotificationRoundnessManager())
.thenReturn(mNotificationRoundnessManager);
mStackScroller.setController(mStackScrollLayoutController);
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 0000000..a2e9230
--- /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 ad497a2..6de8bd5 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 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.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;
@@ -271,7 +273,6 @@
@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 +297,10 @@
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 +324,8 @@
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));
@@ -1017,6 +1020,60 @@
}
@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);
@@ -1102,7 +1159,8 @@
NotificationInterruptLogger logger,
Handler mainHandler,
NotifPipelineFlags flags,
- KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) {
+ KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
+ UiEventLogger uiEventLogger) {
super(
contentResolver,
powerManager,
@@ -1115,7 +1173,8 @@
logger,
mainHandler,
flags,
- keyguardNotificationVisibilityProvider
+ keyguardNotificationVisibilityProvider,
+ uiEventLogger
);
mUseHeadsUp = true;
}
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 8da8d04..0c35659 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 @@
@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/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
index bf43238..eba3b04 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.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.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 @@
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 0000000..b7a6c01
--- /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 0000000..63bbdfc
--- /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 0000000..33a80e1
--- /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 0000000..76016a1
--- /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/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
index f751afc..2f18ce3 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 @@
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 @@
_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 0ba0bd6..a64a4bd 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 @@
}
@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 @@
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 39b886a..71b8bab 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 @@
}
@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 4efb135..c584109 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_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 @@
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 @@
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 a3ad028..a1afcd7 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 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 @@
@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 @@
fun wifiIcon() =
runBlocking(IMMEDIATE) {
wifiRepository.setIsWifiEnabled(testCase.enabled)
+ wifiRepository.setIsWifiDefault(testCase.isDefault)
connectivityRepository.setForceHiddenIcons(
if (testCase.forceHidden) {
setOf(ConnectivitySlot.WIFI)
@@ -101,6 +117,7 @@
.thenReturn(testCase.hasDataCapabilities)
underTest =
WifiViewModel(
+ airplaneModeViewModel,
connectivityConstants,
context,
logger,
@@ -125,19 +142,12 @@
} 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 @@
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 @@
"forceHidden=$forceHidden, " +
"showWhenEnabled=$alwaysShowIconWhenEnabled, " +
"hasDataCaps=$hasDataCapabilities, " +
+ "isDefault=$isDefault, " +
"network=$network) then " +
"EXPECTED($expected)"
}
@@ -303,6 +315,46 @@
),
),
+ // 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 3169eef..7d2c560 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 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 @@
@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 @@
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 @@
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 6ace404..915e999 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 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.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.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 @@
}
@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/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index 6225d0c..9fbf159 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.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.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,437 +60,293 @@
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)
+ fun displayView_loadedIcon_correctlyRendered() {
+ val drawable = context.getDrawable(R.drawable.ic_celebration)!!
- 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)
- }
-
- @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)
- )
- 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)
- }
-
- @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)
- )
- 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()
- }
-
- @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)
- )
- 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()
- }
-
- @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
- )
- }
-
- @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)
- }
-
- @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)
- )
- 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)
- }
-
- @Test
- fun changeFromTransferTriggeredToTransferSucceeded_undoButtonAppears() {
- underTest.displayView(transferToReceiverTriggered())
underTest.displayView(
- transferToReceiverSucceeded(
- object : IUndoMediaTransferCallback.Stub() {
- override fun onUndoTriggered() {}
- }
+ ChipbarInfo(
+ Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
+ Text.Loaded("text"),
+ endItem = null,
)
)
- assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE)
+ val iconView = getChipbarView().getStartIconView()
+ assertThat(iconView.drawable).isEqualTo(drawable)
+ assertThat(iconView.contentDescription).isEqualTo("loadedCD")
}
@Test
- fun changeFromTransferSucceededToAlmostCloseToStart_undoButtonDisappears() {
- underTest.displayView(transferToReceiverSucceeded())
- underTest.displayView(almostCloseToStartCast())
+ 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(getChipView().getUndoButton().visibility).isEqualTo(View.GONE)
+ val iconView = getChipbarView().getStartIconView()
+ assertThat(iconView.contentDescription)
+ .isEqualTo(contentDescription.loadContentDescription(context))
}
@Test
- fun changeFromTransferTriggeredToTransferFailed_failureIconAppears() {
- underTest.displayView(transferToReceiverTriggered())
- underTest.displayView(transferToReceiverFailed())
+ fun displayView_loadedText_correctlyRendered() {
+ underTest.displayView(
+ ChipbarInfo(
+ Icon.Resource(R.id.check_box, null),
+ Text.Loaded("display view text here"),
+ endItem = null,
+ )
+ )
- assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE)
+ assertThat(getChipbarView().getChipText()).isEqualTo("display view text here")
}
- private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
+ @Test
+ fun displayView_resourceText_correctlyRendered() {
+ underTest.displayView(
+ ChipbarInfo(
+ Icon.Resource(R.id.check_box, null),
+ Text.Resource(R.string.screenrecord_start_error),
+ endItem = null,
+ )
+ )
+
+ assertThat(getChipbarView().getChipText())
+ .isEqualTo(context.getString(R.string.screenrecord_start_error))
+ }
+
+ @Test
+ fun displayView_endItemNull_correctlyRendered() {
+ underTest.displayView(
+ ChipbarInfo(
+ Icon.Resource(R.id.check_box, null),
+ Text.Loaded("text"),
+ endItem = null,
+ )
+ )
+
+ 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 displayView_endItemLoading_correctlyRendered() {
+ underTest.displayView(
+ ChipbarInfo(
+ Icon.Resource(R.id.check_box, null),
+ Text.Loaded("text"),
+ endItem = ChipbarEndItem.Loading,
+ )
+ )
+
+ 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 displayView_endItemError_correctlyRendered() {
+ underTest.displayView(
+ ChipbarInfo(
+ Icon.Resource(R.id.check_box, null),
+ Text.Loaded("text"),
+ endItem = ChipbarEndItem.Error,
+ )
+ )
+
+ 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 displayView_endItemButton_correctlyRendered() {
+ underTest.displayView(
+ ChipbarInfo(
+ Icon.Resource(R.id.check_box, null),
+ Text.Loaded("text"),
+ endItem =
+ ChipbarEndItem.Button(
+ Text.Loaded("button text"),
+ onClickListener = {},
+ ),
+ )
+ )
+
+ 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 displayView_endItemButtonClicked_falseTap_listenerNotRun() {
+ whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true)
+ var isClicked = false
+ val buttonClickListener = View.OnClickListener { isClicked = true }
+
+ underTest.displayView(
+ ChipbarInfo(
+ Icon.Resource(R.id.check_box, null),
+ Text.Loaded("text"),
+ endItem =
+ ChipbarEndItem.Button(
+ Text.Loaded("button text"),
+ buttonClickListener,
+ ),
+ )
+ )
+
+ getChipbarView().getEndButton().performClick()
+
+ assertThat(isClicked).isFalse()
+ }
+
+ @Test
+ fun displayView_endItemButtonClicked_notFalseTap_listenerRun() {
+ whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false)
+ var isClicked = false
+ val buttonClickListener = View.OnClickListener { isClicked = true }
+
+ underTest.displayView(
+ ChipbarInfo(
+ Icon.Resource(R.id.check_box, null),
+ Text.Loaded("text"),
+ endItem =
+ ChipbarEndItem.Button(
+ Text.Loaded("button text"),
+ buttonClickListener,
+ ),
+ )
+ )
+
+ getChipbarView().getEndButton().performClick()
+
+ assertThat(isClicked).isTrue()
+ }
+
+ @Test
+ 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(vibratorHelper).vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK))
+ }
+
+ @Test
+ fun updateView_viewUpdated() {
+ // First, display a view
+ val drawable = context.getDrawable(R.drawable.ic_celebration)!!
+
+ underTest.displayView(
+ ChipbarInfo(
+ Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
+ Text.Loaded("title text"),
+ endItem = ChipbarEndItem.Loading,
+ )
+ )
+
+ 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
+ )
+
+ // 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.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 10704ac..17d4023 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 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 @@
accessibilityManager: AccessibilityManager,
configurationController: ConfigurationController,
powerManager: PowerManager,
- uiEventLogger: MediaTttSenderUiEventLogger,
falsingManager: FalsingManager,
falsingCollector: FalsingCollector,
viewUtil: ViewUtil,
+ vibratorHelper: VibratorHelper,
) :
ChipbarCoordinator(
context,
@@ -52,10 +52,10 @@
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 d951f36..525d837 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 @@
val thirdExpectedValue =
setUpUsers(
count = 2,
- hasGuest = true,
+ isLastGuestUser = true,
selectedIndex = 1,
)
underTest.refreshUsers()
@@ -121,21 +121,25 @@
}
@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 = 900
- unsortedUsers[1].creationTime = 700
- unsortedUsers[2].creationTime = 999
- val expectedUsers = listOf(unsortedUsers[1], unsortedUsers[0], unsortedUsers[2])
+ 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
+ )
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 @@
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/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
index 1540f85..97571b2 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 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 @@
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/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 09da52e..fa7ebf6a 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 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.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 @@
private NotificationShadeWindowView mNotificationShadeWindowView;
@Mock
private AuthController mAuthController;
+ @Mock
+ private ShadeExpansionStateManager mShadeExpansionStateManager;
private SysUiState mSysUiState;
private boolean mSysUiStateBubblesExpanded;
@@ -290,7 +294,7 @@
mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController,
mColorExtractor, mDumpManager, mKeyguardStateController,
- mScreenOffAnimationController, mAuthController);
+ mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager);
mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
mNotificationShadeWindowController.attach();
@@ -343,7 +347,8 @@
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 9635faf..e5316bc8 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.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 @@
NotificationInterruptLogger logger,
Handler mainHandler,
NotifPipelineFlags flags,
- KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) {
+ KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
+ UiEventLogger uiEventLogger) {
super(contentResolver,
powerManager,
dreamManager,
@@ -58,7 +60,8 @@
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 cebe946..7af66f6 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.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.MockitoAnnotations;
import java.util.Optional;
+import java.util.concurrent.Executor;
/**
* Tests for {@link WMShell}.
@@ -76,12 +79,14 @@
@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 @@
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 5d52be2..a60b773 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 @@
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/qs/FakeFgsManagerController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
index 5272585..c33ce5d 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 @@
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 2a9aedd..325da4e 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.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 @@
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 8d171be..69575a9 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 @@
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 @@
*
* @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 38237fa..23cacf3 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 @@
}
@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/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index b8af1bf..b11a06e 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.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 @@
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 @@
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 @@
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 @@
+ ", 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 @@
}
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 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();
- mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken));
+ // Current dream stopped, device no longer dreaming.
+ if (dream == mCurrentDream) {
+ mCurrentDream = null;
+
+ if (mSentStartBroadcast) {
+ mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL);
+ }
+
+ 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 @@
mCurrentDream.mService = service;
- if (!mCurrentDream.mIsPreviewMode) {
+ if (!mCurrentDream.mIsPreviewMode && !mSentStartBroadcast) {
mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL);
- mCurrentDream.mSentStartBroadcast = true;
+ mSentStartBroadcast = true;
}
}
@@ -272,10 +301,35 @@
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 @@
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 @@
});
}
+ void stopPreviousDreamsIfNeeded() {
+ if (mCurrentDream == DreamRecord.this) {
+ stopPreviousDreams();
+ }
+ }
+
void releaseWakeLockIfNeeded() {
if (mWakeLock != null) {
mWakeLock.release();
@@ -333,15 +395,5 @@
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 c9557d6..5589673 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 @@
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 6759d79..9a19031 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.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 @@
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 b8131a8..604e8f3 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.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 @@
@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 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 fd1fdce..a55b118 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_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;
@@ -10633,10 +10634,18 @@
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 +10670,16 @@
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());
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 014d580..18c29fa 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 @@
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/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 653b51a9..3df8f58 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 @@
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 0799b95..9c455db 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 @@
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 @@
}
}
+ /** 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 9727558..1469359 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 @@
}
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/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index bf4b65d..3a8fbbb 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 @@
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 @@
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 731754e..eba49bb 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_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 @@
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 867833a..509b1e6 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 @@
}
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 @@
@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 @@
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 @@
}
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 @@
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 @@
}
}
- 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 @@
}
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 @@
}
}
+ 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/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 2ee4af5..4614194 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1823,7 +1823,8 @@
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/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
new file mode 100644
index 0000000..303a370
--- /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 235849c..c484f45 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 @@
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 @@
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 @@
@Override
public void off() {
mOffCount++;
+ applyLatency(mOffLatency);
}
@Override
public void setAmplitude(float amplitude) {
mAmplitudes.add(amplitude);
- applyLatency();
+ applyLatency(mOnLatency);
}
@Override
@@ -121,7 +123,7 @@
}
recordEffectSegment(vibrationId,
new PrebakedSegment((int) effect, false, (int) strength));
- applyLatency();
+ applyLatency(mOnLatency);
scheduleListener(EFFECT_DURATION, vibrationId);
return EFFECT_DURATION;
}
@@ -141,7 +143,7 @@
duration += EFFECT_DURATION + primitive.getDelay();
recordEffectSegment(vibrationId, primitive);
}
- applyLatency();
+ applyLatency(mOnLatency);
scheduleListener(duration, vibrationId);
return duration;
}
@@ -154,7 +156,7 @@
recordEffectSegment(vibrationId, primitive);
}
recordBraking(vibrationId, braking);
- applyLatency();
+ applyLatency(mOnLatency);
scheduleListener(duration, vibrationId);
return duration;
}
@@ -193,10 +195,10 @@
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 @@
/**
* 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 0551bfc..42a2c10 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 @@
// 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 @@
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 @@
// 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 fe0a79c..039e159 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 @@
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 @@
// 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/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
index c5131c8..101a5cb 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 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.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 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.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 @@
private PackageManager mPm;
@Mock
private IPackageManager miPm;
+ @Mock
+ private Resources mResources;
@Mock
NotificationManagerService mNm;
@@ -96,7 +104,8 @@
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 @@
@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 @@
}
@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);
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 d765042..0000000
--- 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/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index 1cd0b19..e30e5db 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 @@
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 0b23359..4202f46 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.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.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
import java.util.List;
@@ -762,6 +764,50 @@
}
@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 @@
@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;
+ // 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 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();
-
- // Verifies that event was not sent
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 @@
.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;
+ // 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 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();
-
- // Verifies that event was not sent
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 @@
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/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 3da4711..352d8d1 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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;
}
}