Merge "Enable user graularity for lockdown mode" into tm-qpr-dev
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
index 7e814af..1403ba2 100644
--- a/core/java/android/animation/AnimationHandler.java
+++ b/core/java/android/animation/AnimationHandler.java
@@ -432,8 +432,9 @@
/**
* Callbacks that receives notifications for animation timing and frame commit timing.
+ * @hide
*/
- interface AnimationFrameCallback {
+ public interface AnimationFrameCallback {
/**
* Run animation based on the frame time.
* @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index da6a551..edf96f7 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -159,6 +159,7 @@
void clearRequestedListenerHints(in INotificationListener token);
void requestHintsFromListener(in INotificationListener token, int hints);
int getHintsFromListener(in INotificationListener token);
+ int getHintsFromListenerNoToken();
void requestInterruptionFilterFromListener(in INotificationListener token, int interruptionFilter);
int getInterruptionFilterFromListener(in INotificationListener token);
void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim);
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 f320b74..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);
}
@@ -9591,21 +9591,16 @@
@NonNull
public ArrayList<Action> getActionsListWithSystemActions() {
// Define the system actions we expect to see
- final Action negativeAction = makeNegativeAction();
- final Action answerAction = makeAnswerAction();
- // Sort the expected actions into the correct order:
- // * If there's no answer action, put the hang up / decline action at the end
- // * Otherwise put the answer action at the end, and put the decline action at start.
- final Action firstAction = answerAction == null ? null : negativeAction;
- final Action lastAction = answerAction == null ? negativeAction : answerAction;
+ final Action firstAction = makeNegativeAction();
+ final Action lastAction = makeAnswerAction();
// Start creating the result list.
int nonContextualActionSlotsRemaining = MAX_ACTION_BUTTONS;
ArrayList<Action> resultActions = new ArrayList<>(MAX_ACTION_BUTTONS);
- if (firstAction != null) {
- resultActions.add(firstAction);
- --nonContextualActionSlotsRemaining;
- }
+
+ // Always have a first action.
+ resultActions.add(firstAction);
+ --nonContextualActionSlotsRemaining;
// Copy actions into the new list, correcting system actions.
if (mBuilder.mActions != null) {
@@ -9621,14 +9616,14 @@
--nonContextualActionSlotsRemaining;
}
// If there's exactly one action slot left, fill it with the lastAction.
- if (nonContextualActionSlotsRemaining == 1) {
+ if (lastAction != null && nonContextualActionSlotsRemaining == 1) {
resultActions.add(lastAction);
--nonContextualActionSlotsRemaining;
}
}
}
// If there are any action slots left, the lastAction still needs to be added.
- if (nonContextualActionSlotsRemaining >= 1) {
+ if (lastAction != null && nonContextualActionSlotsRemaining >= 1) {
resultActions.add(lastAction);
}
return resultActions;
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 392f52a..f6d27ad 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -66,9 +66,9 @@
/**
* Class to notify the user of events that happen. This is how you tell
- * the user that something has happened in the background. {@more}
+ * the user that something has happened in the background.
*
- * Notifications can take different forms:
+ * <p>Notifications can take different forms:
* <ul>
* <li>A persistent icon that goes in the status bar and is accessible
* through the launcher, (when the user selects it, a designated Intent
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index e022ca3..0ea53ce 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -2080,6 +2080,21 @@
}
/**
+ * Set the live wallpaper for the given screen(s).
+ *
+ * This can only be called by packages with android.permission.SET_WALLPAPER_COMPONENT
+ * permission. The caller must hold the INTERACT_ACROSS_USERS_FULL permission to change
+ * another user's wallpaper.
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT)
+ public boolean setWallpaperComponentWithFlags(@NonNull ComponentName name,
+ @SetWallpaperFlags int which) {
+ return setWallpaperComponent(name);
+ }
+
+ /**
* Set the display position of the current wallpaper within any larger space, when
* that wallpaper is visible behind the given window. The X and Y offsets
* are floating point numbers ranging from 0 to 1, representing where the
diff --git a/core/java/android/app/servertransaction/PendingTransactionActions.java b/core/java/android/app/servertransaction/PendingTransactionActions.java
index a47fe82..8174778 100644
--- a/core/java/android/app/servertransaction/PendingTransactionActions.java
+++ b/core/java/android/app/servertransaction/PendingTransactionActions.java
@@ -25,11 +25,12 @@
import android.os.PersistableBundle;
import android.os.TransactionTooLargeException;
import android.util.Log;
-import android.util.LogWriter;
import android.util.Slog;
import com.android.internal.util.IndentingPrintWriter;
+import java.io.StringWriter;
+
/**
* Container that has data pending to be used at later stages of
* {@link android.app.servertransaction.ClientTransaction}.
@@ -134,6 +135,16 @@
mDescription = description;
}
+ private String collectBundleStates() {
+ final StringWriter writer = new StringWriter();
+ final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
+ pw.println("Bundle stats:");
+ Bundle.dumpStats(pw, mState);
+ pw.println("PersistableBundle stats:");
+ Bundle.dumpStats(pw, mPersistentState);
+ return writer.toString().stripTrailing();
+ }
+
@Override
public void run() {
// Tell activity manager we have been stopped.
@@ -142,19 +153,24 @@
// TODO(lifecycler): Use interface callback instead of AMS.
ActivityClient.getInstance().activityStopped(
mActivity.token, mState, mPersistentState, mDescription);
- } catch (RuntimeException ex) {
- // Dump statistics about bundle to help developers debug
- final LogWriter writer = new LogWriter(Log.WARN, TAG);
- final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
- pw.println("Bundle stats:");
- Bundle.dumpStats(pw, mState);
- pw.println("PersistableBundle stats:");
- Bundle.dumpStats(pw, mPersistentState);
+ } catch (RuntimeException runtimeException) {
+ // Collect the statistics about bundle
+ final String bundleStats = collectBundleStates();
- if (ex.getCause() instanceof TransactionTooLargeException
- && mActivity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) {
- Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex);
- return;
+ RuntimeException ex = runtimeException;
+ if (ex.getCause() instanceof TransactionTooLargeException) {
+ // Embed the stats into exception message to help developers debug if the
+ // transaction size is too large.
+ final String message = ex.getMessage() + "\n" + bundleStats;
+ ex = new RuntimeException(message, ex.getCause());
+ if (mActivity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) {
+ Log.e(TAG, "App sent too much data in instance state, so it was ignored",
+ ex);
+ return;
+ }
+ } else {
+ // Otherwise, dump the stats anyway.
+ Log.w(TAG, bundleStats);
}
throw ex;
}
diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java
index cc303fb..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
@@ -418,14 +434,7 @@
AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget);
view.setInteractionHandler(mInteractionHandler);
view.setAppWidget(appWidgetId, appWidget);
- addListener(appWidgetId, view);
- RemoteViews views;
- try {
- views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
- } catch (RemoteException e) {
- throw new RuntimeException("system server dead?", e);
- }
- view.updateAppWidget(views);
+ setListener(appWidgetId, view);
return view;
}
@@ -513,13 +522,19 @@
* The AppWidgetHost retains a pointer to the newly-created listener.
* @param appWidgetId The ID of the app widget for which to add the listener
* @param listener The listener interface that deals with actions towards the widget view
- *
* @hide
*/
- public void addListener(int appWidgetId, @NonNull AppWidgetHostListener listener) {
+ public void setListener(int appWidgetId, @NonNull AppWidgetHostListener listener) {
synchronized (mListeners) {
mListeners.put(appWidgetId, listener);
}
+ RemoteViews views = null;
+ try {
+ views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
+ } catch (RemoteException e) {
+ throw new RuntimeException("system server dead?", e);
+ }
+ listener.updateAppWidget(views);
}
/**
diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
index 0f6010f..2a47851 100644
--- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
@@ -272,6 +272,9 @@
@Override
public void onServiceConnected(ComponentName component, IBinder binder) {
mProxy = ICameraExtensionsProxyService.Stub.asInterface(binder);
+ if (mProxy == null) {
+ throw new IllegalStateException("Camera Proxy service is null");
+ }
try {
mSupportsAdvancedExtensions = mProxy.advancedExtensionsSupported();
} catch (RemoteException e) {
diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java
index 5ca0da2..8afd6de 100644
--- a/core/java/android/os/PowerManagerInternal.java
+++ b/core/java/android/os/PowerManagerInternal.java
@@ -335,4 +335,16 @@
/** Allows power button to intercept a power key button press. */
public abstract boolean interceptPowerKeyDown(KeyEvent event);
+
+ /**
+ * Internal version of {@link android.os.PowerManager#nap} which allows for napping while the
+ * device is not awake.
+ */
+ public abstract void nap(long eventTime, boolean allowWake);
+
+ /**
+ * Returns true if ambient display is suppressed by any app with any token. This method will
+ * return false if ambient display is not available.
+ */
+ public abstract boolean isAmbientDisplaySuppressed();
}
diff --git a/core/java/android/preference/SeekBarVolumizer.java b/core/java/android/preference/SeekBarVolumizer.java
index 0a6a405..16f9a12 100644
--- a/core/java/android/preference/SeekBarVolumizer.java
+++ b/core/java/android/preference/SeekBarVolumizer.java
@@ -115,6 +115,7 @@
private final int mMaxStreamVolume;
private boolean mAffectedByRingerMode;
private boolean mNotificationOrRing;
+ private final boolean mNotifAliasRing;
private final Receiver mReceiver = new Receiver();
private Handler mHandler;
@@ -179,6 +180,8 @@
if (mNotificationOrRing) {
mRingerMode = mAudioManager.getRingerModeInternal();
}
+ mNotifAliasRing = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_alias_ring_notif_stream_types);
mZenMode = mNotificationManager.getZenMode();
if (hasAudioProductStrategies()) {
@@ -280,7 +283,15 @@
if (zenMuted) {
mSeekBar.setProgress(mLastAudibleStreamVolume, true);
} else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
- mSeekBar.setProgress(0, true);
+ /**
+ * the first variable above is preserved and the conditions below are made explicit
+ * so that when user attempts to slide the notification seekbar out of vibrate the
+ * seekbar doesn't wrongly snap back to 0 when the streams aren't aliased
+ */
+ if (mNotifAliasRing || mStreamType == AudioManager.STREAM_RING
+ || (mStreamType == AudioManager.STREAM_NOTIFICATION && mMuted)) {
+ mSeekBar.setProgress(0, true);
+ }
} else if (mMuted) {
mSeekBar.setProgress(0, true);
} else {
@@ -354,6 +365,7 @@
// set the time of stop volume
if ((mStreamType == AudioManager.STREAM_VOICE_CALL
|| mStreamType == AudioManager.STREAM_RING
+ || (!mNotifAliasRing && mStreamType == AudioManager.STREAM_NOTIFICATION)
|| mStreamType == AudioManager.STREAM_ALARM)) {
sStopVolumeTime = java.lang.System.currentTimeMillis();
}
@@ -631,8 +643,8 @@
}
private void updateVolumeSlider(int streamType, int streamValue) {
- final boolean streamMatch = mNotificationOrRing ? isNotificationOrRing(streamType)
- : (streamType == mStreamType);
+ final boolean streamMatch = mNotifAliasRing && mNotificationOrRing
+ ? isNotificationOrRing(streamType) : streamType == mStreamType;
if (mSeekBar != null && streamMatch && streamValue != -1) {
final boolean muted = mAudioManager.isStreamMute(mStreamType)
|| streamValue == 0;
diff --git a/core/java/android/service/dreams/DreamActivity.java b/core/java/android/service/dreams/DreamActivity.java
index f6a7c8e..a2fa139 100644
--- a/core/java/android/service/dreams/DreamActivity.java
+++ b/core/java/android/service/dreams/DreamActivity.java
@@ -44,6 +44,8 @@
public class DreamActivity extends Activity {
static final String EXTRA_CALLBACK = "binder";
static final String EXTRA_DREAM_TITLE = "title";
+ @Nullable
+ private DreamService.DreamActivityCallbacks mCallback;
public DreamActivity() {}
@@ -57,11 +59,19 @@
}
final Bundle extras = getIntent().getExtras();
- final DreamService.DreamActivityCallback callback =
- (DreamService.DreamActivityCallback) extras.getBinder(EXTRA_CALLBACK);
+ mCallback = (DreamService.DreamActivityCallbacks) extras.getBinder(EXTRA_CALLBACK);
- if (callback != null) {
- callback.onActivityCreated(this);
+ if (mCallback != null) {
+ mCallback.onActivityCreated(this);
}
}
+
+ @Override
+ public void onDestroy() {
+ if (mCallback != null) {
+ mCallback.onActivityDestroyed();
+ }
+
+ super.onDestroy();
+ }
}
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 4324442..aa45c20 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -42,8 +42,11 @@
private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
@Override
public void startDream(WindowManager.LayoutParams layoutParams,
- IDreamOverlayCallback callback) {
+ IDreamOverlayCallback callback, String dreamComponent,
+ boolean shouldShowComplications) {
mDreamOverlayCallback = callback;
+ mDreamComponent = ComponentName.unflattenFromString(dreamComponent);
+ mShowComplications = shouldShowComplications;
onStartDream(layoutParams);
}
};
@@ -56,10 +59,6 @@
@Nullable
@Override
public final IBinder onBind(@NonNull Intent intent) {
- mShowComplications = intent.getBooleanExtra(DreamService.EXTRA_SHOW_COMPLICATIONS,
- DreamService.DEFAULT_SHOW_COMPLICATIONS);
- mDreamComponent = intent.getParcelableExtra(DreamService.EXTRA_DREAM_COMPONENT,
- ComponentName.class);
return mDreamOverlay.asBinder();
}
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 3b7698e3..32bdf79 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -214,19 +214,6 @@
private static final String DREAM_META_DATA_ROOT_TAG = "dream";
/**
- * Extra containing a boolean for whether to show complications on the overlay.
- * @hide
- */
- public static final String EXTRA_SHOW_COMPLICATIONS =
- "android.service.dreams.SHOW_COMPLICATIONS";
-
- /**
- * Extra containing the component name for the active dream.
- * @hide
- */
- public static final String EXTRA_DREAM_COMPONENT = "android.service.dreams.DREAM_COMPONENT";
-
- /**
* The default value for whether to show complications on the overlay.
*
* @hide
@@ -252,6 +239,9 @@
private boolean mDebug = false;
+ private ComponentName mDreamComponent;
+ private boolean mShouldShowComplications;
+
private DreamServiceWrapper mDreamServiceWrapper;
private Runnable mDispatchAfterOnAttachedToWindow;
@@ -947,6 +937,11 @@
@Override
public void onCreate() {
if (mDebug) Slog.v(mTag, "onCreate()");
+
+ mDreamComponent = new ComponentName(this, getClass());
+ mShouldShowComplications = fetchShouldShowComplications(this /*context*/,
+ fetchServiceInfo(this /*context*/, mDreamComponent));
+
super.onCreate();
}
@@ -994,14 +989,7 @@
// Connect to the overlay service if present.
if (!mWindowless && overlayComponent != null) {
final Resources resources = getResources();
- final ComponentName dreamService = new ComponentName(this, getClass());
-
- final ServiceInfo serviceInfo = fetchServiceInfo(this, dreamService);
- final Intent overlayIntent = new Intent()
- .setComponent(overlayComponent)
- .putExtra(EXTRA_SHOW_COMPLICATIONS,
- fetchShouldShowComplications(this, serviceInfo))
- .putExtra(EXTRA_DREAM_COMPONENT, dreamService);
+ final Intent overlayIntent = new Intent().setComponent(overlayComponent);
mOverlayConnection = new OverlayConnection(
/* context= */ this,
@@ -1059,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;
}
@@ -1307,7 +1295,7 @@
Intent i = new Intent(this, DreamActivity.class);
i.setPackage(getApplicationContext().getPackageName());
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallback(mDreamToken));
+ i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallbacks(mDreamToken));
final ServiceInfo serviceInfo = fetchServiceInfo(this,
new ComponentName(this, getClass()));
i.putExtra(DreamActivity.EXTRA_DREAM_TITLE, fetchDreamLabel(this, serviceInfo));
@@ -1364,7 +1352,9 @@
// parameters once the window has been attached.
mDreamStartOverlayConsumer = overlay -> {
try {
- overlay.startDream(mWindow.getAttributes(), mOverlayCallback);
+ overlay.startDream(mWindow.getAttributes(), mOverlayCallback,
+ mDreamComponent.flattenToString(),
+ mShouldShowComplications);
} catch (RemoteException e) {
Log.e(mTag, "could not send window attributes:" + e);
}
@@ -1498,10 +1488,10 @@
}
/** @hide */
- final class DreamActivityCallback extends Binder {
+ final class DreamActivityCallbacks extends Binder {
private final IBinder mActivityDreamToken;
- DreamActivityCallback(IBinder token) {
+ DreamActivityCallbacks(IBinder token) {
mActivityDreamToken = token;
}
@@ -1526,6 +1516,12 @@
mActivity = activity;
onWindowCreated(activity.getWindow());
}
+
+ // If DreamActivity is destroyed, wake up from Dream.
+ void onActivityDestroyed() {
+ mActivity = null;
+ onDestroy();
+ }
}
/**
diff --git a/core/java/android/service/dreams/IDreamOverlay.aidl b/core/java/android/service/dreams/IDreamOverlay.aidl
index 2b6633d..05ebbfe 100644
--- a/core/java/android/service/dreams/IDreamOverlay.aidl
+++ b/core/java/android/service/dreams/IDreamOverlay.aidl
@@ -31,7 +31,11 @@
* @param params The {@link LayoutParams} for the associated DreamWindow, including the window
token of the Dream Activity.
* @param callback The {@link IDreamOverlayCallback} for requesting actions such as exiting the
- * dream.
+ * dream.
+ * @param dreamComponent The component name of the dream service requesting overlay.
+ * @param shouldShowComplications Whether the dream overlay should show complications, e.g. clock
+ * and weather.
*/
- void startDream(in LayoutParams params, in IDreamOverlayCallback callback);
+ void startDream(in LayoutParams params, in IDreamOverlayCallback callback,
+ in String dreamComponent, in boolean shouldShowComplications);
}
diff --git a/core/java/android/service/wallpaper/IWallpaperService.aidl b/core/java/android/service/wallpaper/IWallpaperService.aidl
index 56e2486..f46c60f 100644
--- a/core/java/android/service/wallpaper/IWallpaperService.aidl
+++ b/core/java/android/service/wallpaper/IWallpaperService.aidl
@@ -25,6 +25,6 @@
oneway interface IWallpaperService {
void attach(IWallpaperConnection connection,
IBinder windowToken, int windowType, boolean isPreview,
- int reqWidth, int reqHeight, in Rect padding, int displayId);
+ int reqWidth, int reqHeight, in Rect padding, int displayId, int which);
void detach();
}
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 01749c0..e5792a9 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -18,6 +18,7 @@
import static android.app.WallpaperManager.COMMAND_FREEZE;
import static android.app.WallpaperManager.COMMAND_UNFREEZE;
+import static android.app.WallpaperManager.SetWallpaperFlags;
import static android.graphics.Matrix.MSCALE_X;
import static android.graphics.Matrix.MSCALE_Y;
import static android.graphics.Matrix.MSKEW_X;
@@ -2427,7 +2428,7 @@
@Override
public void attach(IWallpaperConnection conn, IBinder windowToken,
int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding,
- int displayId) {
+ int displayId, @SetWallpaperFlags int which) {
mEngineWrapper = new IWallpaperEngineWrapper(mTarget, conn, windowToken,
windowType, isPreview, reqWidth, reqHeight, padding, displayId);
}
diff --git a/core/java/android/view/RemoteAnimationTarget.java b/core/java/android/view/RemoteAnimationTarget.java
index 44f419a..e407707 100644
--- a/core/java/android/view/RemoteAnimationTarget.java
+++ b/core/java/android/view/RemoteAnimationTarget.java
@@ -241,6 +241,8 @@
*/
public boolean willShowImeOnTarget;
+ public int rotationChange;
+
public RemoteAnimationTarget(int taskId, int mode, SurfaceControl leash, boolean isTranslucent,
Rect clipRect, Rect contentInsets, int prefixOrderIndex, Point position,
Rect localBounds, Rect screenSpaceBounds,
@@ -302,6 +304,7 @@
backgroundColor = in.readInt();
showBackdrop = in.readBoolean();
willShowImeOnTarget = in.readBoolean();
+ rotationChange = in.readInt();
}
public void setShowBackdrop(boolean shouldShowBackdrop) {
@@ -316,6 +319,14 @@
return willShowImeOnTarget;
}
+ public void setRotationChange(int rotationChange) {
+ this.rotationChange = rotationChange;
+ }
+
+ public int getRotationChange() {
+ return rotationChange;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -345,6 +356,7 @@
dest.writeInt(backgroundColor);
dest.writeBoolean(showBackdrop);
dest.writeBoolean(willShowImeOnTarget);
+ dest.writeInt(rotationChange);
}
public void dump(PrintWriter pw, String prefix) {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 4522c0d..8f4a836 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -284,7 +284,7 @@
* @hide
*/
public static final boolean LOCAL_LAYOUT =
- SystemProperties.getBoolean("persist.debug.local_layout", false);
+ SystemProperties.getBoolean("persist.debug.local_layout", true);
/**
* Set this system property to true to force the view hierarchy to render
diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java
index 6bf2474..514df59 100644
--- a/core/java/android/view/translation/UiTranslationController.java
+++ b/core/java/android/view/translation/UiTranslationController.java
@@ -175,10 +175,7 @@
*/
public void onActivityDestroyed() {
synchronized (mLock) {
- if (DEBUG) {
- Log.i(TAG,
- "onActivityDestroyed(): mCurrentState is " + stateToString(mCurrentState));
- }
+ Log.i(TAG, "onActivityDestroyed(): mCurrentState is " + stateToString(mCurrentState));
if (mCurrentState != STATE_UI_TRANSLATION_FINISHED) {
notifyTranslationFinished(/* activityDestroyed= */ true);
}
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/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index fbdd325..8815ab3 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -135,8 +135,11 @@
/** This change happened underneath something else. */
public static final int FLAG_IS_OCCLUDED = 1 << 15;
+ /** The container is a system window, excluding wallpaper and input-method. */
+ public static final int FLAG_IS_SYSTEM_WINDOW = 1 << 16;
+
/** The first unused bit. This can be used by remotes to attach custom flags to this change. */
- public static final int FLAG_FIRST_CUSTOM = 1 << 16;
+ public static final int FLAG_FIRST_CUSTOM = 1 << 17;
/** @hide */
@IntDef(prefix = { "FLAG_" }, value = {
@@ -157,6 +160,7 @@
FLAG_CROSS_PROFILE_WORK_THUMBNAIL,
FLAG_IS_BEHIND_STARTING_WINDOW,
FLAG_IS_OCCLUDED,
+ FLAG_IS_SYSTEM_WINDOW,
FLAG_FIRST_CUSTOM
})
public @interface ChangeFlags {}
@@ -369,6 +373,9 @@
if ((flags & FLAG_IS_OCCLUDED) != 0) {
sb.append(sb.length() == 0 ? "" : "|").append("IS_OCCLUDED");
}
+ if ((flags & FLAG_IS_SYSTEM_WINDOW) != 0) {
+ sb.append(sb.length() == 0 ? "" : "|").append("FLAG_IS_SYSTEM_WINDOW");
+ }
if ((flags & FLAG_FIRST_CUSTOM) != 0) {
sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM");
}
@@ -407,6 +414,7 @@
public static final class Change implements Parcelable {
private final WindowContainerToken mContainer;
private WindowContainerToken mParent;
+ private WindowContainerToken mLastParent;
private final SurfaceControl mLeash;
private @TransitionMode int mMode = TRANSIT_NONE;
private @ChangeFlags int mFlags = FLAG_NONE;
@@ -435,6 +443,7 @@
private Change(Parcel in) {
mContainer = in.readTypedObject(WindowContainerToken.CREATOR);
mParent = in.readTypedObject(WindowContainerToken.CREATOR);
+ mLastParent = in.readTypedObject(WindowContainerToken.CREATOR);
mLeash = new SurfaceControl();
mLeash.readFromParcel(in);
mMode = in.readInt();
@@ -458,6 +467,14 @@
mParent = parent;
}
+ /**
+ * Sets the parent of this change's container before the transition if this change's
+ * container is reparented in the transition.
+ */
+ public void setLastParent(@Nullable WindowContainerToken lastParent) {
+ mLastParent = lastParent;
+ }
+
/** Sets the transition mode for this change */
public void setMode(@TransitionMode int mode) {
mMode = mode;
@@ -541,6 +558,17 @@
return mParent;
}
+ /**
+ * @return the parent of the changing container before the transition if it is reparented
+ * in the transition. The parent window may not be collected in the transition as a
+ * participant, and it may have been detached from the display. {@code null} if the changing
+ * container has not been reparented in the transition, or if the parent is not organizable.
+ */
+ @Nullable
+ public WindowContainerToken getLastParent() {
+ return mLastParent;
+ }
+
/** @return which action this change represents. */
public @TransitionMode int getMode() {
return mMode;
@@ -640,6 +668,7 @@
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeTypedObject(mContainer, flags);
dest.writeTypedObject(mParent, flags);
+ dest.writeTypedObject(mLastParent, flags);
mLeash.writeToParcel(dest, flags);
dest.writeInt(mMode);
dest.writeInt(mFlags);
@@ -679,13 +708,37 @@
@Override
public String toString() {
- String out = "{" + mContainer + "(" + mParent + ") leash=" + mLeash
- + " m=" + modeToString(mMode) + " f=" + flagsToString(mFlags) + " sb="
- + mStartAbsBounds + " eb=" + mEndAbsBounds + " eo=" + mEndRelOffset + " r="
- + mStartRotation + "->" + mEndRotation + ":" + mRotationAnimation
- + " endFixedRotation=" + mEndFixedRotation;
- if (mSnapshot != null) out += " snapshot=" + mSnapshot;
- return out + "}";
+ final StringBuilder sb = new StringBuilder();
+ sb.append('{'); sb.append(mContainer);
+ sb.append(" m="); sb.append(modeToString(mMode));
+ sb.append(" f="); sb.append(flagsToString(mFlags));
+ if (mParent != null) {
+ sb.append(" p="); sb.append(mParent);
+ }
+ if (mLeash != null) {
+ sb.append(" leash="); sb.append(mLeash);
+ }
+ sb.append(" sb="); sb.append(mStartAbsBounds);
+ sb.append(" eb="); sb.append(mEndAbsBounds);
+ if (mEndRelOffset.x != 0 || mEndRelOffset.y != 0) {
+ sb.append(" eo="); sb.append(mEndRelOffset);
+ }
+ if (mStartRotation != mEndRotation) {
+ sb.append(" r="); sb.append(mStartRotation);
+ sb.append("->"); sb.append(mEndRotation);
+ sb.append(':'); sb.append(mRotationAnimation);
+ }
+ if (mEndFixedRotation != ROTATION_UNDEFINED) {
+ sb.append(" endFixedRotation="); sb.append(mEndFixedRotation);
+ }
+ if (mSnapshot != null) {
+ sb.append(" snapshot="); sb.append(mSnapshot);
+ }
+ if (mLastParent != null) {
+ sb.append(" lastParent="); sb.append(mLastParent);
+ }
+ sb.append('}');
+ return sb.toString();
}
}
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/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index 4f74ca7..2ae2c09 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -43,6 +43,7 @@
import android.widget.TextView;
import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
import com.android.internal.app.chooser.ChooserTargetInfo;
import com.android.internal.app.chooser.DisplayResolveInfo;
@@ -86,6 +87,7 @@
private final ChooserActivityLogger mChooserActivityLogger;
private int mNumShortcutResults = 0;
+ private final Map<SelectableTargetInfo, LoadDirectShareIconTask> mIconLoaders = new HashMap<>();
private boolean mApplySharingAppLimits;
// Reserve spots for incoming direct share targets by adding placeholders
@@ -239,7 +241,6 @@
mListViewDataChanged = false;
}
-
private void createPlaceHolders() {
mNumShortcutResults = 0;
mServiceTargets.clear();
@@ -268,12 +269,16 @@
holder.bindIcon(info);
if (info instanceof SelectableTargetInfo) {
// direct share targets should append the application name for a better readout
- DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
+ SelectableTargetInfo sti = (SelectableTargetInfo) info;
+ DisplayResolveInfo rInfo = sti.getDisplayResolveInfo();
CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
CharSequence extendedInfo = info.getExtendedInfo();
String contentDescription = String.join(" ", info.getDisplayLabel(),
extendedInfo != null ? extendedInfo : "", appName);
holder.updateContentDescription(contentDescription);
+ if (!sti.hasDisplayIcon()) {
+ loadDirectShareIcon(sti);
+ }
} else if (info instanceof DisplayResolveInfo) {
DisplayResolveInfo dri = (DisplayResolveInfo) info;
if (!dri.hasDisplayIcon()) {
@@ -318,6 +323,20 @@
}
}
+ private void loadDirectShareIcon(SelectableTargetInfo info) {
+ LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
+ if (task == null) {
+ task = createLoadDirectShareIconTask(info);
+ mIconLoaders.put(info, task);
+ task.loadIcon();
+ }
+ }
+
+ @VisibleForTesting
+ protected LoadDirectShareIconTask createLoadDirectShareIconTask(SelectableTargetInfo info) {
+ return new LoadDirectShareIconTask(info);
+ }
+
void updateAlphabeticalList() {
new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
@Override
@@ -332,7 +351,7 @@
Map<String, DisplayResolveInfo> consolidated = new HashMap<>();
for (DisplayResolveInfo info : allTargets) {
String resolvedTarget = info.getResolvedComponentName().getPackageName()
- + '#' + info.getDisplayLabel();
+ + '#' + info.getDisplayLabel();
DisplayResolveInfo multiDri = consolidated.get(resolvedTarget);
if (multiDri == null) {
consolidated.put(resolvedTarget, info);
@@ -341,7 +360,7 @@
} else {
// create consolidated target from the single DisplayResolveInfo
MultiDisplayResolveInfo multiDisplayResolveInfo =
- new MultiDisplayResolveInfo(resolvedTarget, multiDri);
+ new MultiDisplayResolveInfo(resolvedTarget, multiDri);
multiDisplayResolveInfo.addTarget(info);
consolidated.put(resolvedTarget, multiDisplayResolveInfo);
}
@@ -731,7 +750,8 @@
* Necessary methods to communicate between {@link ChooserListAdapter}
* and {@link ChooserActivity}.
*/
- interface ChooserListCommunicator extends ResolverListCommunicator {
+ @VisibleForTesting
+ public interface ChooserListCommunicator extends ResolverListCommunicator {
int getMaxRankedTargets();
@@ -739,4 +759,35 @@
boolean isSendAction(Intent targetIntent);
}
+
+ /**
+ * Loads direct share targets icons.
+ */
+ @VisibleForTesting
+ public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Boolean> {
+ private final SelectableTargetInfo mTargetInfo;
+
+ private LoadDirectShareIconTask(SelectableTargetInfo targetInfo) {
+ mTargetInfo = targetInfo;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ return mTargetInfo.loadIcon();
+ }
+
+ @Override
+ protected void onPostExecute(Boolean isLoaded) {
+ if (isLoaded) {
+ notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * An alias for execute to use with unit tests.
+ */
+ public void loadIcon() {
+ execute();
+ }
+ }
}
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 f6075b0..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);
}
@@ -870,7 +871,12 @@
void onHandlePackagesChanged(ResolverListAdapter listAdapter);
}
- static class ViewHolder {
+ /**
+ * A view holder keeps a reference to a list view and provides functionality for managing its
+ * state.
+ */
+ @VisibleForTesting
+ public static class ViewHolder {
public View itemView;
public Drawable defaultItemViewBackground;
@@ -878,7 +884,8 @@
public TextView text2;
public ImageView icon;
- ViewHolder(View view) {
+ @VisibleForTesting
+ public ViewHolder(View view) {
itemView = view;
defaultItemViewBackground = view.getBackground();
text = (TextView) view.findViewById(com.android.internal.R.id.text1);
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/app/chooser/SelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
index 4b9b7cb..d7f3a76 100644
--- a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
+++ b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
@@ -37,6 +37,7 @@
import android.text.SpannableStringBuilder;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.ChooserActivity;
import com.android.internal.app.ResolverActivity;
import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter;
@@ -59,8 +60,11 @@
private final String mDisplayLabel;
private final PackageManager mPm;
private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator;
+ @GuardedBy("this")
+ private ShortcutInfo mShortcutInfo;
private Drawable mBadgeIcon = null;
private CharSequence mBadgeContentDescription;
+ @GuardedBy("this")
private Drawable mDisplayIcon;
private final Intent mFillInIntent;
private final int mFillInFlags;
@@ -78,6 +82,7 @@
mModifiedScore = modifiedScore;
mPm = mContext.getPackageManager();
mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator;
+ mShortcutInfo = shortcutInfo;
mIsPinned = shortcutInfo != null && shortcutInfo.isPinned();
if (sourceInfo != null) {
final ResolveInfo ri = sourceInfo.getResolveInfo();
@@ -92,8 +97,6 @@
}
}
}
- // TODO(b/121287224): do this in the background thread, and only for selected targets
- mDisplayIcon = getChooserTargetIconDrawable(chooserTarget, shortcutInfo);
if (sourceInfo != null) {
mBackupResolveInfo = null;
@@ -118,7 +121,10 @@
mChooserTarget = other.mChooserTarget;
mBadgeIcon = other.mBadgeIcon;
mBadgeContentDescription = other.mBadgeContentDescription;
- mDisplayIcon = other.mDisplayIcon;
+ synchronized (other) {
+ mShortcutInfo = other.mShortcutInfo;
+ mDisplayIcon = other.mDisplayIcon;
+ }
mFillInIntent = fillInIntent;
mFillInFlags = flags;
mModifiedScore = other.mModifiedScore;
@@ -141,6 +147,27 @@
return mSourceInfo;
}
+ /**
+ * Load display icon, if needed.
+ */
+ public boolean loadIcon() {
+ ShortcutInfo shortcutInfo;
+ Drawable icon;
+ synchronized (this) {
+ shortcutInfo = mShortcutInfo;
+ icon = mDisplayIcon;
+ }
+ boolean shouldLoadIcon = icon == null && shortcutInfo != null;
+ if (shouldLoadIcon) {
+ icon = getChooserTargetIconDrawable(mChooserTarget, shortcutInfo);
+ synchronized (this) {
+ mDisplayIcon = icon;
+ mShortcutInfo = null;
+ }
+ }
+ return shouldLoadIcon;
+ }
+
private Drawable getChooserTargetIconDrawable(ChooserTarget target,
@Nullable ShortcutInfo shortcutInfo) {
Drawable directShareIcon = null;
@@ -271,10 +298,17 @@
}
@Override
- public Drawable getDisplayIcon(Context context) {
+ public synchronized Drawable getDisplayIcon(Context context) {
return mDisplayIcon;
}
+ /**
+ * @return true if display icon is available
+ */
+ public synchronized boolean hasDisplayIcon() {
+ return mDisplayIcon != null;
+ }
+
public ChooserTarget getChooserTarget() {
return mChooserTarget;
}
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/dynamicanimation/animation/DynamicAnimation.java b/core/java/com/android/internal/dynamicanimation/animation/DynamicAnimation.java
new file mode 100644
index 0000000..d4fe7c8d
--- /dev/null
+++ b/core/java/com/android/internal/dynamicanimation/animation/DynamicAnimation.java
@@ -0,0 +1,815 @@
+/*
+ * 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.dynamicanimation.animation;
+
+import android.animation.AnimationHandler;
+import android.animation.ValueAnimator;
+import android.annotation.FloatRange;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.os.Looper;
+import android.util.AndroidRuntimeException;
+import android.util.FloatProperty;
+import android.view.View;
+
+import java.util.ArrayList;
+
+/**
+ * This class is the base class of physics-based animations. It manages the animation's
+ * lifecycle such as {@link #start()} and {@link #cancel()}. This base class also handles the common
+ * setup for all the subclass animations. For example, DynamicAnimation supports adding
+ * {@link OnAnimationEndListener} and {@link OnAnimationUpdateListener} so that the important
+ * animation events can be observed through the callbacks. The start conditions for any subclass of
+ * DynamicAnimation can be set using {@link #setStartValue(float)} and
+ * {@link #setStartVelocity(float)}.
+ *
+ * @param <T> subclass of DynamicAnimation
+ */
+public abstract class DynamicAnimation<T extends DynamicAnimation<T>>
+ implements AnimationHandler.AnimationFrameCallback {
+
+ /**
+ * ViewProperty holds the access of a property of a {@link View}. When an animation is
+ * created with a {@link ViewProperty} instance, the corresponding property value of the view
+ * will be updated through this ViewProperty instance.
+ */
+ public abstract static class ViewProperty extends FloatProperty<View> {
+ private ViewProperty(String name) {
+ super(name);
+ }
+ }
+
+ /**
+ * View's translationX property.
+ */
+ public static final ViewProperty TRANSLATION_X = new ViewProperty("translationX") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setTranslationX(value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return view.getTranslationX();
+ }
+ };
+
+ /**
+ * View's translationY property.
+ */
+ public static final ViewProperty TRANSLATION_Y = new ViewProperty("translationY") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setTranslationY(value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return view.getTranslationY();
+ }
+ };
+
+ /**
+ * View's translationZ property.
+ */
+ public static final ViewProperty TRANSLATION_Z = new ViewProperty("translationZ") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setTranslationZ(value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return view.getTranslationZ();
+ }
+ };
+
+ /**
+ * View's scaleX property.
+ */
+ public static final ViewProperty SCALE_X = new ViewProperty("scaleX") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setScaleX(value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return view.getScaleX();
+ }
+ };
+
+ /**
+ * View's scaleY property.
+ */
+ public static final ViewProperty SCALE_Y = new ViewProperty("scaleY") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setScaleY(value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return view.getScaleY();
+ }
+ };
+
+ /**
+ * View's rotation property.
+ */
+ public static final ViewProperty ROTATION = new ViewProperty("rotation") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setRotation(value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return view.getRotation();
+ }
+ };
+
+ /**
+ * View's rotationX property.
+ */
+ public static final ViewProperty ROTATION_X = new ViewProperty("rotationX") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setRotationX(value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return view.getRotationX();
+ }
+ };
+
+ /**
+ * View's rotationY property.
+ */
+ public static final ViewProperty ROTATION_Y = new ViewProperty("rotationY") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setRotationY(value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return view.getRotationY();
+ }
+ };
+
+ /**
+ * View's x property.
+ */
+ public static final ViewProperty X = new ViewProperty("x") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setX(value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return view.getX();
+ }
+ };
+
+ /**
+ * View's y property.
+ */
+ public static final ViewProperty Y = new ViewProperty("y") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setY(value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return view.getY();
+ }
+ };
+
+ /**
+ * View's z property.
+ */
+ public static final ViewProperty Z = new ViewProperty("z") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setZ(value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return view.getZ();
+ }
+ };
+
+ /**
+ * View's alpha property.
+ */
+ public static final ViewProperty ALPHA = new ViewProperty("alpha") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setAlpha(value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return view.getAlpha();
+ }
+ };
+
+ // Properties below are not RenderThread compatible
+ /**
+ * View's scrollX property.
+ */
+ public static final ViewProperty SCROLL_X = new ViewProperty("scrollX") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setScrollX((int) value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return (float) view.getScrollX();
+ }
+ };
+
+ /**
+ * View's scrollY property.
+ */
+ public static final ViewProperty SCROLL_Y = new ViewProperty("scrollY") {
+ @Override
+ public void setValue(View view, float value) {
+ view.setScrollY((int) value);
+ }
+
+ @Override
+ public Float get(View view) {
+ return (float) view.getScrollY();
+ }
+ };
+
+ /**
+ * The minimum visible change in pixels that can be visible to users.
+ */
+ @SuppressLint("MinMaxConstant")
+ public static final float MIN_VISIBLE_CHANGE_PIXELS = 1f;
+ /**
+ * The minimum visible change in degrees that can be visible to users.
+ */
+ @SuppressLint("MinMaxConstant")
+ public static final float MIN_VISIBLE_CHANGE_ROTATION_DEGREES = 1f / 10f;
+ /**
+ * The minimum visible change in alpha that can be visible to users.
+ */
+ @SuppressLint("MinMaxConstant")
+ public static final float MIN_VISIBLE_CHANGE_ALPHA = 1f / 256f;
+ /**
+ * The minimum visible change in scale that can be visible to users.
+ */
+ @SuppressLint("MinMaxConstant")
+ public static final float MIN_VISIBLE_CHANGE_SCALE = 1f / 500f;
+
+ // Use the max value of float to indicate an unset state.
+ private static final float UNSET = Float.MAX_VALUE;
+
+ // Multiplier to the min visible change value for value threshold
+ private static final float THRESHOLD_MULTIPLIER = 0.75f;
+
+ // Internal tracking for velocity.
+ float mVelocity = 0;
+
+ // Internal tracking for value.
+ float mValue = UNSET;
+
+ // Tracks whether start value is set. If not, the animation will obtain the value at the time
+ // of starting through the getter and use that as the starting value of the animation.
+ boolean mStartValueIsSet = false;
+
+ // Target to be animated.
+ final Object mTarget;
+
+ // View property id.
+ final FloatProperty mProperty;
+
+ // Package private tracking of animation lifecycle state. Visible to subclass animations.
+ boolean mRunning = false;
+
+ // Min and max values that defines the range of the animation values.
+ float mMaxValue = Float.MAX_VALUE;
+ float mMinValue = -mMaxValue;
+
+ // Last frame time. Always gets reset to -1 at the end of the animation.
+ private long mLastFrameTime = 0;
+
+ private float mMinVisibleChange;
+
+ // List of end listeners
+ private final ArrayList<OnAnimationEndListener> mEndListeners = new ArrayList<>();
+
+ // List of update listeners
+ private final ArrayList<OnAnimationUpdateListener> mUpdateListeners = new ArrayList<>();
+
+ // Animation handler used to schedule updates for this animation.
+ private AnimationHandler mAnimationHandler;
+
+ // Internal state for value/velocity pair.
+ static class MassState {
+ float mValue;
+ float mVelocity;
+ }
+
+ /**
+ * Creates a dynamic animation with the given FloatValueHolder instance.
+ *
+ * @param floatValueHolder the FloatValueHolder instance to be animated.
+ */
+ DynamicAnimation(final FloatValueHolder floatValueHolder) {
+ mTarget = null;
+ mProperty = new FloatProperty("FloatValueHolder") {
+ @Override
+ public Float get(Object object) {
+ return floatValueHolder.getValue();
+ }
+
+ @Override
+ public void setValue(Object object, float value) {
+ floatValueHolder.setValue(value);
+ }
+ };
+ mMinVisibleChange = MIN_VISIBLE_CHANGE_PIXELS;
+ }
+
+ /**
+ * Creates a dynamic animation to animate the given property for the given {@link View}
+ *
+ * @param object the Object whose property is to be animated
+ * @param property the property to be animated
+ */
+
+ <K> DynamicAnimation(K object, FloatProperty<K> property) {
+ mTarget = object;
+ mProperty = property;
+ if (mProperty == ROTATION || mProperty == ROTATION_X
+ || mProperty == ROTATION_Y) {
+ mMinVisibleChange = MIN_VISIBLE_CHANGE_ROTATION_DEGREES;
+ } else if (mProperty == ALPHA) {
+ mMinVisibleChange = MIN_VISIBLE_CHANGE_ALPHA;
+ } else if (mProperty == SCALE_X || mProperty == SCALE_Y) {
+ mMinVisibleChange = MIN_VISIBLE_CHANGE_SCALE;
+ } else {
+ mMinVisibleChange = MIN_VISIBLE_CHANGE_PIXELS;
+ }
+ }
+
+ /**
+ * Sets the start value of the animation. If start value is not set, the animation will get
+ * the current value for the view's property, and use that as the start value.
+ *
+ * @param startValue start value for the animation
+ * @return the Animation whose start value is being set
+ */
+ @SuppressWarnings("unchecked")
+ public T setStartValue(float startValue) {
+ mValue = startValue;
+ mStartValueIsSet = true;
+ return (T) this;
+ }
+
+ /**
+ * Start velocity of the animation. Default velocity is 0. Unit: change in property per
+ * second (e.g. pixels per second, scale/alpha value change per second).
+ *
+ * <p>Note when using a fixed value as the start velocity (as opposed to getting the velocity
+ * through touch events), it is recommended to define such a value in dp/second and convert it
+ * to pixel/second based on the density of the screen to achieve a consistent look across
+ * different screens.
+ *
+ * <p>To convert from dp/second to pixel/second:
+ * <pre class="prettyprint">
+ * float pixelPerSecond = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond,
+ * getResources().getDisplayMetrics());
+ * </pre>
+ *
+ * @param startVelocity start velocity of the animation
+ * @return the Animation whose start velocity is being set
+ */
+ @SuppressWarnings("unchecked")
+ public T setStartVelocity(float startVelocity) {
+ mVelocity = startVelocity;
+ return (T) this;
+ }
+
+ /**
+ * Sets the max value of the animation. Animations will not animate beyond their max value.
+ * Whether or not animation will come to an end when max value is reached is dependent on the
+ * child animation's implementation.
+ *
+ * @param max maximum value of the property to be animated
+ * @return the Animation whose max value is being set
+ */
+ @SuppressWarnings("unchecked")
+ public T setMaxValue(float max) {
+ // This max value should be checked and handled in the subclass animations, instead of
+ // assuming the end of the animations when the max/min value is hit in the base class.
+ // The reason is that hitting max/min value may just be a transient state, such as during
+ // the spring oscillation.
+ mMaxValue = max;
+ return (T) this;
+ }
+
+ /**
+ * Sets the min value of the animation. Animations will not animate beyond their min value.
+ * Whether or not animation will come to an end when min value is reached is dependent on the
+ * child animation's implementation.
+ *
+ * @param min minimum value of the property to be animated
+ * @return the Animation whose min value is being set
+ */
+ @SuppressWarnings("unchecked")
+ public T setMinValue(float min) {
+ mMinValue = min;
+ return (T) this;
+ }
+
+ /**
+ * Adds an end listener to the animation for receiving onAnimationEnd callbacks. If the listener
+ * is {@code null} or has already been added to the list of listeners for the animation, no op.
+ *
+ * @param listener the listener to be added
+ * @return the animation to which the listener is added
+ */
+ @SuppressWarnings("unchecked")
+ public T addEndListener(OnAnimationEndListener listener) {
+ if (!mEndListeners.contains(listener)) {
+ mEndListeners.add(listener);
+ }
+ return (T) this;
+ }
+
+ /**
+ * Removes the end listener from the animation, so as to stop receiving animation end callbacks.
+ *
+ * @param listener the listener to be removed
+ */
+ public void removeEndListener(OnAnimationEndListener listener) {
+ removeEntry(mEndListeners, listener);
+ }
+
+ /**
+ * Adds an update listener to the animation for receiving per-frame animation update callbacks.
+ * If the listener is {@code null} or has already been added to the list of listeners for the
+ * animation, no op.
+ *
+ * <p>Note that update listener should only be added before the start of the animation.
+ *
+ * @param listener the listener to be added
+ * @return the animation to which the listener is added
+ * @throws UnsupportedOperationException if the update listener is added after the animation has
+ * started
+ */
+ @SuppressWarnings("unchecked")
+ public T addUpdateListener(OnAnimationUpdateListener listener) {
+ if (isRunning()) {
+ // Require update listener to be added before the animation, such as when we start
+ // the animation, we know whether the animation is RenderThread compatible.
+ throw new UnsupportedOperationException("Error: Update listeners must be added before"
+ + "the animation.");
+ }
+ if (!mUpdateListeners.contains(listener)) {
+ mUpdateListeners.add(listener);
+ }
+ return (T) this;
+ }
+
+ /**
+ * Removes the update listener from the animation, so as to stop receiving animation update
+ * callbacks.
+ *
+ * @param listener the listener to be removed
+ */
+ public void removeUpdateListener(OnAnimationUpdateListener listener) {
+ removeEntry(mUpdateListeners, listener);
+ }
+
+
+ /**
+ * This method sets the minimal change of animation value that is visible to users, which helps
+ * determine a reasonable threshold for the animation's termination condition. It is critical
+ * to set the minimal visible change for custom properties (i.e. non-<code>ViewProperty</code>s)
+ * unless the custom property is in pixels.
+ *
+ * <p>For custom properties, this minimum visible change defaults to change in pixel
+ * (i.e. {@link #MIN_VISIBLE_CHANGE_PIXELS}. It is recommended to adjust this value that is
+ * reasonable for the property to be animated. A general rule of thumb to calculate such a value
+ * is: minimum visible change = range of custom property value / corresponding pixel range. For
+ * example, if the property to be animated is a progress (from 0 to 100) that corresponds to a
+ * 200-pixel change. Then the min visible change should be 100 / 200. (i.e. 0.5).
+ *
+ * <p>It's not necessary to call this method when animating {@link ViewProperty}s, as the
+ * minimum visible change will be derived from the property. For example, if the property to be
+ * animated is in pixels (i.e. {@link #TRANSLATION_X}, {@link #TRANSLATION_Y},
+ * {@link #TRANSLATION_Z}, @{@link #SCROLL_X} or {@link #SCROLL_Y}), the default minimum visible
+ * change is 1 (pixel). For {@link #ROTATION}, {@link #ROTATION_X} or {@link #ROTATION_Y}, the
+ * animation will use {@link #MIN_VISIBLE_CHANGE_ROTATION_DEGREES} as the min visible change,
+ * which is 1/10. Similarly, the minimum visible change for alpha (
+ * i.e. {@link #MIN_VISIBLE_CHANGE_ALPHA} is defined as 1 / 256.
+ *
+ * @param minimumVisibleChange minimum change in property value that is visible to users
+ * @return the animation whose min visible change is being set
+ * @throws IllegalArgumentException if the given threshold is not positive
+ */
+ @SuppressWarnings("unchecked")
+ public T setMinimumVisibleChange(@FloatRange(from = 0.0, fromInclusive = false)
+ float minimumVisibleChange) {
+ if (minimumVisibleChange <= 0) {
+ throw new IllegalArgumentException("Minimum visible change must be positive.");
+ }
+ mMinVisibleChange = minimumVisibleChange;
+ setValueThreshold(minimumVisibleChange * THRESHOLD_MULTIPLIER);
+ return (T) this;
+ }
+
+ /**
+ * Returns the minimum change in the animation property that could be visibly different to
+ * users.
+ *
+ * @return minimum change in property value that is visible to users
+ */
+ public float getMinimumVisibleChange() {
+ return mMinVisibleChange;
+ }
+
+ /**
+ * Remove {@code null} entries from the list.
+ */
+ private static <T> void removeNullEntries(ArrayList<T> list) {
+ // Clean up null entries
+ for (int i = list.size() - 1; i >= 0; i--) {
+ if (list.get(i) == null) {
+ list.remove(i);
+ }
+ }
+ }
+
+ /**
+ * Remove an entry from the list by marking it {@code null} and clean up later.
+ */
+ private static <T> void removeEntry(ArrayList<T> list, T entry) {
+ int id = list.indexOf(entry);
+ if (id >= 0) {
+ list.set(id, null);
+ }
+ }
+
+ /****************Animation Lifecycle Management***************/
+
+ /**
+ * Starts an animation. If the animation has already been started, no op. Note that calling
+ * {@link #start()} will not immediately set the property value to start value of the animation.
+ * The property values will be changed at each animation pulse, which happens before the draw
+ * pass. As a result, the changes will be reflected in the next frame, the same as if the values
+ * were set immediately. This method should only be called on main thread.
+ *
+ * Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler
+ * is created on the same thread as the first call to start/cancel an animation. All the
+ * subsequent animation lifecycle manipulations need to be on that same thread, until the
+ * AnimationHandler is reset (using [setAnimationHandler]).
+ *
+ * @throws AndroidRuntimeException if this method is not called on the same thread as the
+ * animation handler
+ */
+ @MainThread
+ public void start() {
+ if (!isCurrentThread()) {
+ throw new AndroidRuntimeException("Animations may only be started on the same thread "
+ + "as the animation handler");
+ }
+ if (!mRunning) {
+ startAnimationInternal();
+ }
+ }
+
+ boolean isCurrentThread() {
+ return Thread.currentThread() == Looper.myLooper().getThread();
+ }
+
+ /**
+ * Cancels the on-going animation. If the animation hasn't started, no op.
+ *
+ * Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler
+ * is created on the same thread as the first call to start/cancel an animation. All the
+ * subsequent animation lifecycle manipulations need to be on that same thread, until the
+ * AnimationHandler is reset (using [setAnimationHandler]).
+ *
+ * @throws AndroidRuntimeException if this method is not called on the same thread as the
+ * animation handler
+ */
+ @MainThread
+ public void cancel() {
+ if (!isCurrentThread()) {
+ throw new AndroidRuntimeException("Animations may only be canceled from the same "
+ + "thread as the animation handler");
+ }
+ if (mRunning) {
+ endAnimationInternal(true);
+ }
+ }
+
+ /**
+ * Returns whether the animation is currently running.
+ *
+ * @return {@code true} if the animation is currently running, {@code false} otherwise
+ */
+ public boolean isRunning() {
+ return mRunning;
+ }
+
+ /************************** Private APIs below ********************************/
+
+ // This gets called when the animation is started, to finish the setup of the animation
+ // before the animation pulsing starts.
+ private void startAnimationInternal() {
+ if (!mRunning) {
+ mRunning = true;
+ if (!mStartValueIsSet) {
+ mValue = getPropertyValue();
+ }
+ // Sanity check:
+ if (mValue > mMaxValue || mValue < mMinValue) {
+ throw new IllegalArgumentException("Starting value need to be in between min"
+ + " value and max value");
+ }
+ getAnimationHandler().addAnimationFrameCallback(this, 0);
+ }
+ }
+
+ /**
+ * This gets call on each frame of the animation. Animation value and velocity are updated
+ * in this method based on the new frame time. The property value of the view being animated
+ * is then updated. The animation's ending conditions are also checked in this method. Once
+ * the animation reaches equilibrium, the animation will come to its end, and end listeners
+ * will be notified, if any.
+ */
+ @Override
+ public boolean doAnimationFrame(long frameTime) {
+ if (mLastFrameTime == 0) {
+ // First frame.
+ mLastFrameTime = frameTime;
+ setPropertyValue(mValue);
+ return false;
+ }
+ long deltaT = frameTime - mLastFrameTime;
+ mLastFrameTime = frameTime;
+ float durationScale = ValueAnimator.getDurationScale();
+ deltaT = durationScale == 0.0f ? Integer.MAX_VALUE : (long) (deltaT / durationScale);
+ boolean finished = updateValueAndVelocity(deltaT);
+ // Clamp value & velocity.
+ mValue = Math.min(mValue, mMaxValue);
+ mValue = Math.max(mValue, mMinValue);
+
+ setPropertyValue(mValue);
+
+ if (finished) {
+ endAnimationInternal(false);
+ }
+ return finished;
+ }
+
+ @Override
+ public void commitAnimationFrame(long frameTime) {
+ doAnimationFrame(frameTime);
+ }
+
+ /**
+ * Updates the animation state (i.e. value and velocity). This method is package private, so
+ * subclasses can override this method to calculate the new value and velocity in their custom
+ * way.
+ *
+ * @param deltaT time elapsed in millisecond since last frame
+ * @return whether the animation has finished
+ */
+ abstract boolean updateValueAndVelocity(long deltaT);
+
+ /**
+ * Internal method to reset the animation states when animation is finished/canceled.
+ */
+ private void endAnimationInternal(boolean canceled) {
+ mRunning = false;
+ getAnimationHandler().removeCallback(this);
+ mLastFrameTime = 0;
+ mStartValueIsSet = false;
+ for (int i = 0; i < mEndListeners.size(); i++) {
+ if (mEndListeners.get(i) != null) {
+ mEndListeners.get(i).onAnimationEnd(this, canceled, mValue, mVelocity);
+ }
+ }
+ removeNullEntries(mEndListeners);
+ }
+
+ /**
+ * Updates the property value through the corresponding setter.
+ */
+ @SuppressWarnings("unchecked")
+ void setPropertyValue(float value) {
+ mProperty.setValue(mTarget, value);
+ for (int i = 0; i < mUpdateListeners.size(); i++) {
+ if (mUpdateListeners.get(i) != null) {
+ mUpdateListeners.get(i).onAnimationUpdate(this, mValue, mVelocity);
+ }
+ }
+ removeNullEntries(mUpdateListeners);
+ }
+
+ /**
+ * Returns the default threshold.
+ */
+ float getValueThreshold() {
+ return mMinVisibleChange * THRESHOLD_MULTIPLIER;
+ }
+
+ /**
+ * Obtain the property value through the corresponding getter.
+ */
+ @SuppressWarnings("unchecked")
+ private float getPropertyValue() {
+ return (Float) mProperty.get(mTarget);
+ }
+
+ /**
+ * Returns the {@link AnimationHandler} used to schedule updates for this animator.
+ *
+ * @return the {@link AnimationHandler} for this animator.
+ */
+ @NonNull
+ public AnimationHandler getAnimationHandler() {
+ return mAnimationHandler != null ? mAnimationHandler : AnimationHandler.getInstance();
+ }
+
+ /****************Sub class animations**************/
+ /**
+ * Returns the acceleration at the given value with the given velocity.
+ **/
+ abstract float getAcceleration(float value, float velocity);
+
+ /**
+ * Returns whether the animation has reached equilibrium.
+ */
+ abstract boolean isAtEquilibrium(float value, float velocity);
+
+ /**
+ * Updates the default value threshold for the animation based on the property to be animated.
+ */
+ abstract void setValueThreshold(float threshold);
+
+ /**
+ * An animation listener that receives end notifications from an animation.
+ */
+ public interface OnAnimationEndListener {
+ /**
+ * Notifies the end of an animation. Note that this callback will be invoked not only when
+ * an animation reach equilibrium, but also when the animation is canceled.
+ *
+ * @param animation animation that has ended or was canceled
+ * @param canceled whether the animation has been canceled
+ * @param value the final value when the animation stopped
+ * @param velocity the final velocity when the animation stopped
+ */
+ void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
+ float velocity);
+ }
+
+ /**
+ * Implementors of this interface can add themselves as update listeners
+ * to an <code>DynamicAnimation</code> instance to receive callbacks on every animation
+ * frame, after the current frame's values have been calculated for that
+ * <code>DynamicAnimation</code>.
+ */
+ public interface OnAnimationUpdateListener {
+
+ /**
+ * Notifies the occurrence of another frame of the animation.
+ *
+ * @param animation animation that the update listener is added to
+ * @param value the current value of the animation
+ * @param velocity the current velocity of the animation
+ */
+ void onAnimationUpdate(DynamicAnimation animation, float value, float velocity);
+ }
+}
diff --git a/core/java/com/android/internal/dynamicanimation/animation/FloatValueHolder.java b/core/java/com/android/internal/dynamicanimation/animation/FloatValueHolder.java
new file mode 100644
index 0000000..c3a2cac
--- /dev/null
+++ b/core/java/com/android/internal/dynamicanimation/animation/FloatValueHolder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.dynamicanimation.animation;
+
+/**
+ * <p>FloatValueHolder holds a float value. FloatValueHolder provides a setter and a getter (
+ * i.e. {@link #setValue(float)} and {@link #getValue()}) to access this float value. Animations can
+ * be performed on a FloatValueHolder instance. During each frame of the animation, the
+ * FloatValueHolder will have its value updated via {@link #setValue(float)}. The caller can
+ * obtain the up-to-date animation value via {@link FloatValueHolder#getValue()}.
+ *
+ * @see SpringAnimation#SpringAnimation(FloatValueHolder)
+ */
+
+public class FloatValueHolder {
+ private float mValue = 0.0f;
+
+ /**
+ * Constructs a holder for a float value that is initialized to 0.
+ */
+ public FloatValueHolder() {
+ }
+
+ /**
+ * Constructs a holder for a float value that is initialized to the input value.
+ *
+ * @param value the value to initialize the value held in the FloatValueHolder
+ */
+ public FloatValueHolder(float value) {
+ setValue(value);
+ }
+
+ /**
+ * Sets the value held in the FloatValueHolder instance.
+ *
+ * @param value float value held in the FloatValueHolder instance
+ */
+ public void setValue(float value) {
+ mValue = value;
+ }
+
+ /**
+ * Returns the float value held in the FloatValueHolder instance.
+ *
+ * @return float value held in the FloatValueHolder instance
+ */
+ public float getValue() {
+ return mValue;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt b/core/java/com/android/internal/dynamicanimation/animation/Force.java
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
copy to core/java/com/android/internal/dynamicanimation/animation/Force.java
index ca667dd..fcb9c45 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
+++ b/core/java/com/android/internal/dynamicanimation/animation/Force.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,10 +14,14 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone.panelstate
+package com.android.internal.dynamicanimation.animation;
-/** A listener interface to be notified of state change events for the notification panel. */
-fun interface PanelStateListener {
- /** Called when the panel's expansion state has changed. */
- fun onPanelStateChanged(@PanelState state: Int)
+/**
+ * Hide this for now, in case we want to change the API.
+ */
+interface Force {
+ // Acceleration based on position.
+ float getAcceleration(float position, float velocity);
+
+ boolean isAtEquilibrium(float value, float velocity);
}
diff --git a/core/java/com/android/internal/dynamicanimation/animation/SpringAnimation.java b/core/java/com/android/internal/dynamicanimation/animation/SpringAnimation.java
new file mode 100644
index 0000000..2f3b72c
--- /dev/null
+++ b/core/java/com/android/internal/dynamicanimation/animation/SpringAnimation.java
@@ -0,0 +1,314 @@
+/*
+ * 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.dynamicanimation.animation;
+
+import android.util.AndroidRuntimeException;
+import android.util.FloatProperty;
+
+/**
+ * SpringAnimation is an animation that is driven by a {@link SpringForce}. The spring force defines
+ * the spring's stiffness, damping ratio, as well as the rest position. Once the SpringAnimation is
+ * started, on each frame the spring force will update the animation's value and velocity.
+ * The animation will continue to run until the spring force reaches equilibrium. If the spring used
+ * in the animation is undamped, the animation will never reach equilibrium. Instead, it will
+ * oscillate forever.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * </div>
+ *
+ * <p>To create a simple {@link SpringAnimation} that uses the default {@link SpringForce}:</p>
+ * <pre class="prettyprint">
+ * // Create an animation to animate view's X property, set the rest position of the
+ * // default spring to 0, and start the animation with a starting velocity of 5000 (pixel/s).
+ * final SpringAnimation anim = new SpringAnimation(view, DynamicAnimation.X, 0)
+ * .setStartVelocity(5000);
+ * anim.start();
+ * </pre>
+ *
+ * <p>Alternatively, a {@link SpringAnimation} can take a pre-configured {@link SpringForce}, and
+ * use that to drive the animation. </p>
+ * <pre class="prettyprint">
+ * // Create a low stiffness, low bounce spring at position 0.
+ * SpringForce spring = new SpringForce(0)
+ * .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+ * .setStiffness(SpringForce.STIFFNESS_LOW);
+ * // Create an animation to animate view's scaleY property, and start the animation using
+ * // the spring above and a starting value of 0.5. Additionally, constrain the range of value for
+ * // the animation to be non-negative, effectively preventing any spring overshoot.
+ * final SpringAnimation anim = new SpringAnimation(view, DynamicAnimation.SCALE_Y)
+ * .setMinValue(0).setSpring(spring).setStartValue(1);
+ * anim.start();
+ * </pre>
+ */
+public final class SpringAnimation extends DynamicAnimation<SpringAnimation> {
+
+ private SpringForce mSpring = null;
+ private float mPendingPosition = UNSET;
+ private static final float UNSET = Float.MAX_VALUE;
+ private boolean mEndRequested = false;
+
+ /**
+ * <p>This creates a SpringAnimation that animates a {@link FloatValueHolder} instance. During
+ * the animation, the {@link FloatValueHolder} instance will be updated via
+ * {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date
+ * animation value via {@link FloatValueHolder#getValue()}.
+ *
+ * <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via
+ * {@link FloatValueHolder#setValue(float)} outside of the animation during an
+ * animation run will not have any effect on the on-going animation.
+ *
+ * @param floatValueHolder the property to be animated
+ */
+ public SpringAnimation(FloatValueHolder floatValueHolder) {
+ super(floatValueHolder);
+ }
+
+ /**
+ * <p>This creates a SpringAnimation that animates a {@link FloatValueHolder} instance. During
+ * the animation, the {@link FloatValueHolder} instance will be updated via
+ * {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date
+ * animation value via {@link FloatValueHolder#getValue()}.
+ *
+ * A Spring will be created with the given final position and default stiffness and damping
+ * ratio. This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}.
+ *
+ * <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via
+ * {@link FloatValueHolder#setValue(float)} outside of the animation during an
+ * animation run will not have any effect on the on-going animation.
+ *
+ * @param floatValueHolder the property to be animated
+ * @param finalPosition the final position of the spring to be created.
+ */
+ public SpringAnimation(FloatValueHolder floatValueHolder, float finalPosition) {
+ super(floatValueHolder);
+ mSpring = new SpringForce(finalPosition);
+ }
+
+ /**
+ * This creates a SpringAnimation that animates the property of the given object.
+ * Note, a spring will need to setup through {@link #setSpring(SpringForce)} before
+ * the animation starts.
+ *
+ * @param object the Object whose property will be animated
+ * @param property the property to be animated
+ * @param <K> the class on which the Property is declared
+ */
+ public <K> SpringAnimation(K object, FloatProperty<K> property) {
+ super(object, property);
+ }
+
+ /**
+ * This creates a SpringAnimation that animates the property of the given object. A Spring will
+ * be created with the given final position and default stiffness and damping ratio.
+ * This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}.
+ *
+ * @param object the Object whose property will be animated
+ * @param property the property to be animated
+ * @param finalPosition the final position of the spring to be created.
+ * @param <K> the class on which the Property is declared
+ */
+ public <K> SpringAnimation(K object, FloatProperty<K> property,
+ float finalPosition) {
+ super(object, property);
+ mSpring = new SpringForce(finalPosition);
+ }
+
+ /**
+ * Returns the spring that the animation uses for animations.
+ *
+ * @return the spring that the animation uses for animations
+ */
+ public SpringForce getSpring() {
+ return mSpring;
+ }
+
+ /**
+ * Uses the given spring as the force that drives this animation. If this spring force has its
+ * parameters re-configured during the animation, the new configuration will be reflected in the
+ * animation immediately.
+ *
+ * @param force a pre-defined spring force that drives the animation
+ * @return the animation that the spring force is set on
+ */
+ public SpringAnimation setSpring(SpringForce force) {
+ mSpring = force;
+ return this;
+ }
+
+ @Override
+ public void start() {
+ sanityCheck();
+ mSpring.setValueThreshold(getValueThreshold());
+ super.start();
+ }
+
+ /**
+ * Updates the final position of the spring.
+ * <p/>
+ * When the animation is running, calling this method would assume the position change of the
+ * spring as a continuous movement since last frame, which yields more accurate results than
+ * changing the spring position directly through {@link SpringForce#setFinalPosition(float)}.
+ * <p/>
+ * If the animation hasn't started, calling this method will change the spring position, and
+ * immediately start the animation.
+ *
+ * @param finalPosition rest position of the spring
+ */
+ public void animateToFinalPosition(float finalPosition) {
+ if (isRunning()) {
+ mPendingPosition = finalPosition;
+ } else {
+ if (mSpring == null) {
+ mSpring = new SpringForce(finalPosition);
+ }
+ mSpring.setFinalPosition(finalPosition);
+ start();
+ }
+ }
+
+ /**
+ * Cancels the on-going animation. If the animation hasn't started, no op. Note that this method
+ * should only be called on main thread.
+ *
+ * @throws AndroidRuntimeException if this method is not called on the main thread
+ */
+ @Override
+ public void cancel() {
+ super.cancel();
+ if (mPendingPosition != UNSET) {
+ if (mSpring == null) {
+ mSpring = new SpringForce(mPendingPosition);
+ } else {
+ mSpring.setFinalPosition(mPendingPosition);
+ }
+ mPendingPosition = UNSET;
+ }
+ }
+
+ /**
+ * Skips to the end of the animation. If the spring is undamped, an
+ * {@link IllegalStateException} will be thrown, as the animation would never reach to an end.
+ * It is recommended to check {@link #canSkipToEnd()} before calling this method. If animation
+ * is not running, no-op.
+ *
+ * Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler
+ * is created on the same thread as the first call to start/cancel an animation. All the
+ * subsequent animation lifecycle manipulations need to be on that same thread, until the
+ * AnimationHandler is reset (using [setAnimationHandler]).
+ *
+ * @throws IllegalStateException if the spring is undamped (i.e. damping ratio = 0)
+ * @throws AndroidRuntimeException if this method is not called on the same thread as the
+ * animation handler
+ */
+ public void skipToEnd() {
+ if (!canSkipToEnd()) {
+ throw new UnsupportedOperationException("Spring animations can only come to an end"
+ + " when there is damping");
+ }
+ if (!isCurrentThread()) {
+ throw new AndroidRuntimeException("Animations may only be started on the same thread "
+ + "as the animation handler");
+ }
+ if (mRunning) {
+ mEndRequested = true;
+ }
+ }
+
+ /**
+ * Queries whether the spring can eventually come to the rest position.
+ *
+ * @return {@code true} if the spring is damped, otherwise {@code false}
+ */
+ public boolean canSkipToEnd() {
+ return mSpring.mDampingRatio > 0;
+ }
+
+ /************************ Below are private APIs *************************/
+
+ private void sanityCheck() {
+ if (mSpring == null) {
+ throw new UnsupportedOperationException("Incomplete SpringAnimation: Either final"
+ + " position or a spring force needs to be set.");
+ }
+ double finalPosition = mSpring.getFinalPosition();
+ if (finalPosition > mMaxValue) {
+ throw new UnsupportedOperationException("Final position of the spring cannot be greater"
+ + " than the max value.");
+ } else if (finalPosition < mMinValue) {
+ throw new UnsupportedOperationException("Final position of the spring cannot be less"
+ + " than the min value.");
+ }
+ }
+
+ @Override
+ boolean updateValueAndVelocity(long deltaT) {
+ // If user had requested end, then update the value and velocity to end state and consider
+ // animation done.
+ if (mEndRequested) {
+ if (mPendingPosition != UNSET) {
+ mSpring.setFinalPosition(mPendingPosition);
+ mPendingPosition = UNSET;
+ }
+ mValue = mSpring.getFinalPosition();
+ mVelocity = 0;
+ mEndRequested = false;
+ return true;
+ }
+
+ if (mPendingPosition != UNSET) {
+ // Approximate by considering half of the time spring position stayed at the old
+ // position, half of the time it's at the new position.
+ MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT / 2);
+ mSpring.setFinalPosition(mPendingPosition);
+ mPendingPosition = UNSET;
+
+ massState = mSpring.updateValues(massState.mValue, massState.mVelocity, deltaT / 2);
+ mValue = massState.mValue;
+ mVelocity = massState.mVelocity;
+
+ } else {
+ MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT);
+ mValue = massState.mValue;
+ mVelocity = massState.mVelocity;
+ }
+
+ mValue = Math.max(mValue, mMinValue);
+ mValue = Math.min(mValue, mMaxValue);
+
+ if (isAtEquilibrium(mValue, mVelocity)) {
+ mValue = mSpring.getFinalPosition();
+ mVelocity = 0f;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ float getAcceleration(float value, float velocity) {
+ return mSpring.getAcceleration(value, velocity);
+ }
+
+ @Override
+ boolean isAtEquilibrium(float value, float velocity) {
+ return mSpring.isAtEquilibrium(value, velocity);
+ }
+
+ @Override
+ void setValueThreshold(float threshold) {
+ }
+}
diff --git a/core/java/com/android/internal/dynamicanimation/animation/SpringForce.java b/core/java/com/android/internal/dynamicanimation/animation/SpringForce.java
new file mode 100644
index 0000000..36242ae2
--- /dev/null
+++ b/core/java/com/android/internal/dynamicanimation/animation/SpringForce.java
@@ -0,0 +1,323 @@
+/*
+ * 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.dynamicanimation.animation;
+
+import android.annotation.FloatRange;
+
+/**
+ * Spring Force defines the characteristics of the spring being used in the animation.
+ * <p>
+ * By configuring the stiffness and damping ratio, callers can create a spring with the look and
+ * feel suits their use case. Stiffness corresponds to the spring constant. The stiffer the spring
+ * is, the harder it is to stretch it, the faster it undergoes dampening.
+ * <p>
+ * Spring damping ratio describes how oscillations in a system decay after a disturbance.
+ * When damping ratio > 1* (i.e. over-damped), the object will quickly return to the rest position
+ * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
+ * return to equilibrium within the shortest amount of time. When damping ratio is less than 1
+ * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without any
+ * damping (i.e. damping ratio = 0), the mass will oscillate forever.
+ */
+public final class SpringForce implements Force {
+ /**
+ * Stiffness constant for extremely stiff spring.
+ */
+ public static final float STIFFNESS_HIGH = 10_000f;
+ /**
+ * Stiffness constant for medium stiff spring. This is the default stiffness for spring force.
+ */
+ public static final float STIFFNESS_MEDIUM = 1500f;
+ /**
+ * Stiffness constant for a spring with low stiffness.
+ */
+ public static final float STIFFNESS_LOW = 200f;
+ /**
+ * Stiffness constant for a spring with very low stiffness.
+ */
+ public static final float STIFFNESS_VERY_LOW = 50f;
+
+ /**
+ * Damping ratio for a very bouncy spring. Note for under-damped springs
+ * (i.e. damping ratio < 1), the lower the damping ratio, the more bouncy the spring.
+ */
+ public static final float DAMPING_RATIO_HIGH_BOUNCY = 0.2f;
+ /**
+ * Damping ratio for a medium bouncy spring. This is also the default damping ratio for spring
+ * force. Note for under-damped springs (i.e. damping ratio < 1), the lower the damping ratio,
+ * the more bouncy the spring.
+ */
+ public static final float DAMPING_RATIO_MEDIUM_BOUNCY = 0.5f;
+ /**
+ * Damping ratio for a spring with low bounciness. Note for under-damped springs
+ * (i.e. damping ratio < 1), the lower the damping ratio, the higher the bounciness.
+ */
+ public static final float DAMPING_RATIO_LOW_BOUNCY = 0.75f;
+ /**
+ * Damping ratio for a spring with no bounciness. This damping ratio will create a critically
+ * damped spring that returns to equilibrium within the shortest amount of time without
+ * oscillating.
+ */
+ public static final float DAMPING_RATIO_NO_BOUNCY = 1f;
+
+ // This multiplier is used to calculate the velocity threshold given a certain value threshold.
+ // The idea is that if it takes >= 1 frame to move the value threshold amount, then the velocity
+ // is a reasonable threshold.
+ private static final double VELOCITY_THRESHOLD_MULTIPLIER = 1000.0 / 16.0;
+
+ // Natural frequency
+ double mNaturalFreq = Math.sqrt(STIFFNESS_MEDIUM);
+ // Damping ratio.
+ double mDampingRatio = DAMPING_RATIO_MEDIUM_BOUNCY;
+
+ // Value to indicate an unset state.
+ private static final double UNSET = Double.MAX_VALUE;
+
+ // Indicates whether the spring has been initialized
+ private boolean mInitialized = false;
+
+ // Threshold for velocity and value to determine when it's reasonable to assume that the spring
+ // is approximately at rest.
+ private double mValueThreshold;
+ private double mVelocityThreshold;
+
+ // Intermediate values to simplify the spring function calculation per frame.
+ private double mGammaPlus;
+ private double mGammaMinus;
+ private double mDampedFreq;
+
+ // Final position of the spring. This must be set before the start of the animation.
+ private double mFinalPosition = UNSET;
+
+ // Internal state to hold a value/velocity pair.
+ private final DynamicAnimation.MassState mMassState = new DynamicAnimation.MassState();
+
+ /**
+ * Creates a spring force. Note that final position of the spring must be set through
+ * {@link #setFinalPosition(float)} before the spring animation starts.
+ */
+ public SpringForce() {
+ // No op.
+ }
+
+ /**
+ * Creates a spring with a given final rest position.
+ *
+ * @param finalPosition final position of the spring when it reaches equilibrium
+ */
+ public SpringForce(float finalPosition) {
+ mFinalPosition = finalPosition;
+ }
+
+ /**
+ * Sets the stiffness of a spring. The more stiff a spring is, the more force it applies to
+ * the object attached when the spring is not at the final position. Default stiffness is
+ * {@link #STIFFNESS_MEDIUM}.
+ *
+ * @param stiffness non-negative stiffness constant of a spring
+ * @return the spring force that the given stiffness is set on
+ * @throws IllegalArgumentException if the given spring stiffness is not positive
+ */
+ public SpringForce setStiffness(
+ @FloatRange(from = 0.0, fromInclusive = false) float stiffness) {
+ if (stiffness <= 0) {
+ throw new IllegalArgumentException("Spring stiffness constant must be positive.");
+ }
+ mNaturalFreq = Math.sqrt(stiffness);
+ // All the intermediate values need to be recalculated.
+ mInitialized = false;
+ return this;
+ }
+
+ /**
+ * Gets the stiffness of the spring.
+ *
+ * @return the stiffness of the spring
+ */
+ public float getStiffness() {
+ return (float) (mNaturalFreq * mNaturalFreq);
+ }
+
+ /**
+ * Spring damping ratio describes how oscillations in a system decay after a disturbance.
+ * <p>
+ * When damping ratio > 1 (over-damped), the object will quickly return to the rest position
+ * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
+ * return to equilibrium within the shortest amount of time. When damping ratio is less than 1
+ * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without
+ * any damping (i.e. damping ratio = 0), the mass will oscillate forever.
+ * <p>
+ * Default damping ratio is {@link #DAMPING_RATIO_MEDIUM_BOUNCY}.
+ *
+ * @param dampingRatio damping ratio of the spring, it should be non-negative
+ * @return the spring force that the given damping ratio is set on
+ * @throws IllegalArgumentException if the {@param dampingRatio} is negative.
+ */
+ public SpringForce setDampingRatio(@FloatRange(from = 0.0) float dampingRatio) {
+ if (dampingRatio < 0) {
+ throw new IllegalArgumentException("Damping ratio must be non-negative");
+ }
+ mDampingRatio = dampingRatio;
+ // All the intermediate values need to be recalculated.
+ mInitialized = false;
+ return this;
+ }
+
+ /**
+ * Returns the damping ratio of the spring.
+ *
+ * @return damping ratio of the spring
+ */
+ public float getDampingRatio() {
+ return (float) mDampingRatio;
+ }
+
+ /**
+ * Sets the rest position of the spring.
+ *
+ * @param finalPosition rest position of the spring
+ * @return the spring force that the given final position is set on
+ */
+ public SpringForce setFinalPosition(float finalPosition) {
+ mFinalPosition = finalPosition;
+ return this;
+ }
+
+ /**
+ * Returns the rest position of the spring.
+ *
+ * @return rest position of the spring
+ */
+ public float getFinalPosition() {
+ return (float) mFinalPosition;
+ }
+
+ /*********************** Below are private APIs *********************/
+
+ @Override
+ public float getAcceleration(float lastDisplacement, float lastVelocity) {
+
+ lastDisplacement -= getFinalPosition();
+
+ double k = mNaturalFreq * mNaturalFreq;
+ double c = 2 * mNaturalFreq * mDampingRatio;
+
+ return (float) (-k * lastDisplacement - c * lastVelocity);
+ }
+
+ @Override
+ public boolean isAtEquilibrium(float value, float velocity) {
+ if (Math.abs(velocity) < mVelocityThreshold
+ && Math.abs(value - getFinalPosition()) < mValueThreshold) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Initialize the string by doing the necessary pre-calculation as well as some sanity check
+ * on the setup.
+ *
+ * @throws IllegalStateException if the final position is not yet set by the time the spring
+ * animation has started
+ */
+ private void init() {
+ if (mInitialized) {
+ return;
+ }
+
+ if (mFinalPosition == UNSET) {
+ throw new IllegalStateException("Error: Final position of the spring must be"
+ + " set before the animation starts");
+ }
+
+ if (mDampingRatio > 1) {
+ // Over damping
+ mGammaPlus = -mDampingRatio * mNaturalFreq
+ + mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
+ mGammaMinus = -mDampingRatio * mNaturalFreq
+ - mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
+ } else if (mDampingRatio >= 0 && mDampingRatio < 1) {
+ // Under damping
+ mDampedFreq = mNaturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio);
+ }
+
+ mInitialized = true;
+ }
+
+ /**
+ * Internal only call for Spring to calculate the spring position/velocity using
+ * an analytical approach.
+ */
+ DynamicAnimation.MassState updateValues(double lastDisplacement, double lastVelocity,
+ long timeElapsed) {
+ init();
+
+ double deltaT = timeElapsed / 1000d; // unit: seconds
+ lastDisplacement -= mFinalPosition;
+ double displacement;
+ double currentVelocity;
+ if (mDampingRatio > 1) {
+ // Overdamped
+ double coeffA = lastDisplacement - (mGammaMinus * lastDisplacement - lastVelocity)
+ / (mGammaMinus - mGammaPlus);
+ double coeffB = (mGammaMinus * lastDisplacement - lastVelocity)
+ / (mGammaMinus - mGammaPlus);
+ displacement = coeffA * Math.pow(Math.E, mGammaMinus * deltaT)
+ + coeffB * Math.pow(Math.E, mGammaPlus * deltaT);
+ currentVelocity = coeffA * mGammaMinus * Math.pow(Math.E, mGammaMinus * deltaT)
+ + coeffB * mGammaPlus * Math.pow(Math.E, mGammaPlus * deltaT);
+ } else if (mDampingRatio == 1) {
+ // Critically damped
+ double coeffA = lastDisplacement;
+ double coeffB = lastVelocity + mNaturalFreq * lastDisplacement;
+ displacement = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT);
+ currentVelocity = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT)
+ * -mNaturalFreq + coeffB * Math.pow(Math.E, -mNaturalFreq * deltaT);
+ } else {
+ // Underdamped
+ double cosCoeff = lastDisplacement;
+ double sinCoeff = (1 / mDampedFreq) * (mDampingRatio * mNaturalFreq
+ * lastDisplacement + lastVelocity);
+ displacement = Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
+ * (cosCoeff * Math.cos(mDampedFreq * deltaT)
+ + sinCoeff * Math.sin(mDampedFreq * deltaT));
+ currentVelocity = displacement * -mNaturalFreq * mDampingRatio
+ + Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
+ * (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * deltaT)
+ + mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT));
+ }
+
+ mMassState.mValue = (float) (displacement + mFinalPosition);
+ mMassState.mVelocity = (float) currentVelocity;
+ return mMassState;
+ }
+
+ /**
+ * This threshold defines how close the animation value needs to be before the animation can
+ * finish. This default value is based on the property being animated, e.g. animations on alpha,
+ * scale, translation or rotation would have different thresholds. This value should be small
+ * enough to avoid visual glitch of "jumping to the end". But it shouldn't be so small that
+ * animations take seconds to finish.
+ *
+ * @param threshold the difference between the animation value and final spring position that
+ * is allowed to end the animation when velocity is very low
+ */
+ void setValueThreshold(double threshold) {
+ mValueThreshold = Math.abs(threshold);
+ mVelocityThreshold = mValueThreshold * VELOCITY_THRESHOLD_MULTIPLIER;
+ }
+}
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/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 8b96597..6fed26c 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -1326,6 +1326,13 @@
LongSamplingCounter mMobileRadioActiveUnknownTime;
LongSamplingCounter mMobileRadioActiveUnknownCount;
+ /**
+ * The soonest the Mobile Radio stats can be updated due to a mobile radio power state change
+ * after it was last updated.
+ */
+ @VisibleForTesting
+ protected static final long MOBILE_RADIO_POWER_STATE_UPDATE_FREQ_MS = 1000 * 60 * 10;
+
int mWifiRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
@GuardedBy("this")
@@ -6260,6 +6267,15 @@
} else {
mMobileRadioActiveTimer.stopRunningLocked(realElapsedRealtimeMs);
mMobileRadioActivePerAppTimer.stopRunningLocked(realElapsedRealtimeMs);
+
+ if (mLastModemActivityInfo != null) {
+ if (elapsedRealtimeMs < mLastModemActivityInfo.getTimestampMillis()
+ + MOBILE_RADIO_POWER_STATE_UPDATE_FREQ_MS) {
+ // Modem Activity info has been collected recently, don't bother
+ // triggering another update.
+ return false;
+ }
+ }
// Tell the caller to collect radio network/power stats.
return true;
}
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 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/java/com/android/internal/widget/ResolverDrawerLayout.java b/core/java/com/android/internal/widget/ResolverDrawerLayout.java
index af9c5a5..52ffc98 100644
--- a/core/java/com/android/internal/widget/ResolverDrawerLayout.java
+++ b/core/java/com/android/internal/widget/ResolverDrawerLayout.java
@@ -17,6 +17,9 @@
package com.android.internal.widget;
+import static android.content.res.Resources.ID_NULL;
+
+import android.annotation.IdRes;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
@@ -96,6 +99,8 @@
private int mTopOffset;
private boolean mShowAtTop;
+ @IdRes
+ private int mIgnoreOffsetTopLimitViewId = ID_NULL;
private boolean mIsDragging;
private boolean mOpenOnClick;
@@ -156,6 +161,10 @@
mIsMaxCollapsedHeightSmallExplicit =
a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall);
mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
+ if (a.hasValue(R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit)) {
+ mIgnoreOffsetTopLimitViewId = a.getResourceId(
+ R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL);
+ }
a.recycle();
mScrollIndicatorDrawable = mContext.getDrawable(R.drawable.scroll_indicator_material);
@@ -577,12 +586,32 @@
dy -= 1.0f;
}
+ boolean isIgnoreOffsetLimitSet = false;
+ int ignoreOffsetLimit = 0;
+ View ignoreOffsetLimitView = findIgnoreOffsetLimitView();
+ if (ignoreOffsetLimitView != null) {
+ LayoutParams lp = (LayoutParams) ignoreOffsetLimitView.getLayoutParams();
+ ignoreOffsetLimit = ignoreOffsetLimitView.getBottom() + lp.bottomMargin;
+ isIgnoreOffsetLimitSet = true;
+ }
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
+ if (child.getVisibility() == View.GONE) {
+ continue;
+ }
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.ignoreOffset) {
child.offsetTopAndBottom((int) dy);
+ } else if (isIgnoreOffsetLimitSet) {
+ int top = child.getTop();
+ int targetTop = Math.max(
+ (int) (ignoreOffsetLimit + lp.topMargin + dy),
+ lp.mFixedTop);
+ if (top != targetTop) {
+ child.offsetTopAndBottom(targetTop - top);
+ }
+ ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
}
}
final boolean isCollapsedOld = mCollapseOffset != 0;
@@ -1024,6 +1053,8 @@
final int rightEdge = width - getPaddingRight();
final int widthAvailable = rightEdge - leftEdge;
+ boolean isIgnoreOffsetLimitSet = false;
+ int ignoreOffsetLimit = 0;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
@@ -1036,9 +1067,24 @@
continue;
}
+ if (mIgnoreOffsetTopLimitViewId != ID_NULL && !isIgnoreOffsetLimitSet) {
+ if (mIgnoreOffsetTopLimitViewId == child.getId()) {
+ ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
+ isIgnoreOffsetLimitSet = true;
+ }
+ }
+
int top = ypos + lp.topMargin;
if (lp.ignoreOffset) {
- top -= mCollapseOffset;
+ if (!isDragging()) {
+ lp.mFixedTop = (int) (top - mCollapseOffset);
+ }
+ if (isIgnoreOffsetLimitSet) {
+ top = Math.max(ignoreOffsetLimit + lp.topMargin, (int) (top - mCollapseOffset));
+ ignoreOffsetLimit = top + child.getMeasuredHeight() + lp.bottomMargin;
+ } else {
+ top -= mCollapseOffset;
+ }
}
final int bottom = top + child.getMeasuredHeight();
@@ -1102,11 +1148,23 @@
mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved;
}
+ private View findIgnoreOffsetLimitView() {
+ if (mIgnoreOffsetTopLimitViewId == ID_NULL) {
+ return null;
+ }
+ View v = findViewById(mIgnoreOffsetTopLimitViewId);
+ if (v != null && v != this && v.getParent() == this && v.getVisibility() != View.GONE) {
+ return v;
+ }
+ return null;
+ }
+
public static class LayoutParams extends MarginLayoutParams {
public boolean alwaysShow;
public boolean ignoreOffset;
public boolean hasNestedScrollIndicator;
public int maxHeight;
+ int mFixedTop;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
diff --git a/core/res/Android.bp b/core/res/Android.bp
index c42517d..179eff8 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -130,6 +130,10 @@
// Allow overlay to add resource
"--auto-add-overlay",
+
+ // Framework resources benefit tremendously from enabling sparse encoding, saving tens
+ // of MBs in size and RAM use.
+ "--enable-sparse-encoding",
],
resource_zips: [
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 5ae133b..f7467b5 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1035,25 +1035,38 @@
android:priority="900" />
<!-- Allows an application to read from external storage.
- <p>Any app that declares the {@link #WRITE_EXTERNAL_STORAGE} permission is implicitly
- granted this permission.</p>
+ <p class="note"><strong>Note: </strong>Starting in API level 33, this permission has no
+ effect. If your app accesses other apps' media files, request one or more of these permissions
+ instead: <a href="#READ_MEDIA_IMAGES"><code>READ_MEDIA_IMAGES</code></a>,
+ <a href="#READ_MEDIA_VIDEO"><code>READ_MEDIA_VIDEO</code></a>,
+ <a href="#READ_MEDIA_AUDIO"><code>READ_MEDIA_AUDIO</code></a>. Learn more about the
+ <a href="{@docRoot}training/data-storage/shared/media#storage-permission">storage
+ permissions</a> that are associated with media files.</p>
+
<p>This permission is enforced starting in API level 19. Before API level 19, this
permission is not enforced and all apps still have access to read from external storage.
You can test your app with the permission enforced by enabling <em>Protect USB
- storage</em> under Developer options in the Settings app on a device running Android 4.1 or
- higher.</p>
+ storage</em> under <b>Developer options</b> in the Settings app on a device running Android
+ 4.1 or higher.</p>
<p>Also starting in API level 19, this permission is <em>not</em> required to
- read/write files in your application-specific directories returned by
+ read or write files in your application-specific directories returned by
{@link android.content.Context#getExternalFilesDir} and
- {@link android.content.Context#getExternalCacheDir}.
- <p class="note"><strong>Note:</strong> If <em>both</em> your <a
+ {@link android.content.Context#getExternalCacheDir}.</p>
+ <p>Starting in API level 29, apps don't need to request this permission to access files in
+ their app-specific directory on external storage, or their own files in the
+ <a href="{@docRoot}reference/android/provider/MediaStore"><code>MediaStore</code></a>. Apps
+ shouldn't request this permission unless they need to access other apps' files in the
+ <code>MediaStore</code>. Read more about these changes in the
+ <a href="{@docRoot}training/data-storage#scoped-storage">scoped storage</a> section of the
+ developer documentation.</p>
+ <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
targetSdkVersion}</a> values are set to 3 or lower, the system implicitly
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.
+ targetSdkVersion}</a> is 4 or higher.</p>
<p> This is a soft restricted permission which cannot be held by an app it its
full form until the installer on record allowlists the permission.
@@ -1134,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
@@ -1142,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/layout/miniresolver.xml b/core/res/res/layout/miniresolver.xml
index ded23fe..38a71f0 100644
--- a/core/res/res/layout/miniresolver.xml
+++ b/core/res/res/layout/miniresolver.xml
@@ -65,8 +65,7 @@
android:paddingTop="32dp"
android:paddingBottom="@dimen/resolver_button_bar_spacing"
android:orientation="vertical"
- android:background="?attr/colorBackground"
- android:layout_ignoreOffset="true">
+ android:background="?attr/colorBackground">
<RelativeLayout
style="?attr/buttonBarStyle"
android:layout_width="match_parent"
diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml
index a7f2aa7..be1c939 100644
--- a/core/res/res/layout/notification_template_header.xml
+++ b/core/res/res/layout/notification_template_header.xml
@@ -24,6 +24,7 @@
android:gravity="center_vertical"
android:orientation="horizontal"
android:theme="@style/Theme.DeviceDefault.Notification"
+ android:importantForAccessibility="no"
>
<ImageView
diff --git a/core/res/res/layout/resolver_list.xml b/core/res/res/layout/resolver_list.xml
index 6a200d05..a06e8a4 100644
--- a/core/res/res/layout/resolver_list.xml
+++ b/core/res/res/layout/resolver_list.xml
@@ -23,6 +23,7 @@
android:maxWidth="@dimen/resolver_max_width"
android:maxCollapsedHeight="@dimen/resolver_max_collapsed_height"
android:maxCollapsedHeightSmall="56dp"
+ android:ignoreOffsetTopLimit="@id/title_container"
android:id="@id/contentPanel">
<RelativeLayout
diff --git a/core/res/res/values-am/strings.xml b/core/res/res/values-am/strings.xml
index 7c439008..46f0d81 100644
--- a/core/res/res/values-am/strings.xml
+++ b/core/res/res/values-am/strings.xml
@@ -178,7 +178,7 @@
<string name="contentServiceTooManyDeletesNotificationDesc" msgid="4562226280528716090">"በጣም ብዙ <xliff:g id="CONTENT_TYPE">%s</xliff:g> ለመሰረዝ ተሞክሯል።"</string>
<string name="low_memory" product="tablet" msgid="5557552311566179924">"የጡባዊ ተኮ ማከማቻ ሙሉ ነው! ቦታ ነፃ ለማድረግ አንዳንድ ፋይሎች ሰርዝ።"</string>
<string name="low_memory" product="watch" msgid="3479447988234030194">"የእጅ ሰዓት ማከማቻ ሙሉ ነው። ቦታ ለማስለቀቅ አንዳንድ ፋይሎችን ይሰርዙ።"</string>
- <string name="low_memory" product="tv" msgid="6663680413790323318">"Android TV መሣሪያ ማከማቻ ሙሉ ነው። ባዶ ቦታን ነጻ ለማድረግ አንዳንድ ፋይሎችን ይሰርዙ።"</string>
+ <string name="low_memory" product="tv" msgid="6663680413790323318">"Android TV መሣሪያ ማከማቻ ሙሉ ነው። ባዶ ቦታን ነፃ ለማድረግ አንዳንድ ፋይሎችን ይሰርዙ።"</string>
<string name="low_memory" product="default" msgid="2539532364144025569">"የስልክ ማከማቻ ሙሉ ነው! ቦታ ነፃ ለማድረግ አንዳንድ ፋይሎች ሰርዝ።"</string>
<string name="ssl_ca_cert_warning" msgid="7233573909730048571">"{count,plural, =1{የእውቅና ማረጋገጫ ባለስልጣን ተጭኗል}one{የእውቅና ማረጋገጫ ባለስልጣናት ተጭነዋል}other{የእውቅና ማረጋገጫ ባለስልጣናት ተጭነዋል}}"</string>
<string name="ssl_ca_cert_noti_by_unknown" msgid="4961102218216815242">"ባልታወቀ ሶስተኛ ወገን"</string>
@@ -1156,7 +1156,7 @@
<string name="input_method_ime_switch_button_desc" msgid="2736542240252198501">"የግቤት ስልትን ቀይር"</string>
<string name="low_internal_storage_view_title" msgid="9024241779284783414">"የማከማቻ ቦታ እያለቀ ነው"</string>
<string name="low_internal_storage_view_text" msgid="8172166728369697835">"አንዳንድ የስርዓት ተግባራት ላይሰሩ ይችላሉ"</string>
- <string name="low_internal_storage_view_text_no_boot" msgid="7368968163411251788">"ለስርዓቱ የሚሆን በቂ ቦታ የለም። 250 ሜባ ነጻ ቦታ እንዳለዎት ያረጋግጡና ዳግም ያስጀምሩ።"</string>
+ <string name="low_internal_storage_view_text_no_boot" msgid="7368968163411251788">"ለስርዓቱ የሚሆን በቂ ቦታ የለም። 250 ሜባ ነፃ ቦታ እንዳለዎት ያረጋግጡና ዳግም ያስጀምሩ።"</string>
<string name="app_running_notification_title" msgid="8985999749231486569">"<xliff:g id="APP_NAME">%1$s</xliff:g> እያሄደ ነው"</string>
<string name="app_running_notification_text" msgid="5120815883400228566">"ተጨማሪ መረጃ ለማግኘት ወይም መተግበሪያውን ለማቆም መታ ያድርጉ።"</string>
<string name="ok" msgid="2646370155170753815">"እሺ"</string>
@@ -1211,7 +1211,7 @@
<string name="aerr_restart" msgid="2789618625210505419">"መተግበሪያውን እንደገና ክፈት"</string>
<string name="aerr_report" msgid="3095644466849299308">"ግብረመልስ ይላኩ"</string>
<string name="aerr_close" msgid="3398336821267021852">"ዝጋ"</string>
- <string name="aerr_mute" msgid="2304972923480211376">"መሣሪያ ዳግም እስኪጀመር ድረስ ድምጽ ያጥፉ"</string>
+ <string name="aerr_mute" msgid="2304972923480211376">"መሣሪያ ዳግም እስኪጀመር ድረስ ድምፅ ያጥፉ"</string>
<string name="aerr_wait" msgid="3198677780474548217">"ጠብቅ"</string>
<string name="aerr_close_app" msgid="8318883106083050970">"መተግበሪያን ዝጋ"</string>
<string name="anr_title" msgid="7290329487067300120"></string>
@@ -1273,7 +1273,7 @@
<string name="dump_heap_ready_text" msgid="5849618132123045516">"የ<xliff:g id="PROC">%1$s</xliff:g> ሂደት ተራጋፊ ክምር ለማጋራት ለእርስዎ ይገኛል። ይጠንቀቁ፦ ይህ ተራጋፊ ክምር ሂደቱ ሊደርስባቸው የሚችለው ማንኛውም የግል መረጃ ሊኖረው ይችላል፣ ይህ እርስዎ የተየቧቸውን ነገሮች ሊያካትት ይችላል።"</string>
<string name="sendText" msgid="493003724401350724">"ለፅሁፍ ድርጊት ምረጥ"</string>
<string name="volume_ringtone" msgid="134784084629229029">"የስልክ ጥሪ ድምፅ"</string>
- <string name="volume_music" msgid="7727274216734955095">"የማህደረ መረጃ ድምጽ መጠን"</string>
+ <string name="volume_music" msgid="7727274216734955095">"የማህደረ መረጃ ድምፅ መጠን"</string>
<string name="volume_music_hint_playing_through_bluetooth" msgid="2614142915948898228">"በብሉቱዝ በኩል ማጫወት"</string>
<string name="volume_music_hint_silent_ringtone_selected" msgid="1514829655029062233">"የፀጥታ የስልክ የደውል ድምፅ ተዘጋጅቷል"</string>
<string name="volume_call" msgid="7625321655265747433">"የጥሪ ላይ ድም ፅ መጨመሪያ/መቀነሻ"</string>
@@ -1284,7 +1284,7 @@
<string name="volume_icon_description_bluetooth" msgid="7540388479345558400">"የብሉቱዝ ድምፅ መጠን"</string>
<string name="volume_icon_description_ringer" msgid="2187800636867423459">"የስልክ ጥሪ ድምፅ መጠን"</string>
<string name="volume_icon_description_incall" msgid="4491255105381227919">"የስልክ ጥሪ ድምፅ መጠን"</string>
- <string name="volume_icon_description_media" msgid="4997633254078171233">"የማህደረ መረጃ ድምጽ መጠን"</string>
+ <string name="volume_icon_description_media" msgid="4997633254078171233">"የማህደረ መረጃ ድምፅ መጠን"</string>
<string name="volume_icon_description_notification" msgid="579091344110747279">"የማሳወቂያ ክፍልፍል"</string>
<string name="ringtone_default" msgid="9118299121288174597">"ነባሪ የስልክ ላይ ጥሪ"</string>
<string name="ringtone_default_with_actual" msgid="2709686194556159773">"ነባሪ (<xliff:g id="ACTUAL_RINGTONE">%1$s</xliff:g>)"</string>
@@ -1617,7 +1617,7 @@
<string name="default_audio_route_name_headphones" msgid="6954070994792640762">"የጆሮ ማዳመጫዎች"</string>
<string name="default_audio_route_name_usb" msgid="895668743163316932">"ዩ ኤስ ቢ"</string>
<string name="default_audio_route_category_name" msgid="5241740395748134483">"ስርዓት"</string>
- <string name="bluetooth_a2dp_audio_route_name" msgid="4214648773120426288">"የብሉቱዝ ድምጽ"</string>
+ <string name="bluetooth_a2dp_audio_route_name" msgid="4214648773120426288">"የብሉቱዝ ድምፅ"</string>
<string name="wireless_display_route_description" msgid="8297563323032966831">"ገመድ አልባ ማሳያ"</string>
<string name="media_route_button_content_description" msgid="2299223698196869956">"Cast"</string>
<string name="media_route_chooser_title" msgid="6646594924991269208">"ከመሳሪያ ጋር ያገናኙ"</string>
@@ -1674,7 +1674,7 @@
<string name="kg_failed_attempts_almost_at_login" product="default" msgid="5270861875006378092">"የመክፈቻ ስርዓተ ጥለቱን <xliff:g id="NUMBER_0">%1$d</xliff:g> ጊዜ በትክክል አልሳሉትም። ከ<xliff:g id="NUMBER_1">%2$d</xliff:g> ተጨማሪ ያልተሳኩ ሙከራዎች በኋላ የኢሜይል መለያ ተጠቅመው ስልክዎን እንዲከፍቱ ይጠየቃሉ።\n\nእባክዎ ከ<xliff:g id="NUMBER_2">%3$d</xliff:g> ሰከንዶች በኋላ እንደገና ይሞክሩ።"</string>
<string name="kg_text_message_separator" product="default" msgid="4503708889934976866">" — "</string>
<string name="kg_reordering_delete_drop_target_text" msgid="2034358143731750914">"አስወግድ"</string>
- <string name="safe_media_volume_warning" product="default" msgid="3751676824423049994">"ድምጹ ከሚመከረው መጠን በላይ ከፍ ይበል?\n\nበከፍተኛ ድምጽ ለረጅም ጊዜ ማዳመጥ ጆሮዎን ሊጎዳው ይችላል።"</string>
+ <string name="safe_media_volume_warning" product="default" msgid="3751676824423049994">"ድምጹ ከሚመከረው መጠን በላይ ከፍ ይበል?\n\nበከፍተኛ ድምፅ ለረጅም ጊዜ ማዳመጥ ጆሮዎን ሊጎዳው ይችላል።"</string>
<string name="accessibility_shortcut_warning_dialog_title" msgid="4017995837692622933">"የተደራሽነት አቋራጭ ጥቅም ላይ ይዋል?"</string>
<string name="accessibility_shortcut_toogle_warning" msgid="4161716521310929544">"አቋራጩ ሲበራ ሁለቱንም የድምጽ አዝራሮች ለ3 ሰከንዶች ተጭኖ መቆየት የተደራሽነት ባህሪን ያስጀምረዋል።"</string>
<string name="accessibility_shortcut_multiple_service_warning_title" msgid="3135860819356676426">"የተደራሽነት ባህሪዎች አቋራጭ ይብራ?"</string>
diff --git a/core/res/res/values-ar/strings.xml b/core/res/res/values-ar/strings.xml
index 1280485..c2f24a5 100644
--- a/core/res/res/values-ar/strings.xml
+++ b/core/res/res/values-ar/strings.xml
@@ -514,7 +514,7 @@
<string name="permdesc_getAccounts" product="default" msgid="2491273043569751867">"للسماح للتطبيق بالحصول على قائمة بالحسابات التي يعرفها الهاتف. وقد يتضمن ذلك أي حسابات تم إنشاؤها بواسطة التطبيقات التي ثبتها."</string>
<string name="permlab_accessNetworkState" msgid="2349126720783633918">"عرض اتصالات الشبكة"</string>
<string name="permdesc_accessNetworkState" msgid="4394564702881662849">"للسماح للتطبيق بعرض معلومات حول اتصالات الشبكة كعرض معلومات عن الشبكات المتوفرة والشبكات المتصلة."</string>
- <string name="permlab_createNetworkSockets" msgid="3224420491603590541">"حق الوصول الكامل إلى الشبكة"</string>
+ <string name="permlab_createNetworkSockets" msgid="3224420491603590541">"الإذن بالوصول الكامل إلى الشبكة"</string>
<string name="permdesc_createNetworkSockets" msgid="7722020828749535988">"للسماح للتطبيق بإنشاء مقابس شبكات واستخدام بروتوكولات شبكات مخصصة. ويوفر المتصفح وتطبيقات أخرى طرقًا لإرسال البيانات إلى الإنترنت، ولذلك لا يعد هذا الإذن مطلوبًا لإرسال البيانات إلى الإنترنت."</string>
<string name="permlab_changeNetworkState" msgid="8945711637530425586">"تغيير اتصال الشبكة"</string>
<string name="permdesc_changeNetworkState" msgid="649341947816898736">"للسماح للتطبيق بتغيير حالة اتصال الشبكة."</string>
@@ -1272,7 +1272,7 @@
<string name="dump_heap_ready_notification" msgid="2302452262927390268">"نَسْخ الذاكرة <xliff:g id="PROC">%1$s</xliff:g> جاهز"</string>
<string name="dump_heap_notification_detail" msgid="8431586843001054050">"تم جمع مقدار كبير من بيانات الذاكرة. انقر للمشاركة."</string>
<string name="dump_heap_title" msgid="4367128917229233901">"هل تريد مشاركة نَسْخ الذاكرة؟"</string>
- <string name="dump_heap_text" msgid="1692649033835719336">"تجاوزت عملية <xliff:g id="PROC">%1$s</xliff:g> حد الذاكرة المخصص لها وقدره <xliff:g id="SIZE">%2$s</xliff:g>، ويتوفر نَسْخ للذاكرة لمشاركته مع مطور برامج العملية ولكن توخ الحذر حيث قد يحتوي نَسْخ الذاكرة هذا على معلومات شخصية يملك التطبيق حق الوصول إليها."</string>
+ <string name="dump_heap_text" msgid="1692649033835719336">"تجاوزت عملية <xliff:g id="PROC">%1$s</xliff:g> حد الذاكرة المخصص لها وقدره <xliff:g id="SIZE">%2$s</xliff:g>، ويتوفر نَسْخ للذاكرة لمشاركته مع مطور برامج العملية ولكن توخ الحذر حيث قد يحتوي نَسْخ الذاكرة هذا على معلومات شخصية يملك التطبيق الإذن بالوصول إليها."</string>
<string name="dump_heap_system_text" msgid="6805155514925350849">"تجاوزت عملية <xliff:g id="PROC">%1$s</xliff:g> القيد المفروض على الذاكرة الذي يبلغ <xliff:g id="SIZE">%2$s</xliff:g>. ويتوفّر نَسْخ ذاكرة يمكنك مشاركته. تحذير: قد يحتوي نَسْخ الذاكرة هذا على معلومات شخصية حسّاسة يمكن للعملية الوصول إليها، وقد يتضمن معلومات سبق لك كتابتها."</string>
<string name="dump_heap_ready_text" msgid="5849618132123045516">"يتوفّر نَسْخ ذاكرة من عملية <xliff:g id="PROC">%1$s</xliff:g> حتى تتمكّن من مشاركته. تحذير: قد يحتوي نَسْخ الذاكرة هذا على معلومات شخصية حسّاسة يمكن للعملية الوصول إليها، وقد يتضمن معلومات سبق لك كتابتها."</string>
<string name="sendText" msgid="493003724401350724">"اختيار إجراء للنص"</string>
diff --git a/core/res/res/values-as/strings.xml b/core/res/res/values-as/strings.xml
index c9550c8..34cac8b 100644
--- a/core/res/res/values-as/strings.xml
+++ b/core/res/res/values-as/strings.xml
@@ -171,7 +171,7 @@
<string name="httpErrorBadUrl" msgid="754447723314832538">"অমান্য URLৰ বাবে পৃষ্ঠাটো খুলিব পৰা নগ\'ল।"</string>
<string name="httpErrorFile" msgid="3400658466057744084">"ফাইলত খুলিব পৰা নগ\'ল।"</string>
<string name="httpErrorFileNotFound" msgid="5191433324871147386">"অনুৰোধ কৰা ফাইলটো বিচাৰি পোৱা নগ\'ল।"</string>
- <string name="httpErrorTooManyRequests" msgid="2149677715552037198">"বহুত বেছি অনুৰোধৰ প্ৰক্ৰিয়া চলি আছে৷ অনুগ্ৰহ কৰি পিছত আকৌ চেষ্টা কৰক৷"</string>
+ <string name="httpErrorTooManyRequests" msgid="2149677715552037198">"বহুত বেছি অনুৰোধৰ প্ৰক্ৰিয়া চলি আছে৷ অনুগ্ৰহ কৰি পাছত আকৌ চেষ্টা কৰক৷"</string>
<string name="notification_title" msgid="5783748077084481121">"<xliff:g id="ACCOUNT">%1$s</xliff:g>ত ছাইন ইন কৰাত আসোঁৱাহ"</string>
<string name="contentServiceSync" msgid="2341041749565687871">"ছিংক ত্ৰুটি"</string>
<string name="contentServiceSyncNotificationTitle" msgid="5766411446676388623">"ছিংক কৰিব নোৱাৰি"</string>
@@ -203,7 +203,7 @@
<string name="device_policy_manager_service" msgid="5085762851388850332">"ডিভাইচৰ নীতিৰ পৰিচালক সেৱা"</string>
<string name="music_recognition_manager_service" msgid="7481956037950276359">"সংগীত চিনাক্তকৰণ পৰিচালক সেৱা"</string>
<string name="factory_reset_warning" msgid="6858705527798047809">"আপোনাৰ ডিভাইচৰ ডেটা মচা হ\'ব"</string>
- <string name="factory_reset_message" msgid="2657049595153992213">"এই প্ৰশাসক এপটো ব্যৱহাৰ কৰিব নোৱাৰি। এতিয়া আপোনাৰ ডিভাইচটোৰ ডেটা মচা হ\'ব।\n\nআপোনাৰ কিবা প্ৰশ্ন থাকিলে আপোনাৰ প্ৰতিষ্ঠানৰ প্ৰশাসকৰ সৈতে যোগাযোগ কৰক।"</string>
+ <string name="factory_reset_message" msgid="2657049595153992213">"এই প্ৰশাসক এপ্টো ব্যৱহাৰ কৰিব নোৱাৰি। এতিয়া আপোনাৰ ডিভাইচটোৰ ডেটা মচা হ\'ব।\n\nআপোনাৰ কিবা প্ৰশ্ন থাকিলে আপোনাৰ প্ৰতিষ্ঠানৰ প্ৰশাসকৰ সৈতে যোগাযোগ কৰক।"</string>
<string name="printing_disabled_by" msgid="3517499806528864633">"প্ৰিণ্ট কৰা কাৰ্য <xliff:g id="OWNER_APP">%s</xliff:g>এ অক্ষম কৰি ৰাখিছে।"</string>
<string name="personal_apps_suspension_title" msgid="7561416677884286600">"কৰ্মস্থানৰ প্ৰ’ফাইলটো অন কৰক"</string>
<string name="personal_apps_suspension_text" msgid="6115455688932935597">"আপুনি নিজৰ কৰ্মস্থানৰ প্ৰ’ফাইলটো অন নকৰালৈকে আপোনাৰ ব্যক্তিগত এপ্সমূহ অৱৰোধ কৰা থাকে"</string>
@@ -342,23 +342,23 @@
<string name="capability_title_canTakeScreenshot" msgid="3895812893130071930">"স্ক্ৰীনশ্বট লওক"</string>
<string name="capability_desc_canTakeScreenshot" msgid="7762297374317934052">"ডিছপ্লে’খনৰ এটা স্ক্ৰীনশ্বট ল\'ব পাৰে।"</string>
<string name="permlab_statusBar" msgid="8798267849526214017">"স্থিতি দণ্ড অক্ষম কৰক বা সলনি কৰক"</string>
- <string name="permdesc_statusBar" msgid="5809162768651019642">"স্থিতি দণ্ড অক্ষম কৰিবলৈ বা ছিষ্টেম আইকন আঁতৰাবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_statusBar" msgid="5809162768651019642">"স্থিতি দণ্ড অক্ষম কৰিবলৈ বা ছিষ্টেম আইকন আঁতৰাবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_statusBarService" msgid="2523421018081437981">"স্থিতি দণ্ড হ\'ব পাৰে"</string>
- <string name="permdesc_statusBarService" msgid="6652917399085712557">"নিজকে স্থিতি দণ্ডৰূপে দেখুওৱাবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_statusBarService" msgid="6652917399085712557">"নিজকে স্থিতি দণ্ডৰূপে দেখুওৱাবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_expandStatusBar" msgid="1184232794782141698">"স্থিতি দণ্ড সম্প্ৰসাৰিত বা সংকোচিত কৰক"</string>
- <string name="permdesc_expandStatusBar" msgid="7180756900448498536">"স্থিতি দণ্ড বিস্তাৰিত বা সংকুচিত কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_expandStatusBar" msgid="7180756900448498536">"স্থিতি দণ্ড বিস্তাৰিত বা সংকুচিত কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_fullScreenIntent" msgid="4310888199502509104">"কোনো লক কৰি ৰখা ডিভাইচত জাননী পূৰ্ণ স্ক্ৰীনৰ কাৰ্যকলাপ হিচাপে প্ৰদৰ্শন কৰক"</string>
<string name="permdesc_fullScreenIntent" msgid="1100721419406643997">"এপ্টোক কোনো লক কৰি ৰখা ডিভাইচত জাননী পূৰ্ণ স্ক্ৰীনৰ কাৰ্যকলাপ হিচাপে প্ৰদৰ্শন কৰিবলৈ অনুমতি দিয়ে"</string>
<string name="permlab_install_shortcut" msgid="7451554307502256221">"শ্বৰ্টকাট ইনষ্টল কৰিব পাৰে"</string>
- <string name="permdesc_install_shortcut" msgid="4476328467240212503">"এটা এপ্লিকেশ্বনক ব্যৱহাৰকাৰীৰ হস্তক্ষেপৰ অবিহনে গৃহ স্ক্ৰীণ শ্বৰ্টকাট যোগ কৰিবলৈ অনুমতি দিয়ে।"</string>
+ <string name="permdesc_install_shortcut" msgid="4476328467240212503">"এটা এপ্লিকেশ্বনক ব্যৱহাৰকাৰীৰ হস্তক্ষেপৰ অবিহনে গৃহ স্ক্ৰীন শ্বৰ্টকাট যোগ কৰিবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_uninstall_shortcut" msgid="295263654781900390">"শ্বৰ্টকাট আনইনষ্টল কৰিব পাৰে"</string>
- <string name="permdesc_uninstall_shortcut" msgid="1924735350988629188">"ব্যৱহাৰকাৰীৰ হস্তক্ষেপৰ অবিহনে গৃহ স্ক্ৰীণৰ শ্বৰ্টকাটসমূহ আঁতৰাবলৈ এপ্লিকেশ্বনক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_uninstall_shortcut" msgid="1924735350988629188">"ব্যৱহাৰকাৰীৰ হস্তক্ষেপৰ অবিহনে গৃহ স্ক্ৰীনৰ শ্বৰ্টকাটসমূহ আঁতৰাবলৈ এপ্লিকেশ্বনক অনুমতি দিয়ে।"</string>
<string name="permlab_processOutgoingCalls" msgid="4075056020714266558">"বহিৰ্গামী কলসমূহ অন্য ক\'ৰবালৈ পঠিয়াওক"</string>
<string name="permdesc_processOutgoingCalls" msgid="7833149750590606334">"এটা বৰ্হিগামী কল কৰি থকাৰ সময়ত ডায়েল কৰা নম্বৰ চাবলৈ আৰু লগতে এটা পৃথক নম্বৰলৈ কল সংযোগ কৰিবলৈ বা সকলোকে একেলগে বন্ধ কৰিবলৈ এপক অনুমতি দিয়ে।"</string>
<string name="permlab_answerPhoneCalls" msgid="4131324833663725855">"ফ\'ন কলৰ উত্তৰ দিব পাৰে"</string>
- <string name="permdesc_answerPhoneCalls" msgid="894386681983116838">"এপটোক অন্তৰ্গামী ফ\'ন কলৰ উত্তৰ দিবলৈ অনুমতি দিয়ে।"</string>
+ <string name="permdesc_answerPhoneCalls" msgid="894386681983116838">"এপ্টোক অন্তৰ্গামী ফ\'ন কলৰ উত্তৰ দিবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_receiveSms" msgid="505961632050451881">"পাঠ বার্তা (এছএমএছ) বোৰ লাভ কৰক"</string>
- <string name="permdesc_receiveSms" msgid="1797345626687832285">"এপটোক এছএমএছ বাৰ্তাবোৰ পাবলৈ আৰু প্ৰক্ৰিয়া সম্পন্ন কৰিবলৈ অনুমতি দিয়ে৷ ইয়াৰ অৰ্থ এইটোৱেই যে এপটোৱে আপোনাক বাৰ্তাবোৰ নেদেখুৱাকৈয়ে আপোনাৰ ডিভাইচলৈ পঠিওৱা বাৰ্তাবোৰ নিৰীক্ষণ কৰিব বা মচিব পাৰে৷"</string>
+ <string name="permdesc_receiveSms" msgid="1797345626687832285">"এপ্টোক এছএমএছ বাৰ্তাবোৰ পাবলৈ আৰু প্ৰক্ৰিয়া সম্পন্ন কৰিবলৈ অনুমতি দিয়ে৷ ইয়াৰ অৰ্থ এইটোৱেই যে এপটোৱে আপোনাক বাৰ্তাবোৰ নেদেখুৱাকৈয়ে আপোনাৰ ডিভাইচলৈ পঠিওৱা বাৰ্তাবোৰ নিৰীক্ষণ কৰিব বা মচিব পাৰে৷"</string>
<string name="permlab_receiveMms" msgid="4000650116674380275">"পাঠ বার্তা (এমএমএছ) বোৰ লাভ কৰক"</string>
<string name="permdesc_receiveMms" msgid="958102423732219710">"এমএমএছ বার্তাবোৰ লাভ আৰু ইয়াৰ প্ৰক্ৰিয়া সম্পন্ন কৰিবলৈ এপক অনুমতি দিয়ে। ইয়াৰ অৰ্থ হৈছে এই এপে আপোনাৰ ডিভাইচলৈ প্ৰেৰণ কৰা বার্তাসমূহ আপোনাক নেদেখুৱাকৈয়ে পৰ্যবেক্ষণ আৰু মচিব পাৰে।"</string>
<string name="permlab_bindCellBroadcastService" msgid="586746677002040651">"চেল সম্প্ৰচাৰ বাৰ্তাসমূহ ফৰৱাৰ্ড কৰক"</string>
@@ -368,26 +368,26 @@
<string name="permlab_readCellBroadcasts" msgid="5869884450872137693">"চেল সম্প্ৰচাৰৰ বার্তাবোৰ পঢ়ক"</string>
<string name="permdesc_readCellBroadcasts" msgid="672513437331980168">"আপোনাৰ ডিভাইচে লাভ কৰা চেল সম্প্ৰচাৰৰ বার্তাবোৰ পঢ়িবলৈ এপক অনুমতি দিয়ে। আপোনাক জৰুৰীকালীন পৰিস্থিতিবোৰত সর্তক কৰিবলৈ চেল সম্প্ৰচাৰৰ বার্তাবোৰ প্ৰেৰণ কৰা হয়। জৰুৰীকালীন চেল সম্প্ৰচাৰ লাভ কৰাৰ সময়ত আপোনাৰ ডিভাইচৰ কাৰ্যদক্ষতা বা কাৰ্যপ্ৰণালীত ক্ষতিকাৰক এপবোৰে হস্তক্ষেপ কৰিব পাৰে।"</string>
<string name="permlab_subscribedFeedsRead" msgid="217624769238425461">"আপুনি সদস্যভুক্ত হোৱা ফীডসমূহ পঢ়ক"</string>
- <string name="permdesc_subscribedFeedsRead" msgid="6911349196661811865">"বৰ্তমান ছিংক কৰা ফীডৰ সবিশেষ লাভ কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_subscribedFeedsRead" msgid="6911349196661811865">"বৰ্তমান ছিংক কৰা ফীডৰ সবিশেষ লাভ কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_sendSms" msgid="7757368721742014252">"এছএমএছ ৰ বার্তাবোৰ প্ৰেৰণ কৰিব আৰু চাব পাৰে"</string>
- <string name="permdesc_sendSms" msgid="6757089798435130769">"এপটোক এছএমএছ বাৰ্তা পঠিয়াবলৈ অনুমতি দিয়ে৷ ইয়াৰ ফলত অপ্ৰত্যাশিত মাচুল ভৰিবলগা হ\'ব পাৰে৷ ক্ষতিকাৰক এপসমূহে আপোনাৰ অনুমতি নোলোৱাকৈয়ে বাৰ্তা পঠিয়াই আপোনাৰ পৰা মাচুল কাটিব পাৰে৷"</string>
+ <string name="permdesc_sendSms" msgid="6757089798435130769">"এপ্টোক এছএমএছ বাৰ্তা পঠিয়াবলৈ অনুমতি দিয়ে৷ ইয়াৰ ফলত অপ্ৰত্যাশিত মাচুল ভৰিবলগা হ\'ব পাৰে৷ ক্ষতিকাৰক এপসমূহে আপোনাৰ অনুমতি নোলোৱাকৈয়ে বাৰ্তা পঠিয়াই আপোনাৰ পৰা মাচুল কাটিব পাৰে৷"</string>
<string name="permlab_readSms" msgid="5164176626258800297">"আপোনাৰ পাঠ বার্তাবোৰ পঢ়ক (এছএমএছ বা এমএমএছ)"</string>
<string name="permdesc_readSms" product="tablet" msgid="7912990447198112829">"এই এপ্টোৱে আপোনাৰ টেবলেটটোত সংৰক্ষিত আটাইবোৰ এছএমএছ (পাঠ) বাৰ্তা পঢ়িব পাৰে।"</string>
<string name="permdesc_readSms" product="tv" msgid="3054753345758011986">"এই এপ্টোৱে আপোনাৰ Android TV ডিভাইচত ষ্ট’ৰ কৰি ৰখা আটাইবোৰ এছএমএছ (পাঠ) বাৰ্তা পঢ়িব পাৰে।"</string>
<string name="permdesc_readSms" product="default" msgid="774753371111699782">"এই এপ্টোৱে আপোনাৰ ফ\'নত সংৰক্ষিত আটাইবোৰ এছএমএছ (পাঠ) বাৰ্তা পঢ়িব পাৰে।"</string>
<string name="permlab_receiveWapPush" msgid="4223747702856929056">"পাঠ বার্তা (WAP) বোৰ লাভ কৰক"</string>
- <string name="permdesc_receiveWapPush" msgid="1638677888301778457">"এপটোক WAP বাৰ্তাবোৰ পাবলৈ আৰু প্ৰক্ৰিয়া সম্পন্ন কৰিবলৈ অনুমতি দিয়ে৷ এই অনুমতিত আপোনালৈ পঠিওৱা বাৰ্তাবোৰ আপোনাক নেদেখুৱাকৈয়ে নিৰীক্ষণ বা মচাৰ সক্ষমতা অন্তৰ্ভুক্ত থাকে৷"</string>
+ <string name="permdesc_receiveWapPush" msgid="1638677888301778457">"এপ্টোক WAP বাৰ্তাবোৰ পাবলৈ আৰু প্ৰক্ৰিয়া সম্পন্ন কৰিবলৈ অনুমতি দিয়ে৷ এই অনুমতিত আপোনালৈ পঠিওৱা বাৰ্তাবোৰ আপোনাক নেদেখুৱাকৈয়ে নিৰীক্ষণ বা মচাৰ সক্ষমতা অন্তৰ্ভুক্ত থাকে৷"</string>
<string name="permlab_getTasks" msgid="7460048811831750262">"চলি থকা এপসমূহ বিচাৰি উলিয়াওক"</string>
- <string name="permdesc_getTasks" msgid="7388138607018233726">"এপটোক বৰ্তমানে আৰু শেহতীয়াভাৱে চলি থকা কাৰ্যসমূহৰ বিষয়ে তথ্য পুনৰুদ্ধাৰ কৰিবলৈ অনুমতি দিয়ে৷ এইটোৱে এপটোক ডিভাইচটোত কোনবোৰ এপ্লিকেশ্বন ব্যৱহাৰ হৈ আছে তাৰ বিষয়ে তথ্য বিচাৰি উলিয়াবলৈ অনুমতি দিব পাৰে৷"</string>
+ <string name="permdesc_getTasks" msgid="7388138607018233726">"এপ্টোক বৰ্তমানে আৰু শেহতীয়াভাৱে চলি থকা কাৰ্যসমূহৰ বিষয়ে তথ্য পুনৰুদ্ধাৰ কৰিবলৈ অনুমতি দিয়ে৷ এইটোৱে এপ্টোক ডিভাইচটোত কোনবোৰ এপ্লিকেশ্বন ব্যৱহাৰ হৈ আছে তাৰ বিষয়ে তথ্য বিচাৰি উলিয়াবলৈ অনুমতি দিব পাৰে৷"</string>
<string name="permlab_manageProfileAndDeviceOwners" msgid="639849495253987493">"প্ৰ\'ফাইল আৰু ডিভাইচৰ গৰাকীসকলক পৰিচালনা কৰিব পাৰে"</string>
- <string name="permdesc_manageProfileAndDeviceOwners" msgid="7304240671781989283">"প্ৰ\'ফাইলৰ গৰাকী আৰু ডিভাইচৰ গৰাকী ছেট কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_manageProfileAndDeviceOwners" msgid="7304240671781989283">"প্ৰ\'ফাইলৰ গৰাকী আৰু ডিভাইচৰ গৰাকী ছেট কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_reorderTasks" msgid="7598562301992923804">"চলি থকা এপসমূহক পুনৰাই ক্ৰমবদ্ধ কৰক"</string>
<string name="permdesc_reorderTasks" msgid="8796089937352344183">"গতিবিধিক অগ্ৰভাগ আৰু নেপথ্যলৈ নিবলৈ এপক অনুমতি দিয়ে। এপে এই কার্য আপোনাৰ ইনপুট অবিহনেই কৰিব পাৰে।"</string>
<string name="permlab_enableCarMode" msgid="893019409519325311">"গাড়ীৰ ম\'ড সক্ষম কৰক"</string>
- <string name="permdesc_enableCarMode" msgid="56419168820473508">"গাড়ী ম\'ড সক্ষম কৰিবলৈ এপটোক অনুমতি দিয়ে৷"</string>
+ <string name="permdesc_enableCarMode" msgid="56419168820473508">"গাড়ী ম\'ড সক্ষম কৰিবলৈ এপ্টোক অনুমতি দিয়ে৷"</string>
<string name="permlab_killBackgroundProcesses" msgid="6559320515561928348">"অন্য এপবোৰ বন্ধ কৰক"</string>
- <string name="permdesc_killBackgroundProcesses" msgid="2357013583055434685">"এপটোক অন্য এপসমূহৰ নেপথ্যৰ প্ৰক্ৰিয়াসমূহ শেষ কৰিবলৈ অনুমতি দিয়ে৷ এই কার্যৰ বাবে অন্য এপসমূহ চলাটো বন্ধ হ\'ব পাৰে৷"</string>
- <string name="permlab_systemAlertWindow" msgid="5757218350944719065">"এই এপটো অইন এপৰ ওপৰত প্ৰদৰ্শিত হ\'ব পাৰে"</string>
+ <string name="permdesc_killBackgroundProcesses" msgid="2357013583055434685">"এপ্টোক অন্য এপসমূহৰ নেপথ্যৰ প্ৰক্ৰিয়াসমূহ শেষ কৰিবলৈ অনুমতি দিয়ে৷ এই কার্যৰ বাবে অন্য এপসমূহ চলাটো বন্ধ হ\'ব পাৰে৷"</string>
+ <string name="permlab_systemAlertWindow" msgid="5757218350944719065">"এই এপ্টো অইন এপৰ ওপৰত প্ৰদৰ্শিত হ\'ব পাৰে"</string>
<string name="permdesc_systemAlertWindow" msgid="1145660714855738308">"এই এপ্টো অন্য এপৰ ওপৰত বা স্ক্ৰীনৰ অন্য অংশত প্ৰদৰ্শিত হ\'ব পাৰে। এই কাৰ্যই এপৰ স্বাভাৱিক ব্যৱহাৰত ব্যাঘাত জন্মাব পাৰে আৰু অন্য এপ্সমূহক স্ক্ৰীনত কেনেকৈ দেখা পোৱা যায় সেইটো সলনি কৰিব পাৰে।"</string>
<string name="permlab_runInBackground" msgid="541863968571682785">"নেপথ্যত চলিব পাৰে"</string>
<string name="permdesc_runInBackground" msgid="4344539472115495141">"এই এপ্টো নেপথ্যত চলিব পাৰে। ইয়াৰ ফলত বেটাৰী সোনকালে শেষ হ’ব পাৰে।"</string>
@@ -398,15 +398,15 @@
<string name="permdesc_persistentActivity" product="tv" msgid="6800526387664131321">"এপ্টোক মেম’ৰীত নিজৰ বাবে প্ৰয়োজনীয় ঠাই পৃথক কৰিবলৈ অনুমতি দিয়ে। এই কার্যই আপোনাৰ Android TV ডিভাইচটোক লেহেমীয়া কৰি অন্য এপ্সমূহৰ বাবে উপলব্ধ মেম’ৰীক সীমাবদ্ধ কৰিব পাৰে।"</string>
<string name="permdesc_persistentActivity" product="default" msgid="1914841924366562051">"মেম\'ৰিত নিজৰ বাবে প্ৰয়োজনীয় ঠাই পৃথক কৰিবলৈ এপক অনুমতি দিয়ে। এই কার্যই ফ\'নৰ কার্যক লেহেমীয়া কৰি অন্য এপবোৰৰ বাবে উপলব্ধ মেম\'ৰিক সীমাবদ্ধ কৰে।"</string>
<string name="permlab_foregroundService" msgid="1768855976818467491">"অগ্ৰভূমিৰ সেৱা চলাব পাৰে"</string>
- <string name="permdesc_foregroundService" msgid="8720071450020922795">"এপটোক অগ্ৰভূমি সেৱাসমূহ ব্যৱহাৰ কৰিবলৈ অনুমতি দিয়ে।"</string>
+ <string name="permdesc_foregroundService" msgid="8720071450020922795">"এপ্টোক অগ্ৰভূমি সেৱাসমূহ ব্যৱহাৰ কৰিবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_getPackageSize" msgid="375391550792886641">"এপৰ ষ্ট’ৰেজৰ খালী ঠাই হিচাপ কৰক"</string>
- <string name="permdesc_getPackageSize" msgid="742743530909966782">"এপটোক ইয়াৰ ক\'ড, ডেটা আৰু কেশ্বৰ আকাৰ বিচাৰি উলিয়াবলৈ অনুমতি দিয়ে"</string>
+ <string name="permdesc_getPackageSize" msgid="742743530909966782">"এপ্টোক ইয়াৰ ক\'ড, ডেটা আৰু কেশ্বৰ আকাৰ বিচাৰি উলিয়াবলৈ অনুমতি দিয়ে"</string>
<string name="permlab_writeSettings" msgid="8057285063719277394">"ছিষ্টেম ছেটিংহ সংশোধন কৰক"</string>
<string name="permdesc_writeSettings" msgid="8293047411196067188">"এপ্টোক ছিষ্টেমৰ ছেটিঙৰ ডেটা সংশোধন কৰিবলৈ অনুমতি দিয়ে৷ ক্ষতিকাৰক এপ্সমূহে আপোনাৰ ছিষ্টেম কনফিগাৰেশ্বনক ক্ষতিগ্ৰস্ত কৰিব পাৰে৷"</string>
<string name="permlab_receiveBootCompleted" msgid="6643339400247325379">"আৰম্ভ হোৱাৰ সময়ত চলাওক"</string>
- <string name="permdesc_receiveBootCompleted" product="tablet" msgid="5565659082718177484">"ছিষ্টেমে বুট কৰা কাৰ্য সমাপ্ত কৰাৰ লগে লগে এপটোক নিজে নিজে আৰম্ভ হ\'বলৈ অনুমতি দিয়ে। ইয়াৰ ফলত ফ\'নটো ষ্টাৰ্ট হওতে বেছি সময়ৰ প্ৰয়োজন হ\'ব পাৰে, আৰু এপটো সদায় চলি থকাৰ কাৰণে ফ\'নটো সামগ্ৰিকভাৱে লেহেমীয়া হ\'ব পাৰে।"</string>
+ <string name="permdesc_receiveBootCompleted" product="tablet" msgid="5565659082718177484">"ছিষ্টেমে বুট কৰা কাৰ্য সমাপ্ত কৰাৰ লগে লগে এপ্টোক নিজে নিজে আৰম্ভ হ\'বলৈ অনুমতি দিয়ে। ইয়াৰ ফলত ফ\'নটো ষ্টাৰ্ট হওতে বেছি সময়ৰ প্ৰয়োজন হ\'ব পাৰে, আৰু এপ্টো সদায় চলি থকাৰ কাৰণে ফ\'নটো সামগ্ৰিকভাৱে লেহেমীয়া হ\'ব পাৰে।"</string>
<string name="permdesc_receiveBootCompleted" product="tv" msgid="4900842256047614307">"ছিষ্টেমে বুটিং সমাপ্ত কৰাৰ লগে লগে এই এপ্টোক নিজে নিজে আৰম্ভ হ’বলৈ অনুমতি দিয়ে। এই কাৰ্যৰ বাবে আপোনাৰ Android TV ডিভাইচটো আৰম্ভ হ’বলৈ দীঘলীয়া সময়ৰ প্ৰয়োজন হ’ব পাৰে আৰু সকলো সময়তে চলি থাকি এপ্টোক সামগ্ৰিকভাৱে ডিভাইচটো লেহেমীয়া কৰিবলৈ দিয়ে।"</string>
- <string name="permdesc_receiveBootCompleted" product="default" msgid="7912677044558690092">"ছিষ্টেমে বুট কৰা কাৰ্য সমাপ্ত কৰাৰ লগে লগে এপটোক নিজে নিজে আৰম্ভ হ\'বলৈ অনুমতি দিয়ে। ইয়াৰ ফলত ফ\'নটো ষ্টাৰ্ট হওতে বেছি সময়ৰ প্ৰয়োজন হ\'ব পাৰে, আৰু এপটো সদায় চলি থকাৰ কাৰণে ফ\'নটো সামগ্ৰিকভাৱে লেহেমীয়া হ\'ব পাৰে।"</string>
+ <string name="permdesc_receiveBootCompleted" product="default" msgid="7912677044558690092">"ছিষ্টেমে বুট কৰা কাৰ্য সমাপ্ত কৰাৰ লগে লগে এপ্টোক নিজে নিজে আৰম্ভ হ\'বলৈ অনুমতি দিয়ে। ইয়াৰ ফলত ফ\'নটো ষ্টাৰ্ট হওতে বেছি সময়ৰ প্ৰয়োজন হ\'ব পাৰে, আৰু এপ্টো সদায় চলি থকাৰ কাৰণে ফ\'নটো সামগ্ৰিকভাৱে লেহেমীয়া হ\'ব পাৰে।"</string>
<string name="permlab_broadcastSticky" msgid="4552241916400572230">"ষ্টিকী ব্ৰ\'ডকাষ্ট পঠিয়াওক"</string>
<string name="permdesc_broadcastSticky" product="tablet" msgid="5058486069846384013">"সম্প্ৰচাৰৰ শেষত বাকী ৰোৱা ষ্টিকী ব্ৰ\'ডকাষ্টবোৰ প্ৰেৰণ কৰিবলৈ এপক অনুমতি দিয়ে। ইয়াক অত্য়ধিক ব্যৱহাৰ কৰাৰ ফলত মেম\'ৰি অধিক খৰচ হোৱাৰ বাবে টেবলেট লেহেমীয়া বা অস্থিৰ হৈ পৰে।"</string>
<string name="permdesc_broadcastSticky" product="tv" msgid="2338185920171000650">"এপ্টোক ব্ৰ’ডকাষ্ট শেষ হোৱাৰ পাছত বাকী থকা ষ্টিকী ব্ৰ’ডকাষ্টবোৰ পঠিয়াবলৈ অনুমতি দিয়ে। ইয়াক অত্যধিক ব্যৱহাৰ কৰিলে আপোনাৰ Android TV ডিভাইচটোক অতি বেছি পৰিমাণৰ মেম’ৰী খৰচ কৰাই লেহেমীয়া অথবা অস্থিৰ কৰিব পাৰে।"</string>
@@ -464,62 +464,62 @@
<string name="permlab_cameraOpenCloseListener" msgid="5548732769068109315">"কোনো এপ্লিকেশ্বন অথবা সেৱাক কেমেৰা ডিভাইচসমূহ খোলা অথবা বন্ধ কৰাৰ বিষয়ে কলবেকসমূহ গ্ৰহণ কৰিবলৈ অনুমতি দিয়ক।"</string>
<string name="permdesc_cameraOpenCloseListener" msgid="2002636131008772908">"যিকোনো কেমেৰা ডিভাইচ খুলি থকা অথবা বন্ধ কৰি থকাৰ সময়ত (কোনো এপ্লিকেশ্বনৰ দ্বাৰা) এই এপ্টোৱে কলবেক গ্ৰহণ কৰিব পাৰে।"</string>
<string name="permlab_vibrate" msgid="8596800035791962017">"কম্পন নিয়ন্ত্ৰণ কৰক"</string>
- <string name="permdesc_vibrate" msgid="8733343234582083721">"ভাইব্ৰেটৰ নিয়ন্ত্ৰণ কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_vibrate" msgid="8733343234582083721">"ভাইব্ৰেটৰ নিয়ন্ত্ৰণ কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permdesc_vibrator_state" msgid="7050024956594170724">"এপ্টোক কম্পন স্থিতিটো এক্সেছ কৰিবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_callPhone" msgid="1798582257194643320">"পোনপটীয়াকৈ ফ\'ন নম্বৰলৈ কল কৰক"</string>
- <string name="permdesc_callPhone" msgid="5439809516131609109">"আপোনাৰ কোনো ব্যাঘাত নোহোৱাকৈ ফ\'ন নম্বৰবোৰত কল কৰিবলৈ এপক অনুমতি দিয়ে৷ ইয়াৰ ফলত অপ্ৰত্যাশিত মাচুল ভৰিবলগা বা কলবোৰ কৰা হ\'ব পাৰে৷ মনত ৰাখিব যে ই এপটোক জৰুৰীকালীন নম্বৰবোৰত কল কৰিবলৈ অনুমতি নিদিয়ে৷ ক্ষতিকাৰক এপসমূহে আপোনাৰ অনুমতি নোলোৱাকৈয়ে কল কৰি আপোনাক টকা খৰছ কৰাব পাৰে৷"</string>
+ <string name="permdesc_callPhone" msgid="5439809516131609109">"আপোনাৰ কোনো ব্যাঘাত নোহোৱাকৈ ফ\'ন নম্বৰবোৰত কল কৰিবলৈ এপক অনুমতি দিয়ে৷ ইয়াৰ ফলত অপ্ৰত্যাশিত মাচুল ভৰিবলগা বা কলবোৰ কৰা হ\'ব পাৰে৷ মনত ৰাখিব যে ই এপ্টোক জৰুৰীকালীন নম্বৰবোৰত কল কৰিবলৈ অনুমতি নিদিয়ে৷ ক্ষতিকাৰক এপসমূহে আপোনাৰ অনুমতি নোলোৱাকৈয়ে কল কৰি আপোনাক টকা খৰছ কৰাব পাৰে৷"</string>
<string name="permlab_accessImsCallService" msgid="442192920714863782">"আইএমএছ কল সেৱা ব্যৱহাৰ কৰিব পাৰে"</string>
<string name="permdesc_accessImsCallService" msgid="6328551241649687162">"আপোনাৰ হস্তক্ষেপৰ অবিহনে আইএমএছ সেৱা ব্যৱহাৰ কৰি কল কৰিবলৈ এপক অনুমতি দিয়ে।"</string>
<string name="permlab_readPhoneState" msgid="8138526903259297969">"ফ\'নৰ স্থিতি আৰু পৰিচয় পঢ়ক"</string>
- <string name="permdesc_readPhoneState" msgid="7229063553502788058">"ডিভাইচত থকা ফ\'নৰ সুবিধাসমূহ ব্যৱহাৰ কৰিবলৈ এপটোক অনুমতি দিয়ে৷ এই অনুমতিয়ে কোনো কল সক্ৰিয় হৈ থাককেই বা নাথাকক আৰু দূৰবৰ্তী নম্বৰটো কলৰ দ্বাৰা সংযোজিত হওকেই বা নহওক এপটোক ফ\'ন নম্বৰ আৰু ডিভাইচৰ পৰিচয় নিৰ্ধাৰণ কৰিবলৈ অনুমতি দিয়ে৷"</string>
+ <string name="permdesc_readPhoneState" msgid="7229063553502788058">"ডিভাইচত থকা ফ\'নৰ সুবিধাসমূহ ব্যৱহাৰ কৰিবলৈ এপ্টোক অনুমতি দিয়ে৷ এই অনুমতিয়ে কোনো কল সক্ৰিয় হৈ থাককেই বা নাথাকক আৰু দূৰবৰ্তী নম্বৰটো কলৰ দ্বাৰা সংযোজিত হওকেই বা নহওক এপ্টোক ফ\'ন নম্বৰ আৰু ডিভাইচৰ পৰিচয় নিৰ্ধাৰণ কৰিবলৈ অনুমতি দিয়ে৷"</string>
<string name="permlab_readBasicPhoneState" msgid="3214853233263871347">"প্ৰাথমিক টেলিফ\'নী স্থিতি আৰু পৰিচয় পঢ়ক"</string>
<string name="permdesc_readBasicPhoneState" msgid="828185691675460520">"এপ্টোক ডিভাইচটোৰ প্ৰাথমিক টেলিফ’নী সুবিধাসমূহ এক্সেছ কৰাৰ অনুমতি দিয়ে।"</string>
<string name="permlab_manageOwnCalls" msgid="9033349060307561370">"ছিষ্টেমৰ জৰিয়তে কল কৰিব পাৰে"</string>
- <string name="permdesc_manageOwnCalls" msgid="4431178362202142574">"কল কৰাৰ অভিজ্ঞতাক উন্নত কৰিবলৈ এপটোক ছিষ্টেমৰ জৰিয়তে কলসমূহ কৰিবলৈ দিয়ে।"</string>
+ <string name="permdesc_manageOwnCalls" msgid="4431178362202142574">"কল কৰাৰ অভিজ্ঞতাক উন্নত কৰিবলৈ এপ্টোক ছিষ্টেমৰ জৰিয়তে কলসমূহ কৰিবলৈ দিয়ে।"</string>
<string name="permlab_callCompanionApp" msgid="3654373653014126884">"ছিষ্টেমৰ জৰিয়তে কলবোৰ চোৱা আৰু নিয়ন্ত্ৰণ কৰা।"</string>
- <string name="permdesc_callCompanionApp" msgid="8474168926184156261">"এপটোক ডিভাইচত চলি থকা কল চাবলৈ আৰু নিয়ন্ত্ৰণ কৰিবলৈ অনুমতি দিয়ে। কলৰ সংখ্যা আৰু কলবোৰৰ স্থিতি ইয়াত অন্তৰ্ভুক্ত হয়।"</string>
+ <string name="permdesc_callCompanionApp" msgid="8474168926184156261">"এপ্টোক ডিভাইচত চলি থকা কল চাবলৈ আৰু নিয়ন্ত্ৰণ কৰিবলৈ অনুমতি দিয়ে। কলৰ সংখ্যা আৰু কলবোৰৰ স্থিতি ইয়াত অন্তৰ্ভুক্ত হয়।"</string>
<string name="permlab_exemptFromAudioRecordRestrictions" msgid="1164725468350759486">"অডিঅ’ ৰেকৰ্ড কৰাৰ প্ৰতিবন্ধকতাসমূহৰ পৰা ৰেহাই দিয়ক"</string>
<string name="permdesc_exemptFromAudioRecordRestrictions" msgid="2425117015896871976">"অডিঅ’ ৰেকৰ্ড কৰাৰ প্ৰতিবন্ধকতাসমূহৰ পৰা এপ্টোক ৰেহাই দিয়ক।"</string>
<string name="permlab_acceptHandover" msgid="2925523073573116523">"অইন এটা এপত আৰম্ভ হোৱা কল এটা অব্যাহত ৰাখিব পাৰে"</string>
- <string name="permdesc_acceptHandovers" msgid="7129026180128626870">"এপটোক এনে কল কৰিবলৈ দিয়ে যিটোৰ আৰম্ভণি অইন এটা এপত হৈছিল।"</string>
+ <string name="permdesc_acceptHandovers" msgid="7129026180128626870">"এপ্টোক এনে কল কৰিবলৈ দিয়ে যিটোৰ আৰম্ভণি অইন এটা এপত হৈছিল।"</string>
<string name="permlab_readPhoneNumbers" msgid="5668704794723365628">"ফ\'ন নম্বৰসমূহ পঢ়ে"</string>
- <string name="permdesc_readPhoneNumbers" msgid="7368652482818338871">"এপটোক ডিভাইচটোৰ ফ\'ন নম্বৰসমূহ চাবলৈ অনুমতি দিয়ে।"</string>
+ <string name="permdesc_readPhoneNumbers" msgid="7368652482818338871">"এপ্টোক ডিভাইচটোৰ ফ\'ন নম্বৰসমূহ চাবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_wakeLock" product="automotive" msgid="1904736682319375676">"গাড়ীৰ স্ক্রীনখন অন কৰি ৰখা"</string>
<string name="permlab_wakeLock" product="tablet" msgid="1527660973931694000">"টে\'বলেট সুপ্ত অৱস্থালৈ যোৱাত বাধা দিয়ক"</string>
<string name="permlab_wakeLock" product="tv" msgid="2856941418123343518">"আপোনাৰ Android TV ডিভাইচটো সুপ্ত অৱস্থালৈ যোৱাত বাধা দিয়ক"</string>
<string name="permlab_wakeLock" product="default" msgid="569409726861695115">"ফ\'ন সুপ্ত অৱস্থালৈ যোৱাত বাধা দিয়ক"</string>
<string name="permdesc_wakeLock" product="automotive" msgid="5995045369683254571">"এপ্টোক গাড়ীৰ স্ক্রীনখন অন কৰি ৰাখিবলৈ অনুমতি দিয়ে।"</string>
- <string name="permdesc_wakeLock" product="tablet" msgid="2441742939101526277">"টে\'বলেট সুপ্ত অৱস্থালৈ যোৱাৰ পৰা প্ৰতিৰোধ কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_wakeLock" product="tablet" msgid="2441742939101526277">"টে\'বলেট সুপ্ত অৱস্থালৈ যোৱাৰ পৰা প্ৰতিৰোধ কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permdesc_wakeLock" product="tv" msgid="2329298966735118796">"এপ্টোক আপোনাৰ Android TV ডিভাইচটো সুপ্ত অৱস্থালৈ যোৱাত বাধা দিবলৈ অনুমতি দিয়ে।"</string>
- <string name="permdesc_wakeLock" product="default" msgid="3689523792074007163">"ফ\'ন সুপ্ত অৱস্থালৈ যোৱাৰ পৰা প্ৰতিৰোধ কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_wakeLock" product="default" msgid="3689523792074007163">"ফ\'ন সুপ্ত অৱস্থালৈ যোৱাৰ পৰা প্ৰতিৰোধ কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_transmitIr" msgid="8077196086358004010">"ইনফ্ৰাৰেড ট্ৰান্সমিট কৰিব পাৰে"</string>
<string name="permdesc_transmitIr" product="tablet" msgid="5884738958581810253">"টে\'বলেটৰ ইনফ্ৰাৰেড ট্ৰান্সমিটাৰ ব্যৱহাৰ কৰিবলৈ এপক অনুমতি দিয়ে।"</string>
<string name="permdesc_transmitIr" product="tv" msgid="3278506969529173281">"এপ্টোক আপোনাৰ Android TV ডিভাইচৰ ইনফ্ৰাৰেড ট্ৰান্সমিটাৰ ব্যৱহাৰ কৰিবলৈ অনুমতি দিয়ে।"</string>
<string name="permdesc_transmitIr" product="default" msgid="8484193849295581808">"ফ\'নৰ ইনফ্ৰাৰেড ট্ৰান্সমিটাৰ ব্যৱহাৰ কৰিবলৈ এপক অনুমতি দিয়ে।"</string>
<string name="permlab_setWallpaper" msgid="6959514622698794511">"ৱালপেপাৰ ছেট কৰক"</string>
- <string name="permdesc_setWallpaper" msgid="2973996714129021397">"ছিষ্টেমৰ ৱালপেপাৰ ছেট কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_setWallpaper" msgid="2973996714129021397">"ছিষ্টেমৰ ৱালপেপাৰ ছেট কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_setWallpaperHints" msgid="1153485176642032714">"আপোনাৰ ৱালপেপাৰৰ আকাৰ মিলাওক"</string>
- <string name="permdesc_setWallpaperHints" msgid="6257053376990044668">"ছিষ্টেমৰ ৱালপেপাৰৰ আকাৰ হিণ্ট ছেট কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_setWallpaperHints" msgid="6257053376990044668">"ছিষ্টেমৰ ৱালপেপাৰৰ আকাৰ হিণ্ট ছেট কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_setTimeZone" msgid="7922618798611542432">"সময় মণ্ডল ছেট কৰক"</string>
- <string name="permdesc_setTimeZone" product="tablet" msgid="1788868809638682503">"টে\'বলেটৰ সময় মণ্ডল সলনি কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_setTimeZone" product="tablet" msgid="1788868809638682503">"টে\'বলেটৰ সময় মণ্ডল সলনি কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permdesc_setTimeZone" product="tv" msgid="9069045914174455938">"এপ্টোক আপোনাৰ Android TV ডিভাইচটোৰ সময় মণ্ডল সলনি কৰিবলৈ অনুমতি দিয়ে।"</string>
- <string name="permdesc_setTimeZone" product="default" msgid="4611828585759488256">"ফ\'নৰ সময় মণ্ডল সলনি কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_setTimeZone" product="default" msgid="4611828585759488256">"ফ\'নৰ সময় মণ্ডল সলনি কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_getAccounts" msgid="5304317160463582791">"ডিভাইচত একাউণ্টবোৰ বিচাৰক"</string>
- <string name="permdesc_getAccounts" product="tablet" msgid="1784452755887604512">"এপটোক টেবলেটটোৰ জ্ঞাত একাউণ্টসমূহৰ সূচীখন পাবলৈ অনুমতি দিয়ে৷ এইটোৱে আপুনি ইনষ্টল কৰি ৰখা এপ্লিকেশ্বনসমূহে সৃষ্টি কৰা যিকোনো একাউণ্টক অন্তৰ্ভুক্ত কৰিব পাৰে৷"</string>
+ <string name="permdesc_getAccounts" product="tablet" msgid="1784452755887604512">"এপ্টোক টেবলেটটোৰ জ্ঞাত একাউণ্টসমূহৰ সূচীখন পাবলৈ অনুমতি দিয়ে৷ এইটোৱে আপুনি ইনষ্টল কৰি ৰখা এপ্লিকেশ্বনসমূহে সৃষ্টি কৰা যিকোনো একাউণ্টক অন্তৰ্ভুক্ত কৰিব পাৰে৷"</string>
<string name="permdesc_getAccounts" product="tv" msgid="437604680436540822">"আপোনাৰ Android TV ডিভাইচটোৰ পৰিচিত একাউণ্টসমূহৰ সূচীখন পাবলৈ অনুমতি দিয়ে। আপুনি ইনষ্টল কৰি ৰখা এপ্লিকেশ্বনসমূহে সৃষ্টি কৰা যিকোনো একাউণ্ট অন্তৰ্ভুক্ত হ’ব পাৰে।"</string>
- <string name="permdesc_getAccounts" product="default" msgid="2491273043569751867">"এপটোক ফ\'নটোৰ জ্ঞাত একাউণ্টসমূহৰ সূচীখন পাবলৈ অনুমতি দিয়ে৷ এইটোৱে আপুনি ইনষ্টল কৰি ৰখা এপ্লিকেশ্বনসমূহে সৃষ্টি কৰা যিকোনো একাউণ্টক অন্তৰ্ভুক্ত কৰিব পাৰে৷"</string>
+ <string name="permdesc_getAccounts" product="default" msgid="2491273043569751867">"এপ্টোক ফ\'নটোৰ জ্ঞাত একাউণ্টসমূহৰ সূচীখন পাবলৈ অনুমতি দিয়ে৷ এইটোৱে আপুনি ইনষ্টল কৰি ৰখা এপ্লিকেশ্বনসমূহে সৃষ্টি কৰা যিকোনো একাউণ্টক অন্তৰ্ভুক্ত কৰিব পাৰে৷"</string>
<string name="permlab_accessNetworkState" msgid="2349126720783633918">"নেটৱৰ্কৰ সংযোগবোৰ চাওক"</string>
- <string name="permdesc_accessNetworkState" msgid="4394564702881662849">"মজুত থকা আৰু সংযোগ হৈ থকা নেটৱৰ্ক সংযোগসমূহৰ বিষয়ে তথ্য চাবলৈ এপটোক অনুমতি দিয়ে৷"</string>
+ <string name="permdesc_accessNetworkState" msgid="4394564702881662849">"মজুত থকা আৰু সংযোগ হৈ থকা নেটৱৰ্ক সংযোগসমূহৰ বিষয়ে তথ্য চাবলৈ এপ্টোক অনুমতি দিয়ে৷"</string>
<string name="permlab_createNetworkSockets" msgid="3224420491603590541">"সম্পূর্ণ নেটৱর্কৰ সুবিধা লাভ কৰিব পাৰে"</string>
- <string name="permdesc_createNetworkSockets" msgid="7722020828749535988">"এপটোক নেটৱৰ্ক ছ\'কেটবোৰ সৃষ্টি কৰিবলৈ আৰু কাষ্টম নেটৱৰ্ক প্ৰ\'ট\'কল ব্যৱহাৰ কৰিবলৈ অনুমতি দিয়ে৷ ব্ৰাউজাৰ আৰু অন্য এপ্লিকেশ্বনসমূহে ইণ্টাৰনেটলৈ ডেটা পঠিওৱা মাধ্য়ম প্ৰদান কৰে, গতিকে ইণ্টাৰনেটলৈ ডেটা পঠিয়াবলৈ এই অনুমতিৰ প্ৰয়োজন নাই৷"</string>
+ <string name="permdesc_createNetworkSockets" msgid="7722020828749535988">"এপ্টোক নেটৱৰ্ক ছ\'কেটবোৰ সৃষ্টি কৰিবলৈ আৰু কাষ্টম নেটৱৰ্ক প্ৰ\'ট\'কল ব্যৱহাৰ কৰিবলৈ অনুমতি দিয়ে৷ ব্ৰাউজাৰ আৰু অন্য এপ্লিকেশ্বনসমূহে ইণ্টাৰনেটলৈ ডেটা পঠিওৱা মাধ্য়ম প্ৰদান কৰে, গতিকে ইণ্টাৰনেটলৈ ডেটা পঠিয়াবলৈ এই অনুমতিৰ প্ৰয়োজন নাই৷"</string>
<string name="permlab_changeNetworkState" msgid="8945711637530425586">"নেটৱৰ্কৰ সংযোগ সলনি কৰক"</string>
- <string name="permdesc_changeNetworkState" msgid="649341947816898736">"নেটৱৰ্ক সংযোগৰ অৱস্থাটো সলনি কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_changeNetworkState" msgid="649341947816898736">"নেটৱৰ্ক সংযোগৰ অৱস্থাটো সলনি কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_changeTetherState" msgid="9079611809931863861">"টেডাৰিং সংযোগ সলনি কৰক"</string>
- <string name="permdesc_changeTetherState" msgid="3025129606422533085">"টেডাৰ হৈ থকা ইণ্টাৰনেট সংযোগৰ অৱস্থা সলনি কৰিবলৈ এপটোক অনুমতি দিয়ে৷"</string>
+ <string name="permdesc_changeTetherState" msgid="3025129606422533085">"টেডাৰ হৈ থকা ইণ্টাৰনেট সংযোগৰ অৱস্থা সলনি কৰিবলৈ এপ্টোক অনুমতি দিয়ে৷"</string>
<string name="permlab_accessWifiState" msgid="5552488500317911052">"ৱাই-ফাইৰ সংযোগবোৰ চাওক"</string>
<string name="permdesc_accessWifiState" msgid="6913641669259483363">"ৱাই-ফাই সক্ষম কৰা হ’ল নে নাই আৰু সংযোগ হৈ থকা ৱাই-ফাই ডিভাইচসমূহৰ নামবোৰৰ দৰে ৱাই-ফাইৰ ইণ্টাৰনেট সম্পর্কীয় তথ্য চাবলৈ এপক অনুমতি দিয়ে।"</string>
<string name="permlab_changeWifiState" msgid="7947824109713181554">"ৱাই-ফাই সংযোগ কৰক আৰু ইয়াৰ সংযোগ বিচ্ছিন্ন কৰক"</string>
- <string name="permdesc_changeWifiState" msgid="7170350070554505384">"এপটোক ৱাই-ফাই এক্সেছ পইণ্টলৈ সংযোগ কৰিবলৈ আৰু তাৰ সংযোগ বিচ্ছিন্ন কৰিবলৈ আৰু ৱাই-ফাই নেটৱৰ্কসমূহৰ বাবে ডিভাইচ কনফিগাৰেশ্বনত সাল-সলনি কৰিবলৈ অনুমতি দিয়ে৷"</string>
+ <string name="permdesc_changeWifiState" msgid="7170350070554505384">"এপ্টোক ৱাই-ফাই এক্সেছ পইণ্টলৈ সংযোগ কৰিবলৈ আৰু তাৰ সংযোগ বিচ্ছিন্ন কৰিবলৈ আৰু ৱাই-ফাই নেটৱৰ্কসমূহৰ বাবে ডিভাইচ কনফিগাৰেশ্বনত সাল-সলনি কৰিবলৈ অনুমতি দিয়ে৷"</string>
<string name="permlab_changeWifiMulticastState" msgid="285626875870754696">"ৱাই-ফাই মাল্টিকাষ্ট প্ৰচাৰৰ অনুমতি দিয়ক"</string>
<string name="permdesc_changeWifiMulticastState" product="tablet" msgid="191079868596433554">"কেৱল আপোনাৰ টেবলেটটোৱেই নহয়, মাল্টিকাষ্ট ঠিকনা ব্যৱহাৰ কৰি এটা ৱাই-ফাই নেটৱর্কত থকা আটাইবোৰ ডিভাইচলৈ পঠিওৱা পেকেট লাভ কৰিবলৈ এপ্টোক অনুমতি দিয়ে। এই কার্যই নন মাল্টিকাষ্ট ম\'ডতকৈ অধিক বেটাৰী ব্যৱহাৰ কৰে।"</string>
<string name="permdesc_changeWifiMulticastState" product="tv" msgid="1336952358450652595">"কেৱল আপোনাৰ Android TV ডিভাইচটোৱেই নহয়, মাল্টিকাষ্ট ঠিকনাবোৰ ব্যৱহাৰ কৰি এটা ৱাই-ফাই নেটৱর্কত থকা আটাইবোৰ ডিভাইচলৈ পঠিওৱা পেকেট লাভ কৰিবলৈ এপ্টোক অনুমতি দিয়ে। এই কার্যই নন-মাল্টিকাষ্ট ম’ডতকৈ অধিক পাৱাৰ ব্যৱহাৰ কৰে।"</string>
@@ -529,15 +529,15 @@
<string name="permdesc_bluetoothAdmin" product="tv" msgid="1623992984547014588">"এপ্টোক আপোনাৰ Android TV ডিভাইচটোত ব্লুটুথ কনফিগাৰ কৰিবলৈ আৰু ৰিম’ট ডিভাইচসমূহ বিচাৰি উলিয়াবলৈ আৰু পেয়াৰ কৰিবলৈ অনুমতি দিয়ে।"</string>
<string name="permdesc_bluetoothAdmin" product="default" msgid="7381341743021234863">"স্থানীয় ব্লুটুথ ফ’ন কনফিগাৰ কৰিবলৈ আৰু দূৰৱৰ্তী ডিভাইচসমূহৰ সৈতে পেয়াৰ কৰিবলৈ আৰু বিচাৰি উলিয়াবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_accessWimaxState" msgid="7029563339012437434">"WiMAXৰ লগত সংযোগ কৰক আৰু ইয়াৰ পৰা সংযোগ বিচ্ছিন্ন কৰক"</string>
- <string name="permdesc_accessWimaxState" msgid="5372734776802067708">"WiMAX সক্ষম হৈ আছেনে নাই আৰু সংযোজিত যিকোনো WiMAX নেটৱৰ্কৰ বিষয়ে তথ্য নিৰ্ধাৰণ কৰিবলৈ এপটোক অনুমতি দিয়ে৷"</string>
+ <string name="permdesc_accessWimaxState" msgid="5372734776802067708">"WiMAX সক্ষম হৈ আছেনে নাই আৰু সংযোজিত যিকোনো WiMAX নেটৱৰ্কৰ বিষয়ে তথ্য নিৰ্ধাৰণ কৰিবলৈ এপ্টোক অনুমতি দিয়ে৷"</string>
<string name="permlab_changeWimaxState" msgid="6223305780806267462">"WiMAXৰ স্থিতি সলনি কৰক"</string>
- <string name="permdesc_changeWimaxState" product="tablet" msgid="4011097664859480108">"এপটোক টেবলেটলৈ সংযোগ কৰিবলৈ আৰু WiMAX নেটৱৰ্কসমূহৰ পৰা টেবলেটৰ সংযোগ বিচ্ছিন্ন কৰিবলৈ অনুমতি দিয়ে৷"</string>
+ <string name="permdesc_changeWimaxState" product="tablet" msgid="4011097664859480108">"এপ্টোক টেবলেটলৈ সংযোগ কৰিবলৈ আৰু WiMAX নেটৱৰ্কসমূহৰ পৰা টেবলেটৰ সংযোগ বিচ্ছিন্ন কৰিবলৈ অনুমতি দিয়ে৷"</string>
<string name="permdesc_changeWimaxState" product="tv" msgid="5373274458799425276">"এপ্টোক আপোনাৰ Android TV ডিভাইচৰ সৈতে সংযোগ কৰিবলৈ আৰু WiMAX নেটৱৰ্কসমূহৰ পৰা আপোনাৰ Android TV ডিভাইচৰ সংযোগ বিচ্ছিন্ন কৰিবলৈ অনুমতি দিয়ে।"</string>
- <string name="permdesc_changeWimaxState" product="default" msgid="1551666203780202101">"এপটোক ফ\'নলৈ সংযোগ কৰিবলৈ আৰু WiMAX নেটৱৰ্কসমূহৰ পৰা ফ\'নৰ সংযোগ বিচ্ছিন্ন কৰিবলৈ অনুমতি দিয়ে৷"</string>
+ <string name="permdesc_changeWimaxState" product="default" msgid="1551666203780202101">"এপ্টোক ফ\'নলৈ সংযোগ কৰিবলৈ আৰু WiMAX নেটৱৰ্কসমূহৰ পৰা ফ\'নৰ সংযোগ বিচ্ছিন্ন কৰিবলৈ অনুমতি দিয়ে৷"</string>
<string name="permlab_bluetooth" msgid="586333280736937209">"ব্লুটুথ ডিভাইচবোৰৰ সৈতে পেয়াৰ কৰক"</string>
- <string name="permdesc_bluetooth" product="tablet" msgid="3053222571491402635">"টেবলেটত ব্লুটুথৰ কনফিগাৰেশ্বন চাবলৈ আৰু যোৰা লগোৱা ডিভাইচসমূহৰ জৰিয়তে সংযোগ কৰিবলৈ আৰু সংযোগৰ অনুৰোধ স্বীকাৰ কৰিবলৈ এপটোক অনুমতি দিয়ে৷"</string>
+ <string name="permdesc_bluetooth" product="tablet" msgid="3053222571491402635">"টেবলেটত ব্লুটুথৰ কনফিগাৰেশ্বন চাবলৈ আৰু যোৰা লগোৱা ডিভাইচসমূহৰ জৰিয়তে সংযোগ কৰিবলৈ আৰু সংযোগৰ অনুৰোধ স্বীকাৰ কৰিবলৈ এপ্টোক অনুমতি দিয়ে৷"</string>
<string name="permdesc_bluetooth" product="tv" msgid="8851534496561034998">"এপ্টোক আপোনাৰ Android TV ডিভাইচটোত ব্লুটুথৰ কনফিগাৰেশ্বন চাবলৈ আৰু পেয়াৰ কৰি থোৱা ডিভাইচসমূহৰ সৈতে সংযোগ কৰিবলৈ আৰু গ্ৰহণ কৰিবলৈ অনুমতি দিয়ে।"</string>
- <string name="permdesc_bluetooth" product="default" msgid="2779606714091276746">"ফ\'নটোত ব্লুটুথৰ কনফিগাৰেশ্বন চাবলৈ আৰু যোৰা লগোৱা ডিভাইচসমূহৰ জৰিয়তে সংযোগ কৰিবলৈ আৰু সংযোগৰ অনুৰোধ স্বীকাৰ কৰিবলৈ এপটোক অনুমতি দিয়ে৷"</string>
+ <string name="permdesc_bluetooth" product="default" msgid="2779606714091276746">"ফ\'নটোত ব্লুটুথৰ কনফিগাৰেশ্বন চাবলৈ আৰু যোৰা লগোৱা ডিভাইচসমূহৰ জৰিয়তে সংযোগ কৰিবলৈ আৰু সংযোগৰ অনুৰোধ স্বীকাৰ কৰিবলৈ এপ্টোক অনুমতি দিয়ে৷"</string>
<string name="permlab_bluetooth_scan" msgid="5402587142833124594">"নিকটৱৰ্তী ব্লুটুথ ডিভাইচ বিচাৰক আৰু তাৰ সৈতে পেয়াৰ কৰক"</string>
<string name="permdesc_bluetooth_scan" product="default" msgid="6540723536925289276">"এপ্টোক নিকটৱৰ্তী ব্লুটুথ ডিভাইচ বিচাৰি উলিয়াবলৈ আৰু সেইসমূহৰ সৈতে পেয়াৰ কৰিবলৈ অনুমতি দিয়ে"</string>
<string name="permlab_bluetooth_connect" msgid="6657463246355003528">"পেয়াৰ কৰা ব্লুটুথ ডিভাইচৰ সৈতে সংযোগ কৰক"</string>
@@ -551,9 +551,9 @@
<string name="permlab_preferredPaymentInfo" msgid="5274423844767445054">"অগ্ৰাধিকাৰ দিয়া NFC পৰিশোধ সেৱাৰ তথ্য"</string>
<string name="permdesc_preferredPaymentInfo" msgid="8583552469807294967">"এপ্টোক অগ্ৰাধিকাৰ দিয়া nfc পৰিশোধ সেৱাৰ পঞ্জীকৃত সহায়কসমূহ আৰু পৰিশোধ কৰিব লগা লক্ষ্যস্থান দৰে তথ্য পাবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_nfc" msgid="1904455246837674977">"নিয়েৰ ফিল্ড কমিউনিকেশ্বন নিয়ন্ত্ৰণ কৰক"</string>
- <string name="permdesc_nfc" msgid="8352737680695296741">"এপটোক নিয়েৰ ফিল্ড কমিউনিকেশ্বন (NFC) টেগ, কাৰ্ড আৰু ৰিডাৰসমূহৰ সৈতে যোগাযোগ কৰিবলৈ অনুমতি দিয়ে।"</string>
+ <string name="permdesc_nfc" msgid="8352737680695296741">"এপ্টোক নিয়েৰ ফিল্ড কমিউনিকেশ্বন (NFC) টেগ, কাৰ্ড আৰু ৰিডাৰসমূহৰ সৈতে যোগাযোগ কৰিবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_disableKeyguard" msgid="3605253559020928505">"আপোনাৰ স্ক্ৰীন লক অক্ষম কৰক"</string>
- <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"এপটোক কী ল\'ক আৰু জড়িত হোৱা যিকোনো পাছৱৰ্ডৰ সুৰক্ষা অক্ষম কৰিব দিয়ে৷ উদাহৰণস্বৰূপে, কোনো অন্তৰ্গামী ফ\'ন কল উঠোৱাৰ সময়ত ফ\'নটোৱে কী-লকটো অক্ষম কৰে, তাৰ পিছত কল শেষ হ\'লেই কী লকটো পুনৰ সক্ষম কৰে৷"</string>
+ <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"এপ্টোক কী ল\'ক আৰু জড়িত হোৱা যিকোনো পাছৱৰ্ডৰ সুৰক্ষা অক্ষম কৰিব দিয়ে৷ উদাহৰণস্বৰূপে, কোনো অন্তৰ্গামী ফ\'ন কল উঠোৱাৰ সময়ত ফ\'নটোৱে কী-লকটো অক্ষম কৰে, তাৰ পাছত কল শেষ হ\'লেই কী লকটো পুনৰ সক্ষম কৰে৷"</string>
<string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"স্ক্ৰীন লকৰ জটিলতাৰ অনুৰোধ"</string>
<string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"এপ্টোক স্ক্ৰীন লকৰ জটিলতাৰ স্তৰ (উচ্চ, মধ্যম, নিম্ন বা একেবাৰে নাই)ৰ বিষয়ে জানিবলৈ অনুমতি দিয়ে, যিয়ে স্ক্ৰীন লকৰ সম্ভাব্য দৈৰ্ঘ্য বা স্ক্ৰীন লকৰ প্ৰকাৰ দৰ্শায়। লগতে এপ্টোৱে ব্যৱহাৰকাৰীক স্ক্ৰীন লকটো এটা নিৰ্দিষ্ট স্তৰলৈ আপডে’ট কৰিবলৈ পৰামৰ্শ দিব পাৰে যিটো ব্যৱহাৰকাৰীয়ে অৱজ্ঞা কৰি পৰৱর্তী পৃষ্ঠালৈ যাব পাৰে। মনত ৰাখিব যে স্ক্ৰীন লকটো সাধাৰণ পাঠ হিচাপে ষ্ট\'ৰ কৰা নহয়; সেয়েহে, এপ্টোৱে সঠিক পাছৱৰ্ডটো জানিব নোৱাৰে।"</string>
<string name="permlab_postNotification" msgid="4875401198597803658">"জাননী দেখুৱাওক"</string>
@@ -561,9 +561,9 @@
<string name="permlab_useBiometric" msgid="6314741124749633786">"বায়োমেট্ৰিক হাৰ্ডৱেৰ ব্যৱহাৰ কৰক"</string>
<string name="permdesc_useBiometric" msgid="7502858732677143410">"বিশ্বাসযোগ্য়তা প্ৰমাণীকৰণৰ বাবে এপক বায়োমেট্ৰিক হাৰ্ডৱেৰ ব্যৱহাৰ কৰিবলৈ অনুমতি দিয়ে"</string>
<string name="permlab_manageFingerprint" msgid="7432667156322821178">"ফিংগাৰপ্ৰিণ্ট হাৰ্ডৱেৰ পৰিচালনা কৰিব পাৰে"</string>
- <string name="permdesc_manageFingerprint" msgid="2025616816437339865">"ফিংগাৰপ্ৰিণ্ট টেম্প্লেটসমূহ যোগ কৰা বা মচাৰ পদ্ধতিসমূহ কামত লগাবলৈ নিৰ্দেশ দিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_manageFingerprint" msgid="2025616816437339865">"ফিংগাৰপ্ৰিণ্ট টেম্প্লেটসমূহ যোগ কৰা বা মচাৰ পদ্ধতিসমূহ কামত লগাবলৈ নিৰ্দেশ দিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_useFingerprint" msgid="1001421069766751922">"ফিংগাৰপ্ৰিণ্ট হাৰ্ডৱেৰ ব্যৱহাৰ কৰিব পাৰে"</string>
- <string name="permdesc_useFingerprint" msgid="412463055059323742">"প্ৰমাণীকৰণৰ বাবে ফিংগাৰপ্ৰিণ্ট হাৰ্ডৱেৰ ব্যৱহাৰ কৰিবলৈ এপটোক অনুমতি দিয়ে"</string>
+ <string name="permdesc_useFingerprint" msgid="412463055059323742">"প্ৰমাণীকৰণৰ বাবে ফিংগাৰপ্ৰিণ্ট হাৰ্ডৱেৰ ব্যৱহাৰ কৰিবলৈ এপ্টোক অনুমতি দিয়ে"</string>
<string name="permlab_audioWrite" msgid="8501705294265669405">"আপোনাৰ সংগীত সংগ্ৰহ সালসলনি কৰিবলৈ"</string>
<string name="permdesc_audioWrite" msgid="8057399517013412431">"এপক আপোনাৰ সংগীত সংগ্ৰহ সালসলনি কৰিবলৈ দিয়ে।"</string>
<string name="permlab_videoWrite" msgid="5940738769586451318">"আপোনাৰ ভিডিঅ’ সংগ্ৰহ সালসলনি কৰিবলৈ"</string>
@@ -689,7 +689,7 @@
<string name="permlab_readSyncSettings" msgid="6250532864893156277">"ছিংকৰ ছেটিং পঢ়ক"</string>
<string name="permdesc_readSyncSettings" msgid="1325658466358779298">"একাউণ্টৰ ছিংক ছেটিংবোৰ পঢ়িবলৈ এপক অনুমতি দিয়ে। যেনে, People এপ্টো কোনো একাউণ্টৰ সৈতে ছিংক কৰা হৈছে নে নাই সেয়া নির্ধাৰণ কৰিব পাৰে।"</string>
<string name="permlab_writeSyncSettings" msgid="6583154300780427399">"ছিংকক অন আৰু অফ ট\'গল কৰক"</string>
- <string name="permdesc_writeSyncSettings" msgid="6029151549667182687">"এপটোক কোনো একাউণ্টৰ ছিংক সম্পৰ্কীয় ছেটিংসমূহ সংশোধন কৰিবলৈ অনুমতি দিয়ে৷ উদাহৰণস্বৰূপে, এই কাৰ্যক কোনো একাউণ্টৰ জৰিয়তে People এপটোৰ ছিংক সক্ষম কৰিবলৈ ব্যৱহাৰ কৰিব পাৰি৷"</string>
+ <string name="permdesc_writeSyncSettings" msgid="6029151549667182687">"এপ্টোক কোনো একাউণ্টৰ ছিংক সম্পৰ্কীয় ছেটিংসমূহ সংশোধন কৰিবলৈ অনুমতি দিয়ে৷ উদাহৰণস্বৰূপে, এই কাৰ্যক কোনো একাউণ্টৰ জৰিয়তে People এপ্টোৰ ছিংক সক্ষম কৰিবলৈ ব্যৱহাৰ কৰিব পাৰি৷"</string>
<string name="permlab_readSyncStats" msgid="3747407238320105332">"ছিংকৰ পৰিসংখ্যা পঢ়ক"</string>
<string name="permdesc_readSyncStats" msgid="3867809926567379434">"ছিংকৰ কাৰ্যক্ৰমসমূহৰ ইতিহাস আৰু ছিংক কৰা ডেটাৰ পৰিমাণসহ কোনো একাউণ্টৰ ছিংকৰ তথ্য পঢ়িবলৈ এপক অনুমতি দিয়ে।"</string>
<string name="permlab_sdcardRead" msgid="5791467020950064920">"আপোনাৰ শ্বেয়াৰ কৰি ৰখা ষ্ট’ৰেজৰ সমল পঢ়িব পাৰে"</string>
@@ -703,23 +703,23 @@
<string name="permlab_sdcardWrite" msgid="4863021819671416668">"আপোনাৰ শ্বেয়াৰ কৰি ৰখা ষ্ট’ৰেজৰ সমল সংশোধন কৰিব বা মচিব পাৰে"</string>
<string name="permdesc_sdcardWrite" msgid="8376047679331387102">"আপোনাৰ শ্বেয়াৰ কৰি ৰখা ষ্ট’ৰেজৰ সমল লিখিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_use_sip" msgid="8250774565189337477">"SIP কল কৰা/পোৱা"</string>
- <string name="permdesc_use_sip" msgid="3590270893253204451">"এপটোক SIP কলসমূহ কৰিবলৈ আৰু পাবলৈ অনুমতি দিয়ে।"</string>
+ <string name="permdesc_use_sip" msgid="3590270893253204451">"এপ্টোক SIP কলসমূহ কৰিবলৈ আৰু পাবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_register_sim_subscription" msgid="1653054249287576161">"নতুন টেলিকম ছিম সংযোগসমূহ পঞ্জীয়ন কৰা"</string>
- <string name="permdesc_register_sim_subscription" msgid="4183858662792232464">"এপটোক নতুন টেলিকম সংযোগ পঞ্জীয়ন কৰিবলৈ অনুমতি দিয়ে।"</string>
+ <string name="permdesc_register_sim_subscription" msgid="4183858662792232464">"এপ্টোক নতুন টেলিকম সংযোগ পঞ্জীয়ন কৰিবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_register_call_provider" msgid="6135073566140050702">"নতুন টেলিকম সংযোগসমূহ পঞ্জীয়ন কৰা"</string>
- <string name="permdesc_register_call_provider" msgid="4201429251459068613">"এপটোক নতুন টেলিকম সংযোগ পঞ্জীয়ন কৰিবলৈ অনুমতি দিয়ে।"</string>
+ <string name="permdesc_register_call_provider" msgid="4201429251459068613">"এপ্টোক নতুন টেলিকম সংযোগ পঞ্জীয়ন কৰিবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_connection_manager" msgid="3179365584691166915">"টেলিকম সংযোগ পৰিচালনা কৰা"</string>
- <string name="permdesc_connection_manager" msgid="1426093604238937733">"এপটোক টেলিকম সংযোগ পৰিচালনা কৰিবলৈ অনুমতি দিয়ে।"</string>
+ <string name="permdesc_connection_manager" msgid="1426093604238937733">"এপ্টোক টেলিকম সংযোগ পৰিচালনা কৰিবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_bind_incall_service" msgid="5990625112603493016">"ইন-কল স্ক্ৰীনৰ সৈতে সংযোগ স্থাপন"</string>
<string name="permdesc_bind_incall_service" msgid="4124917526967765162">"ব্যৱহাৰকাৰীগৰাকীয়ে কেতিয়া আৰু কেনেদৰে ইন-কল-স্ক্ৰীন চায় সেয়া নিয়ন্ত্ৰণ কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_bind_connection_service" msgid="5409268245525024736">"টেলিফ\'নী সেৱাসমূহৰ সৈতে সংযোগ স্থাপন"</string>
<string name="permdesc_bind_connection_service" msgid="6261796725253264518">"কল কৰিবলৈ/লাভ কৰিবলৈ টেলিফ\'নী সেৱাসমূহৰ সৈতে এপক সংযোগ স্থাপনৰ বাবে অনুমতি দিয়ে।"</string>
<string name="permlab_control_incall_experience" msgid="6436863486094352987">"ইন-কল ব্যৱহাৰকাৰীৰ অভিজ্ঞতা প্ৰদান কৰা"</string>
- <string name="permdesc_control_incall_experience" msgid="5896723643771737534">"এপটোক ইন-কল ব্যৱহাৰকাৰীৰ অভিজ্ঞতা প্ৰদান কৰিবলৈ অনুমতি দিয়ে।"</string>
+ <string name="permdesc_control_incall_experience" msgid="5896723643771737534">"এপ্টোক ইন-কল ব্যৱহাৰকাৰীৰ অভিজ্ঞতা প্ৰদান কৰিবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_readNetworkUsageHistory" msgid="8470402862501573795">"নেটৱর্কৰ পূৰ্বতে হোৱা ব্যৱহাৰৰ বিষয়ে পঢ়ক"</string>
- <string name="permdesc_readNetworkUsageHistory" msgid="1112962304941637102">"এপটোক বিশেষ নেটৱৰ্কবিলাকৰ আৰু এপ্সমূহৰ নেটৱৰ্ক ব্যৱহাৰৰ ইতিহাস পঢ়িবলৈ অনুমতি দিয়ে।"</string>
+ <string name="permdesc_readNetworkUsageHistory" msgid="1112962304941637102">"এপ্টোক বিশেষ নেটৱৰ্কবিলাকৰ আৰু এপ্সমূহৰ নেটৱৰ্ক ব্যৱহাৰৰ ইতিহাস পঢ়িবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_manageNetworkPolicy" msgid="6872549423152175378">"নেটৱর্কৰ নীতি পৰিচালনা কৰক"</string>
- <string name="permdesc_manageNetworkPolicy" msgid="1865663268764673296">"এপটোক নেটৱৰ্ক সংযোগৰ নীতিসমূহ পৰিচালনা কৰিবলৈ আৰু এপ্-বিশেষ নিয়ম সংজ্ঞাবদ্ধ কৰিবলৈ অনুমতি দিয়ে।"</string>
+ <string name="permdesc_manageNetworkPolicy" msgid="1865663268764673296">"এপ্টোক নেটৱৰ্ক সংযোগৰ নীতিসমূহ পৰিচালনা কৰিবলৈ আৰু এপ্-বিশেষ নিয়ম সংজ্ঞাবদ্ধ কৰিবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_modifyNetworkAccounting" msgid="7448790834938749041">"নেটৱর্ক ব্যৱহাৰৰ হিচাপ সলনি কৰক"</string>
<string name="permdesc_modifyNetworkAccounting" msgid="5076042642247205390">"এপ অনুসুৰি নেটৱর্কৰ ব্যৱহাৰৰ হিচাপ সংশোধন কৰিবলৈ এপক অনুমতি দিয়ে। এয়া সাধাৰণ এপবোৰৰ ব্যৱহাৰৰ বাবে নহয়।"</string>
<string name="permlab_accessNotifications" msgid="7130360248191984741">"জাননীসমূহ এক্সেছ কৰে"</string>
@@ -747,7 +747,7 @@
<string name="permlab_bindCarrierServices" msgid="2395596978626237474">"বাহক সেৱাসমূহৰ সৈতে সংযুক্ত হ\'ব পাৰে"</string>
<string name="permdesc_bindCarrierServices" msgid="9185614481967262900">"বাহক সেৱাৰ সৈতে সংযুক্ত হ\'বলৈ ধাৰকক অনুমতি দিয়ে। সাধাৰণ এপসমূহৰ বাবে সাধাৰণতে প্ৰয়োজন হ\'ব নালাগে।"</string>
<string name="permlab_access_notification_policy" msgid="5524112842876975537">"অসুবিধা নিদিব চাব পাৰে"</string>
- <string name="permdesc_access_notification_policy" msgid="8538374112403845013">"অসুবিধা নিদিবৰ কনফিগাৰেশ্বনক পঢ়িবলৈ আৰু সালসলনি কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_access_notification_policy" msgid="8538374112403845013">"অসুবিধা নিদিবৰ কনফিগাৰেশ্বনক পঢ়িবলৈ আৰু সালসলনি কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_startViewPermissionUsage" msgid="1504564328641112341">"চোৱাৰ অনুমতিৰ ব্যৱহাৰ আৰম্ভ কৰক"</string>
<string name="permdesc_startViewPermissionUsage" msgid="2820325605959586538">"ধাৰকক কোনো এপৰ বাবে অনুমতিৰ ব্যৱহাৰ আৰম্ভ কৰিবলৈ দিয়ে। সাধাৰণ এপ্সমূহৰ বাবে কেতিয়াও প্ৰয়োজন হ’ব নালাগে।"</string>
<string name="permlab_startReviewPermissionDecisions" msgid="8690578688476599284">"অনুমতিৰ সিদ্ধান্তসমূহ চোৱা আৰম্ভ কৰক"</string>
@@ -914,7 +914,7 @@
<string name="keyguard_password_enter_password_code" msgid="2751130557661643482">"আনলক কৰিবলৈ পাছৱৰ্ড লিখক"</string>
<string name="keyguard_password_enter_pin_password_code" msgid="7792964196473964340">"আনলক কৰিবলৈ পিন লিখক"</string>
<string name="keyguard_password_wrong_pin_code" msgid="8583732939138432793">"ভুল পিন ক\'ড।"</string>
- <string name="keyguard_label_text" msgid="3841953694564168384">"আনলক কৰিবলৈ মেনু টিপাৰ পিছত ০ টিপক।"</string>
+ <string name="keyguard_label_text" msgid="3841953694564168384">"আনলক কৰিবলৈ মেনু টিপাৰ পাছত ০ টিপক।"</string>
<string name="emergency_call_dialog_number_for_display" msgid="2978165477085612673">"জৰুৰীকালীন নম্বৰ"</string>
<string name="lockscreen_carrier_default" msgid="6192313772955399160">"কোনো সেৱা নাই"</string>
<string name="lockscreen_screen_locked" msgid="7364905540516041817">"স্ক্ৰীন লক কৰা হ’ল।"</string>
@@ -949,12 +949,12 @@
<string name="lockscreen_sim_puk_locked_instructions" msgid="5307979043730860995">"ব্যৱহাৰকাৰীৰ নিৰ্দেশনা চাওক বা গ্ৰাহক সেৱা কেন্দ্ৰৰ সৈতে যোগাযোগ কৰক।"</string>
<string name="lockscreen_sim_locked_message" msgid="3160196135801185938">"ছিম কাৰ্ড লক কৰা হৈছে।"</string>
<string name="lockscreen_sim_unlock_progress_dialog_message" msgid="2286497117428409709">"ছিম কার্ড আনলক কৰি থকা হৈছে…"</string>
- <string name="lockscreen_too_many_failed_attempts_dialog_message" msgid="6458790975898594240">"আপুনি অশুদ্ধভাৱে আপোনাৰ আনলক আৰ্হি <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ আঁকিছে। \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> ছেকেণ্ডৰ পিছত পুনৰ চেষ্টা কৰক।"</string>
- <string name="lockscreen_too_many_failed_password_attempts_dialog_message" msgid="3118353451602377380">"আপুনি অশুদ্ধভাৱে আপোনাৰ পাছৱৰ্ড <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ছেকেণ্ডৰ পিছত পুনৰ চেষ্টা কৰক।"</string>
- <string name="lockscreen_too_many_failed_pin_attempts_dialog_message" msgid="2874278239714821984">"আপুনি অশুদ্ধভাৱে আপোনাৰ পিন <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ছেকেণ্ডৰ পিছত পুনৰ চেষ্টা কৰক।"</string>
- <string name="lockscreen_failed_attempts_almost_glogin" product="tablet" msgid="3069635524964070596">"আপুনি অশুদ্ধভাৱে আপোনাৰ লক খোলাৰ আৰ্হিটো <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ আঁকিলে৷ <xliff:g id="NUMBER_1">%2$d</xliff:g> তকৈ অধিকবাৰ অসফলভাৱে কৰা প্ৰয়াসৰ পিছত, আপোনাৰ ফ\'নটো আনলক কৰিবৰ বাবে Google ছাইন ইনৰ জৰিয়তে কাৰ্যটো কৰিবলৈ কোৱা হ\'ব৷\n\n <xliff:g id="NUMBER_2">%3$d</xliff:g> ছেকেণ্ডৰ পিছত পুনৰ চেষ্টা কৰক৷"</string>
+ <string name="lockscreen_too_many_failed_attempts_dialog_message" msgid="6458790975898594240">"আপুনি অশুদ্ধভাৱে আপোনাৰ আনলক আৰ্হি <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ আঁকিছে। \n\n <xliff:g id="NUMBER_1">%2$d</xliff:g> ছেকেণ্ডৰ পাছত পুনৰ চেষ্টা কৰক।"</string>
+ <string name="lockscreen_too_many_failed_password_attempts_dialog_message" msgid="3118353451602377380">"আপুনি অশুদ্ধভাৱে আপোনাৰ পাছৱৰ্ড <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ছেকেণ্ডৰ পাছত পুনৰ চেষ্টা কৰক।"</string>
+ <string name="lockscreen_too_many_failed_pin_attempts_dialog_message" msgid="2874278239714821984">"আপুনি অশুদ্ধভাৱে আপোনাৰ পিন <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ছেকেণ্ডৰ পাছত পুনৰ চেষ্টা কৰক।"</string>
+ <string name="lockscreen_failed_attempts_almost_glogin" product="tablet" msgid="3069635524964070596">"আপুনি অশুদ্ধভাৱে আপোনাৰ লক খোলাৰ আৰ্হিটো <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ আঁকিলে৷ <xliff:g id="NUMBER_1">%2$d</xliff:g> তকৈ অধিকবাৰ অসফলভাৱে কৰা প্ৰয়াসৰ পাছত, আপোনাৰ ফ\'নটো আনলক কৰিবৰ বাবে Google ছাইন ইনৰ জৰিয়তে কাৰ্যটো কৰিবলৈ কোৱা হ\'ব৷\n\n <xliff:g id="NUMBER_2">%3$d</xliff:g> ছেকেণ্ডৰ পাছত পুনৰ চেষ্টা কৰক৷"</string>
<string name="lockscreen_failed_attempts_almost_glogin" product="tv" msgid="6399092175942158529">"আপুনি নিজৰ আনলক আৰ্হিটো <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ দিলে। আকৌ <xliff:g id="NUMBER_1">%2$d</xliff:g>বাৰ ভুলকৈ প্ৰয়াস কৰাৰ পাছত আপোনাক নিজৰ Google ছাইন ইন ব্যৱহাৰ কৰি আপোনাৰ Android TV ডিভাইচটো আনলক কৰিবলৈ কোৱা হ’ব।\n\n <xliff:g id="NUMBER_2">%3$d</xliff:g>ছেকেণ্ডৰ পাছত পুনৰ চেষ্টা কৰক।"</string>
- <string name="lockscreen_failed_attempts_almost_glogin" product="default" msgid="5691623136957148335">"আপুনি অশুদ্ধভাৱে আপোনাৰ লক খোলাৰ আৰ্হিটো <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ আঁকিলে৷ <xliff:g id="NUMBER_1">%2$d</xliff:g> তকৈ অধিকবাৰ অসফলভাৱে কৰা প্ৰয়াসৰ পিছত, আপোনাৰ ফ\'নটো আনলক কৰিবৰ বাবে Google ছাইন ইনৰ জৰিয়তে কাৰ্যটো কৰিবলৈ কোৱা হ\'ব৷\n\n <xliff:g id="NUMBER_2">%3$d</xliff:g> ছেকেণ্ডৰ পিছত পুনৰ চেষ্টা কৰক৷"</string>
+ <string name="lockscreen_failed_attempts_almost_glogin" product="default" msgid="5691623136957148335">"আপুনি অশুদ্ধভাৱে আপোনাৰ লক খোলাৰ আৰ্হিটো <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ আঁকিলে৷ <xliff:g id="NUMBER_1">%2$d</xliff:g> তকৈ অধিকবাৰ অসফলভাৱে কৰা প্ৰয়াসৰ পাছত, আপোনাৰ ফ\'নটো আনলক কৰিবৰ বাবে Google ছাইন ইনৰ জৰিয়তে কাৰ্যটো কৰিবলৈ কোৱা হ\'ব৷\n\n <xliff:g id="NUMBER_2">%3$d</xliff:g> ছেকেণ্ডৰ পাছত পুনৰ চেষ্টা কৰক৷"</string>
<string name="lockscreen_failed_attempts_almost_at_wipe" product="tablet" msgid="7914445759242151426">"আপুনি টে\'বলেটটো <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ ভুলকৈ আনলক কৰিবলৈ প্ৰয়াস কৰিছে। <xliff:g id="NUMBER_1">%2$d</xliff:g> বাৰতকৈ বেছি প্ৰয়াস কৰিলে টে\'বলেটটো ফেক্টৰী ডিফ\'ল্টলৈ ৰিছেট কৰা হ\'ব আৰু আটাইবোৰ ব্যৱহাৰকাৰী ডেটা হেৰুৱাব।"</string>
<string name="lockscreen_failed_attempts_almost_at_wipe" product="tv" msgid="4275591249631864248">"আপুনি নিজৰ Android TV ডিভাইচটো আনলক কৰিবলৈ <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ প্ৰয়াস কৰিছে। <xliff:g id="NUMBER_1">%2$d</xliff:g>তকৈ বেছি বাৰ ভুলকৈ প্ৰয়াস কৰাৰ পাছত আপোনাৰ Android TV ডিভাইচটো ফেক্টৰী ডিফ’ল্টলৈ ৰিছেট কৰা হ’ব আৰু ব্যৱহাৰকাৰীৰ সকলো ডেটা হেৰুৱাব।"</string>
<string name="lockscreen_failed_attempts_almost_at_wipe" product="default" msgid="1166532464798446579">"আপুনি ফ\'নটো <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ ভুলকৈ আনলক কৰিবলৈ প্ৰয়াস কৰিছে। <xliff:g id="NUMBER_1">%2$d</xliff:g> বাৰতকৈ বেছি প্ৰয়াস কৰিলে ফ\'নটো ফেক্টৰী ডিফ\'ল্টলৈ ৰিছেট কৰা হ\'ব আৰু আটাইবোৰ ব্যৱহাৰকাৰী ডেটা হেৰুৱাব।"</string>
@@ -1049,11 +1049,11 @@
<string name="permdesc_writeHistoryBookmarks" product="tv" msgid="88642768580408561">"এপ্টোক আপোনাৰ Android TV ডিভাইচত ষ্ট’ৰ কৰি ৰখা ব্ৰাউজাৰৰ ইতিহাস আৰু বুকমার্কবোৰ সংশোধন কৰিবলৈ অনুমতি দিয়ে। ব্ৰাউজাৰ ডেটা মোহাৰিবলৈ অথবা সংশোধন কৰিবলৈ ই এপ্টোক অনুমতি দিব পাৰে। টোকা: এই অনুমতি তৃতীয় পক্ষৰ ব্ৰাউজাৰবোৰ অথবা ৱেব ব্ৰাউজিঙৰ ক্ষমতা থকা অন্য এপ্লিকেশ্বনবোৰৰ দ্বাৰা বলৱৎ কৰা নহ’বও পাৰে।"</string>
<string name="permdesc_writeHistoryBookmarks" product="default" msgid="2245203087160913652">"আপোনাৰ ফ\'নত সঞ্চয় কৰি ৰখা ব্ৰাউজাৰৰ বুকমার্ক আৰু ব্ৰাউজাৰৰ ইতিহাস সংশোধন কৰিবলৈ এপক অনুমতি দিয়ে। টোকা: এই অনুমতি তৃতীয় পক্ষৰ ব্ৰাউজাৰবোৰ বা ৱেব ব্ৰাউজিং কৰিব পৰা অন্য এপ্লিকেশ্বনবোৰৰ দ্বাৰা বলৱৎ নহ\'বও পাৰে।"</string>
<string name="permlab_setAlarm" msgid="1158001610254173567">"এলাৰ্ম ছেট কৰক"</string>
- <string name="permdesc_setAlarm" msgid="2185033720060109640">"এপটোক ইনষ্টল হৈ থকা এলাৰ্ম ক্লক এপত এলাৰ্ম ছেট কৰিবলৈ অনুমতি দিয়ে। কিছুমান এলাৰ্ম ক্লক এপত এই সুবিধাটো প্ৰযোজ্য নহ’ব পাৰে।"</string>
+ <string name="permdesc_setAlarm" msgid="2185033720060109640">"এপ্টোক ইনষ্টল হৈ থকা এলাৰ্ম ক্লক এপত এলাৰ্ম ছেট কৰিবলৈ অনুমতি দিয়ে। কিছুমান এলাৰ্ম ক্লক এপত এই সুবিধাটো প্ৰযোজ্য নহ’ব পাৰে।"</string>
<string name="permlab_addVoicemail" msgid="4770245808840814471">"ভইচমেইল যোগ কৰক"</string>
- <string name="permdesc_addVoicemail" msgid="5470312139820074324">"আপোনাৰ ভইচমেইল ইনবক্সত বাৰ্তাবোৰ যোগ কৰিবলৈ এপটোক অনুমতি দিয়ক।"</string>
+ <string name="permdesc_addVoicemail" msgid="5470312139820074324">"আপোনাৰ ভইচমেইল ইনবক্সত বাৰ্তাবোৰ যোগ কৰিবলৈ এপ্টোক অনুমতি দিয়ক।"</string>
<string name="permlab_writeGeolocationPermissions" msgid="8605631647492879449">"ব্ৰাউজাৰৰ জিঅ\'লোকেশ্বনৰ অনুমতিসমূহ সংশোধন কৰক"</string>
- <string name="permdesc_writeGeolocationPermissions" msgid="5817346421222227772">"ব্ৰাউজাৰৰ জিঅ\'লোকেশ্বন বিষয়ক অনুমতিসমূহ সংশোধন কৰিবলৈ এপটোক অনুমতি দিয়ে৷ ক্ষতিকাৰক এপবোৰে একপক্ষীয় ৱেবছাইটসমূহলৈ অৱস্থান সেৱাৰ তথ্য পঠিয়াবলৈ ইয়াক ব্যৱহাৰ কৰিব পাৰে৷"</string>
+ <string name="permdesc_writeGeolocationPermissions" msgid="5817346421222227772">"ব্ৰাউজাৰৰ জিঅ\'লোকেশ্বন বিষয়ক অনুমতিসমূহ সংশোধন কৰিবলৈ এপ্টোক অনুমতি দিয়ে৷ ক্ষতিকাৰক এপবোৰে একপক্ষীয় ৱেবছাইটসমূহলৈ অৱস্থান সেৱাৰ তথ্য পঠিয়াবলৈ ইয়াক ব্যৱহাৰ কৰিব পাৰে৷"</string>
<string name="save_password_message" msgid="2146409467245462965">"ব্ৰাউজাৰে এই পাছৱর্ডটো মনত ৰখাটো বিচাৰেনে?"</string>
<string name="save_password_notnow" msgid="2878327088951240061">"এতিয়াই নহয়"</string>
<string name="save_password_remember" msgid="6490888932657708341">"মনত ৰাখিব"</string>
@@ -1208,12 +1208,12 @@
<string name="aerr_process" msgid="4268018696970966407">"<xliff:g id="PROCESS">%1$s</xliff:g> বন্ধ হ’ল"</string>
<string name="aerr_application_repeated" msgid="7804378743218496566">"<xliff:g id="APPLICATION">%1$s</xliff:g> বাৰে বাৰে বন্ধ হৈ গৈছে"</string>
<string name="aerr_process_repeated" msgid="1153152413537954974">"<xliff:g id="PROCESS">%1$s</xliff:g> বাৰে বাৰে বন্ধ হৈ গৈছে"</string>
- <string name="aerr_restart" msgid="2789618625210505419">"আকৌ এপটো খোলক"</string>
+ <string name="aerr_restart" msgid="2789618625210505419">"আকৌ এপ্টো খোলক"</string>
<string name="aerr_report" msgid="3095644466849299308">"আপোনাৰ প্ৰতিক্ৰিয়া পঠিয়াওক"</string>
<string name="aerr_close" msgid="3398336821267021852">"বন্ধ কৰক"</string>
<string name="aerr_mute" msgid="2304972923480211376">"ডিভাইচ ৰিষ্টাৰ্ট নোহোৱালৈ মিউট কৰক"</string>
<string name="aerr_wait" msgid="3198677780474548217">"অপেক্ষা কৰক"</string>
- <string name="aerr_close_app" msgid="8318883106083050970">"এপটো বন্ধ কৰক"</string>
+ <string name="aerr_close_app" msgid="8318883106083050970">"এপ্টো বন্ধ কৰক"</string>
<string name="anr_title" msgid="7290329487067300120"></string>
<string name="anr_activity_application" msgid="8121716632960340680">"<xliff:g id="APPLICATION">%2$s</xliff:g>য়ে সঁহাৰি দিয়া নাই"</string>
<string name="anr_activity_process" msgid="3477362583767128667">"<xliff:g id="ACTIVITY">%1$s</xliff:g>য়ে সঁহাৰি দিয়া নাই"</string>
@@ -1231,7 +1231,7 @@
<string name="screen_compat_mode_hint" msgid="4032272159093750908">"ছিষ্টেমৰ ছেটিং > এপ্ > ডাউনল’ড কৰা সমল-লৈ গৈ ইয়াক আকৌ সক্ষম কৰক।"</string>
<string name="unsupported_display_size_message" msgid="7265211375269394699">"<xliff:g id="APP_NAME">%1$s</xliff:g>এ বর্তমানৰ ডিছপ্লে’ৰ আকাৰ ছেটিং ব্যৱহাৰ কৰিব নোৱাৰে আৰু ই সঠিকভাৱে নচলিবও পাৰে।"</string>
<string name="unsupported_display_size_show" msgid="980129850974919375">"সদায় দেখুৱাওক"</string>
- <string name="unsupported_compile_sdk_message" msgid="7326293500707890537">"<xliff:g id="APP_NAME">%1$s</xliff:g>ক এটা খাপ নোখোৱা Android OS সংস্কৰণৰ বাবে তৈয়াৰ কৰা হৈছিল, যাৰ ফলত ই অস্বাভাৱিকধৰণে আচৰণ কৰিব পাৰে। এপটোৰ শেহতীয়া সংস্কৰণ উপলব্ধ হ\'ব পাৰে।"</string>
+ <string name="unsupported_compile_sdk_message" msgid="7326293500707890537">"<xliff:g id="APP_NAME">%1$s</xliff:g>ক এটা খাপ নোখোৱা Android OS সংস্কৰণৰ বাবে তৈয়াৰ কৰা হৈছিল, যাৰ ফলত ই অস্বাভাৱিকধৰণে আচৰণ কৰিব পাৰে। এপ্টোৰ শেহতীয়া সংস্কৰণ উপলব্ধ হ\'ব পাৰে।"</string>
<string name="unsupported_compile_sdk_show" msgid="1601210057960312248">"সদায় দেখুৱাওক"</string>
<string name="unsupported_compile_sdk_check_update" msgid="1103639989147664456">"আপডে’ট আছে নেকি চাওক"</string>
<string name="smv_application" msgid="3775183542777792638">"এপটোৱে <xliff:g id="APPLICATION">%1$s</xliff:g> (প্ৰক্ৰিয়াটোৱে <xliff:g id="PROCESS">%2$s</xliff:g>) নিজে বলবৎ কৰা StrictMode নীতি ভংগ কৰিলে।"</string>
@@ -1328,7 +1328,7 @@
<string name="sms_short_code_confirm_allow" msgid="920477594325526691">"পঠিয়াওক"</string>
<string name="sms_short_code_confirm_deny" msgid="1356917469323768230">"বাতিল কৰক"</string>
<string name="sms_short_code_remember_choice" msgid="1374526438647744862">"মোৰ পচন্দ মনত ৰাখিব"</string>
- <string name="sms_short_code_remember_undo_instruction" msgid="2620984439143080410">"আপুনি ইয়াক পিছত ছেটিং > এপত সলনি কৰিব পাৰে"</string>
+ <string name="sms_short_code_remember_undo_instruction" msgid="2620984439143080410">"আপুনি ইয়াক পাছত ছেটিং > এপত সলনি কৰিব পাৰে"</string>
<string name="sms_short_code_confirm_always_allow" msgid="2223014893129755950">"যিকোনো সময়ত অনুমতি দিয়ক"</string>
<string name="sms_short_code_confirm_never_allow" msgid="2688828813521652079">"কেতিয়াও অনুমতি নিদিব"</string>
<string name="sim_removed_title" msgid="5387212933992546283">"ছিম কাৰ্ড আঁতৰোৱা হ’ল"</string>
@@ -1338,8 +1338,8 @@
<string name="sim_added_message" msgid="6602906609509958680">"ম\'বাইলৰ নেটৱর্ক ব্যৱহাৰ কৰিবলৈ আপোনাৰ ডিভাইচটো ৰিষ্টার্ট কৰক।"</string>
<string name="sim_restart_button" msgid="8481803851341190038">"ৰিষ্টাৰ্ট কৰক"</string>
<string name="install_carrier_app_notification_title" msgid="5712723402213090102">"ম’বাইল সেৱা সক্ৰিয় কৰক"</string>
- <string name="install_carrier_app_notification_text" msgid="2781317581274192728">"আপোনাৰ নতুন ছিমখন সক্ৰিয় কৰিবলৈ বাহকৰ এপটো ডাউনল’ড কৰক"</string>
- <string name="install_carrier_app_notification_text_app_name" msgid="4086877327264106484">"আপোনাৰ নতুন ছিমখন সক্ৰিয় কৰিবলৈ <xliff:g id="APP_NAME">%1$s</xliff:g> এপটো ডাউনল’ড কৰক"</string>
+ <string name="install_carrier_app_notification_text" msgid="2781317581274192728">"আপোনাৰ নতুন ছিমখন সক্ৰিয় কৰিবলৈ বাহকৰ এপ্টো ডাউনল’ড কৰক"</string>
+ <string name="install_carrier_app_notification_text_app_name" msgid="4086877327264106484">"আপোনাৰ নতুন ছিমখন সক্ৰিয় কৰিবলৈ <xliff:g id="APP_NAME">%1$s</xliff:g> এপ্টো ডাউনল’ড কৰক"</string>
<string name="install_carrier_app_notification_button" msgid="6257740533102594290">"এপ্ ডাউনল’ড কৰক"</string>
<string name="carrier_app_notification_title" msgid="5815477368072060250">"নতুন ছিম ভৰোৱা হৈছে"</string>
<string name="carrier_app_notification_text" msgid="6567057546341958637">"ছেট আপ কৰিবলৈ টিপক"</string>
@@ -1452,9 +1452,9 @@
<string name="permlab_readInstallSessions" msgid="7279049337895583621">"ইনষ্টল কৰা ছেশ্বনসমূহ পঢ়িব পাৰে"</string>
<string name="permdesc_readInstallSessions" msgid="4012608316610763473">"এটা এপ্লিকেশ্বনক ইনষ্টল কৰা ছেশ্বনসমূহ পঢ়িবলৈ অনুমতি দিয়ে। এই কাৰ্যই সক্ৰিয় পেকেজ ইনষ্টলেশ্বনৰ বিষয়ে চাবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_requestInstallPackages" msgid="7600020863445351154">"পেকেজ ইনষ্টলৰ বাবে অনুৰোধ কৰিব পাৰে"</string>
- <string name="permdesc_requestInstallPackages" msgid="3969369278325313067">"পেকেজ ইনষ্টল কৰাৰ অনুৰোধ প্ৰেৰণ কৰিবলৈ এপটোক অনুমতি দিয়ে।"</string>
+ <string name="permdesc_requestInstallPackages" msgid="3969369278325313067">"পেকেজ ইনষ্টল কৰাৰ অনুৰোধ প্ৰেৰণ কৰিবলৈ এপ্টোক অনুমতি দিয়ে।"</string>
<string name="permlab_requestDeletePackages" msgid="2541172829260106795">"পেকেজ মচাৰ অনুৰোধ কৰিব পাৰে"</string>
- <string name="permdesc_requestDeletePackages" msgid="6133633516423860381">"এপটোক পেকেজবোৰ মচাৰ অনুৰোধ কৰিবলৈ দিয়ে।"</string>
+ <string name="permdesc_requestDeletePackages" msgid="6133633516423860381">"এপ্টোক পেকেজবোৰ মচাৰ অনুৰোধ কৰিবলৈ দিয়ে।"</string>
<string name="permlab_requestIgnoreBatteryOptimizations" msgid="7646611326036631439">"বেটাৰী অপ্টিমাইজেশ্বন উপেক্ষা কৰিবলৈ বিচাৰক"</string>
<string name="permdesc_requestIgnoreBatteryOptimizations" msgid="634260656917874356">"কোনো এপক সেই এপ্টোৰ বাবে বেটাৰী অপ্টিমাইজেশ্বন উপেক্ষা কৰিবলৈ অনুমতি বিচাৰিবলৈ দিয়ে।"</string>
<string name="permlab_queryAllPackages" msgid="2928450604653281650">"আটাইবোৰ পেকেজত প্ৰশ্ন সোধক"</string>
@@ -1478,8 +1478,8 @@
<string name="permission_request_notification_title" msgid="1810025922441048273">"অনুমতি বিচাৰি অনুৰোধ কৰা হৈছে"</string>
<string name="permission_request_notification_with_subtitle" msgid="3743417870360129298">"<xliff:g id="ACCOUNT">%s</xliff:g> একাউণ্টৰ বাবে\nঅনুমতি বিচাৰি অনুৰোধ কৰা হৈছে।"</string>
<string name="permission_request_notification_for_app_with_subtitle" msgid="1298704005732851350">"<xliff:g id="APP">%1$s</xliff:g>এ <xliff:g id="ACCOUNT">%2$s</xliff:g> একাউণ্টটো এক্সেছৰ \nঅনুমতি বিচাৰি অনুৰোধ জনাইছে।"</string>
- <string name="forward_intent_to_owner" msgid="4620359037192871015">"আপুনি আপোনাৰ কৰ্মস্থানৰ প্ৰ\'ফাইলৰ বাহিৰত এই এপটো ব্যৱহাৰ কৰি আছে"</string>
- <string name="forward_intent_to_work" msgid="3620262405636021151">"আপুনি আপোনাৰ কৰ্মস্থানৰ প্ৰ\'ফাইলৰ ভিতৰত এই এপটো ব্যৱহাৰ কৰি আছে"</string>
+ <string name="forward_intent_to_owner" msgid="4620359037192871015">"আপুনি আপোনাৰ কৰ্মস্থানৰ প্ৰ\'ফাইলৰ বাহিৰত এই এপ্টো ব্যৱহাৰ কৰি আছে"</string>
+ <string name="forward_intent_to_work" msgid="3620262405636021151">"আপুনি আপোনাৰ কৰ্মস্থানৰ প্ৰ\'ফাইলৰ ভিতৰত এই এপ্টো ব্যৱহাৰ কৰি আছে"</string>
<string name="input_method_binding_label" msgid="1166731601721983656">"ইনপুট পদ্ধতি"</string>
<string name="sync_binding_label" msgid="469249309424662147">"ছিংক"</string>
<string name="accessibility_binding_label" msgid="1974602776545801715">"সাধ্য সুবিধাসমূহ"</string>
@@ -1560,7 +1560,7 @@
<string name="shareactionprovider_share_with" msgid="2753089758467748982">"ইয়াৰ জৰিয়তে শ্বেয়াৰ কৰক"</string>
<string name="shareactionprovider_share_with_application" msgid="4902832247173666973">"<xliff:g id="APPLICATION_NAME">%s</xliff:g>ৰ জৰিয়তে শ্বেয়াৰ কৰক"</string>
<string name="content_description_sliding_handle" msgid="982510275422590757">"শ্লাইড কৰা হেণ্ডেল৷ স্পৰ্শ কৰক আৰু ধৰি ৰাখক৷"</string>
- <string name="description_target_unlock_tablet" msgid="7431571180065859551">"স্ক্ৰীণ আনলক কৰিবলৈ ছোৱাইপ কৰক৷"</string>
+ <string name="description_target_unlock_tablet" msgid="7431571180065859551">"স্ক্ৰীন আনলক কৰিবলৈ ছোৱাইপ কৰক৷"</string>
<string name="action_bar_home_description" msgid="1501655419158631974">"গৃহ পৃষ্ঠালৈ যাওক"</string>
<string name="action_bar_up_description" msgid="6611579697195026932">"ওপৰলৈ যাওক"</string>
<string name="action_menu_overflow_description" msgid="4579536843510088170">"অধিক বিকল্প"</string>
@@ -1660,18 +1660,18 @@
<string name="kg_login_invalid_input" msgid="8292367491901220210">"ব্যৱহাৰকাৰীৰ অমান্য নাম বা পাছৱর্ড।"</string>
<string name="kg_login_account_recovery_hint" msgid="4892466171043541248">"নিজৰ ব্যৱহাৰকাৰী নাম আৰু পাছৱর্ড পাহৰিলেনে?\n"<b>"google.com/accounts/recovery"</b>" লৈ যাওক।"</string>
<string name="kg_login_checking_password" msgid="4676010303243317253">"একাউণ্ট পৰীক্ষা কৰি থকা হৈছে…"</string>
- <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="23741434207544038">"আপুনি আপোনাৰ পিন <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>ছেকেণ্ডৰ পিছত আকৌ চেষ্টা কৰক।"</string>
- <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="3328686432962224215">"আপুনি আপোনাৰ পাছৱৰ্ড <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ছেকেণ্ডৰ পিছত আকৌ চেষ্টা কৰক।"</string>
- <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="7357404233979139075">"আপুনি আপোনাৰ ল\'ক খোলাৰ আৰ্হি <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ আঁকিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>ছেকেণ্ডৰ পিছত আকৌ চেষ্টা কৰক।"</string>
+ <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="23741434207544038">"আপুনি আপোনাৰ পিন <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>ছেকেণ্ডৰ পাছত আকৌ চেষ্টা কৰক।"</string>
+ <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="3328686432962224215">"আপুনি আপোনাৰ পাছৱৰ্ড <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ছেকেণ্ডৰ পাছত আকৌ চেষ্টা কৰক।"</string>
+ <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="7357404233979139075">"আপুনি আপোনাৰ ল\'ক খোলাৰ আৰ্হি <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ আঁকিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>ছেকেণ্ডৰ পাছত আকৌ চেষ্টা কৰক।"</string>
<string name="kg_failed_attempts_almost_at_wipe" product="tablet" msgid="3479940221343361587">"আপুনি <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ টেবলেটৰ ল\'ক খোলাৰ প্ৰয়াস কৰিছে। <xliff:g id="NUMBER_1">%2$d</xliff:g>তকৈ বেছি বাৰ ভুল প্ৰয়াস কৰিলে টেবলেটটো ফেক্টৰী ডিফ\'ল্টলৈ ৰিছেট কৰা হ\'ব আৰু আটাইবোৰ ব্যৱহাৰকাৰীৰ ডেটা হেৰুৱাব।"</string>
<string name="kg_failed_attempts_almost_at_wipe" product="tv" msgid="9064457748587850217">"আপুনি নিজৰ Android TV ডিভাইচটো আনলক কৰিবলৈ <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ প্ৰয়াস কৰিছে। <xliff:g id="NUMBER_1">%2$d</xliff:g>তকৈ বেছি বাৰ ভুলকৈ প্ৰয়াস কৰাৰ পাছত আপোনাৰ Android TV ডিভাইচটো ফেক্টৰী ডিফ’ল্টলৈ ৰিছেট কৰা হ’ব আৰু ব্যৱহাৰকাৰীৰ আটাইবোৰ ডেটা হেৰুৱাব।"</string>
<string name="kg_failed_attempts_almost_at_wipe" product="default" msgid="5955398963754432548">"আপুনি <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ ফ\'নৰ ল\'ক খোলাৰ প্ৰয়াস কৰিছে। <xliff:g id="NUMBER_1">%2$d</xliff:g>তকৈ বেছি বাৰ ভুল প্ৰয়াস কৰিলে ফ\'নটো ফেক্টৰী ডিফ\'ল্টলৈ ৰিছেট কৰা হ\'ব আৰু ব্যৱহাৰকাৰীৰ আটাইবোৰ ডেটা হেৰুৱাব।"</string>
<string name="kg_failed_attempts_now_wiping" product="tablet" msgid="2299099385175083308">"আপুনি <xliff:g id="NUMBER">%d</xliff:g>বাৰ ভুলকৈ টেবলেটৰ ল\'ক খোলাৰ প্ৰয়াস কৰিছে। টেবলেটটো এতিয়া ফেক্টৰী ডিফ\'ল্টলৈ ৰিছেট কৰা হ\'ব।"</string>
<string name="kg_failed_attempts_now_wiping" product="tv" msgid="5045460916106267585">"আপুনি নিজৰ Android TV ডিভাইচটো আনলক কৰিবলৈ <xliff:g id="NUMBER">%d</xliff:g>বাৰ ভুলকৈ প্ৰয়াস কৰিছে। আপোনাৰ Android TV ডিভাইচটো এতিয়া ফেক্টৰী ডিফ’ল্টলৈ ৰিছেট কৰা হ’ব।"</string>
<string name="kg_failed_attempts_now_wiping" product="default" msgid="5043730590446071189">"আপুনি <xliff:g id="NUMBER">%d</xliff:g>বাৰ ভুলকৈ ফ\'নৰ ল\'ক খোলাৰ প্ৰয়াস কৰিছে। ফ\'নটো এতিয়া ফেক্টৰী ডিফ\'ল্টলৈ ৰিছেট কৰা হ\'ব।"</string>
- <string name="kg_failed_attempts_almost_at_login" product="tablet" msgid="7086799295109717623">"আপুনি আপোনাৰ ল\'ক খোলাৰ আৰ্হিটো <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ আঁকিছে। <xliff:g id="NUMBER_1">%2$d</xliff:g>তকৈ বেছি বাৰ ভুল আৰ্হি আঁকিলে আপোনাৰ টেবলেটটো কোনো একাউণ্টৰ জৰিয়তে আনলক কৰিবলৈ কোৱা হ\'ব।\n\n <xliff:g id="NUMBER_2">%3$d</xliff:g> ছেকেণ্ডৰ পিছত আকৌ চেষ্টা কৰক।"</string>
+ <string name="kg_failed_attempts_almost_at_login" product="tablet" msgid="7086799295109717623">"আপুনি আপোনাৰ ল\'ক খোলাৰ আৰ্হিটো <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ আঁকিছে। <xliff:g id="NUMBER_1">%2$d</xliff:g>তকৈ বেছি বাৰ ভুল আৰ্হি আঁকিলে আপোনাৰ টেবলেটটো কোনো একাউণ্টৰ জৰিয়তে আনলক কৰিবলৈ কোৱা হ\'ব।\n\n <xliff:g id="NUMBER_2">%3$d</xliff:g> ছেকেণ্ডৰ পাছত আকৌ চেষ্টা কৰক।"</string>
<string name="kg_failed_attempts_almost_at_login" product="tv" msgid="4670840383567106114">"আপুনি নিজৰ আনলক আর্হিটো <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ দিয়ে। আকৌ <xliff:g id="NUMBER_1">%2$d</xliff:g>বাৰ ভুলকৈ প্ৰয়াস কৰাৰ পাছত আপোনাক এটা ইমেইল একাউণ্ট ব্যৱহাৰ কৰি নিজৰ Android TV ডিভাইচটো আনলক কৰিবলৈ কোৱা হ’ব।\n\n <xliff:g id="NUMBER_2">%3$d</xliff:g>ছেকেণ্ডৰ পাছত পুনৰ চেষ্টা কৰক।"</string>
- <string name="kg_failed_attempts_almost_at_login" product="default" msgid="5270861875006378092">"আপুনি আপোনাৰ ল\'ক খোলাৰ আৰ্হিটো <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ আঁকিছে। <xliff:g id="NUMBER_1">%2$d</xliff:g>তকৈ বেছি বাৰ ভুল আৰ্হি আঁকিলে আপোনাৰ ফ\'নটো কোনো একাউণ্টৰ জৰিয়তে আনলক কৰিবলৈ কোৱা হ\'ব।\n\n <xliff:g id="NUMBER_2">%3$d</xliff:g> ছেকেণ্ডৰ পিছত আকৌ চেষ্টা কৰক।"</string>
+ <string name="kg_failed_attempts_almost_at_login" product="default" msgid="5270861875006378092">"আপুনি আপোনাৰ ল\'ক খোলাৰ আৰ্হিটো <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ আঁকিছে। <xliff:g id="NUMBER_1">%2$d</xliff:g>তকৈ বেছি বাৰ ভুল আৰ্হি আঁকিলে আপোনাৰ ফ\'নটো কোনো একাউণ্টৰ জৰিয়তে আনলক কৰিবলৈ কোৱা হ\'ব।\n\n <xliff:g id="NUMBER_2">%3$d</xliff:g> ছেকেণ্ডৰ পাছত আকৌ চেষ্টা কৰক।"</string>
<string name="kg_text_message_separator" product="default" msgid="4503708889934976866">" — "</string>
<string name="kg_reordering_delete_drop_target_text" msgid="2034358143731750914">"আঁতৰাওক"</string>
<string name="safe_media_volume_warning" product="default" msgid="3751676824423049994">"অনুমোদিত স্তৰতকৈ ওপৰলৈ ভলিউম বঢ়াব নেকি?\n\nদীৰ্ঘ সময়ৰ বাবে উচ্চ ভলিউমত শুনাৰ ফলত শ্ৰৱণ ক্ষমতাৰ ক্ষতি হ\'ব পাৰে।"</string>
@@ -1835,7 +1835,7 @@
<string name="restr_pin_create_pin" msgid="917067613896366033">"সীমাবদ্ধতা সংশোধন কৰিবলৈ এটা পিন সৃষ্টি কৰক"</string>
<string name="restr_pin_error_doesnt_match" msgid="7063392698489280556">"পিনবোৰ মিলা নাই। আকৌ চেষ্টা কৰক।"</string>
<string name="restr_pin_error_too_short" msgid="1547007808237941065">"পিনটো অতি চুটি। কমেও ৪টা সংখ্যাৰ হ\'ব লাগিব।"</string>
- <string name="restr_pin_try_later" msgid="5897719962541636727">"পিছত আকৌ চেষ্টা কৰক"</string>
+ <string name="restr_pin_try_later" msgid="5897719962541636727">"পাছত আকৌ চেষ্টা কৰক"</string>
<string name="immersive_cling_title" msgid="2307034298721541791">"স্ক্ৰীন পূৰ্ণৰূপত চাই আছে"</string>
<string name="immersive_cling_description" msgid="7092737175345204832">"বাহিৰ হ\'বলৈ ওপৰৰপৰা তললৈ ছোৱাইপ কৰক।"</string>
<string name="immersive_cling_positive" msgid="7047498036346489883">"বুজি পালোঁ"</string>
@@ -1931,7 +1931,7 @@
<string name="language_picker_section_all" msgid="1985809075777564284">"সকলো ভাষা"</string>
<string name="region_picker_section_all" msgid="756441309928774155">"আটাইবোৰ অঞ্চল"</string>
<string name="locale_search_menu" msgid="6258090710176422934">"সন্ধান কৰক"</string>
- <string name="app_suspended_title" msgid="888873445010322650">"এপটো নাই"</string>
+ <string name="app_suspended_title" msgid="888873445010322650">"এপ্টো নাই"</string>
<string name="app_suspended_default_message" msgid="6451215678552004172">"এই মুহূৰ্তত <xliff:g id="APP_NAME_0">%1$s</xliff:g> উপলব্ধ নহয়। ইয়াক <xliff:g id="APP_NAME_1">%2$s</xliff:g>এ পৰিচালনা কৰে।"</string>
<string name="app_suspended_more_details" msgid="211260942831587014">"অধিক জানক"</string>
<string name="app_suspended_unsuspend_message" msgid="1665438589450555459">"এপ্ আনপজ কৰক"</string>
@@ -1958,7 +1958,7 @@
<string name="app_streaming_blocked_message_for_settings_dialog" product="tv" msgid="820334666354451145">"এইটো আপোনাৰ <xliff:g id="DEVICE">%1$s</xliff:g>ত এক্সেছ কৰিব নোৱাৰি। তাৰ পৰিৱৰ্তে আপোনাৰ Android TV ডিভাইচত চেষ্টা কৰি চাওক।"</string>
<string name="app_streaming_blocked_message_for_settings_dialog" product="tablet" msgid="3286849551133045896">"এইটো আপোনাৰ <xliff:g id="DEVICE">%1$s</xliff:g>ত এক্সেছ কৰিব নোৱাৰি। তাৰ পৰিৱৰ্তে আপোনাৰ টেবলেটত চেষ্টা কৰি চাওক।"</string>
<string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"এইটো আপোনাৰ <xliff:g id="DEVICE">%1$s</xliff:g>ত এক্সেছ কৰিব নোৱাৰি। তাৰ পৰিৱৰ্তে আপোনাৰ ফ’নত চেষ্টা কৰি চাওক।"</string>
- <string name="deprecated_target_sdk_message" msgid="5203207875657579953">"এই এপটো Androidৰ এটা পুৰণা সংস্কৰণৰ বাবে প্ৰস্তুত কৰা হৈছিল, আৰু ই বিচৰাধৰণে কাম নকৰিবও পাৰে। ইয়াৰ আপডে’ট আছে নেকি চাওক, বা বিকাশকৰ্তাৰ সৈতে যোগাযোগ কৰক।"</string>
+ <string name="deprecated_target_sdk_message" msgid="5203207875657579953">"এই এপ্টো Androidৰ এটা পুৰণা সংস্কৰণৰ বাবে প্ৰস্তুত কৰা হৈছিল, আৰু ই বিচৰাধৰণে কাম নকৰিবও পাৰে। ইয়াৰ আপডে’ট আছে নেকি চাওক, বা বিকাশকৰ্তাৰ সৈতে যোগাযোগ কৰক।"</string>
<string name="deprecated_target_sdk_app_store" msgid="8456784048558808909">"আপডে’ট আছে নেকি চাওক"</string>
<string name="new_sms_notification_title" msgid="6528758221319927107">"আপুনি নতুন বার্তা লাভ কৰিছে"</string>
<string name="new_sms_notification_content" msgid="3197949934153460639">"চাবলৈ এছএমএছ এপ্ খোলক"</string>
@@ -1997,7 +1997,7 @@
<string name="time_picker_text_input_mode_description" msgid="4761160667516611576">"সময়ৰ ইনপুটৰ বাবে পাঠৰ ইনপুট ম\'ডলৈ যাওক।"</string>
<string name="time_picker_radial_mode_description" msgid="1222342577115016953">"সময়ৰ ইনপুটৰ বাবে ঘড়ী ম\'ডলৈ যাওক।"</string>
<string name="autofill_picker_accessibility_title" msgid="4425806874792196599">"স্বয়ংপূৰ্তিৰ বিকল্পসমূহ"</string>
- <string name="autofill_save_accessibility_title" msgid="1523225776218450005">"পিছত স্বয়ংপূৰ্তি কৰিবলৈ ছেভ কৰক"</string>
+ <string name="autofill_save_accessibility_title" msgid="1523225776218450005">"পাছত স্বয়ংপূৰ্তি কৰিবলৈ ছেভ কৰক"</string>
<string name="autofill_error_cannot_autofill" msgid="6528827648643138596">"সমলসমূহ স্বয়ংপূৰ্তি কৰিব নোৱাৰি"</string>
<string name="autofill_picker_no_suggestions" msgid="1076022650427481509">"কোনো স্বয়ংপূৰ্তি পৰামৰ্শ নাই"</string>
<string name="autofill_picker_some_suggestions" msgid="5560549696296202701">"{count,plural, =1{এটা স্বয়ংপূৰ্তি পৰামৰ্শ}one{# টা স্বয়ংপূৰ্তি পৰামৰ্শ}other{# টা স্বয়ংপূৰ্তি পৰামৰ্শ}}"</string>
@@ -2040,7 +2040,7 @@
<string name="popup_window_default_title" msgid="6907717596694826919">"পপআপ ৱিণ্ড\'"</string>
<string name="slice_more_content" msgid="3377367737876888459">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
<string name="shortcut_restored_on_lower_version" msgid="9206301954024286063">"এপৰ সংস্কৰণ অৱনমিত কৰা হৈছে, বা ই এই শ্বৰ্টকাটটোৰ লগত খাপ নাখায়"</string>
- <string name="shortcut_restore_not_supported" msgid="4763198938588468400">"এপটোত বেকআপ আৰু পুনঃস্থাপন সুবিধা নথকাৰ বাবে শ্বৰ্টকাট পুনঃস্থাপন কৰিবপৰা নগ\'ল"</string>
+ <string name="shortcut_restore_not_supported" msgid="4763198938588468400">"এপ্টোত বেকআপ আৰু পুনঃস্থাপন সুবিধা নথকাৰ বাবে শ্বৰ্টকাট পুনঃস্থাপন কৰিবপৰা নগ\'ল"</string>
<string name="shortcut_restore_signature_mismatch" msgid="579345304221605479">"এপৰ স্বাক্ষৰৰ অমিল হোৱাৰ বাবে শ্বৰ্টকাট পুনঃস্থাপন কৰিবপৰা নগ\'ল"</string>
<string name="shortcut_restore_unknown_issue" msgid="2478146134395982154">"শ্বৰ্টকাট পুনঃস্থাপন কৰিবপৰা নগ\'ল"</string>
<string name="shortcut_disabled_reason_unknown" msgid="753074793553599166">"শ্বৰ্টকাট অক্ষম কৰি থোৱা হৈছে"</string>
diff --git a/core/res/res/values-ca/strings.xml b/core/res/res/values-ca/strings.xml
index b52e57a..4f6d0c7 100644
--- a/core/res/res/values-ca/strings.xml
+++ b/core/res/res/values-ca/strings.xml
@@ -1949,15 +1949,15 @@
<string name="app_streaming_blocked_title_for_settings_dialog" product="tv" msgid="196994247017450357">"La configuració d\'Android TV no està disponible"</string>
<string name="app_streaming_blocked_title_for_settings_dialog" product="tablet" msgid="8222710146267948647">"La configuració de la tauleta no està disponible"</string>
<string name="app_streaming_blocked_title_for_settings_dialog" product="default" msgid="6895719984375299791">"Configuració del telèfon no disponible"</string>
- <string name="app_streaming_blocked_message" product="tv" msgid="4003011766528814377">"En aquests moments, no es pot accedir a aquesta aplicació al dispositiu <xliff:g id="DEVICE">%1$s</xliff:g>. Prova-ho al dispositiu Android TV."</string>
- <string name="app_streaming_blocked_message" product="tablet" msgid="4242053045964946062">"En aquests moments, no es pot accedir a aquesta aplicació al dispositiu <xliff:g id="DEVICE">%1$s</xliff:g>. Prova-ho a la tauleta."</string>
- <string name="app_streaming_blocked_message" product="default" msgid="6159168735030739398">"No es pot accedir a aquesta aplicació al teu <xliff:g id="DEVICE">%1$s</xliff:g>. Prova-ho al telèfon."</string>
+ <string name="app_streaming_blocked_message" product="tv" msgid="4003011766528814377">"En aquests moments, No s\'hi pot accedir des del teu <xliff:g id="DEVICE">%1$s</xliff:g>. Prova-ho al dispositiu Android TV."</string>
+ <string name="app_streaming_blocked_message" product="tablet" msgid="4242053045964946062">"En aquests moments, No s\'hi pot accedir des del teu <xliff:g id="DEVICE">%1$s</xliff:g>. Prova-ho a la tauleta."</string>
+ <string name="app_streaming_blocked_message" product="default" msgid="6159168735030739398">"No s\'hi pot accedir des del teu <xliff:g id="DEVICE">%1$s</xliff:g>. Prova-ho al telèfon."</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tv" msgid="3470977315395784567">"Aquesta aplicació sol·licita seguretat addicional. Prova-ho al dispositiu Android TV."</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tablet" msgid="698460091901465092">"Aquesta aplicació sol·licita seguretat addicional. Prova-ho a la tauleta."</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="default" msgid="8552691971910603907">"Aquesta aplicació sol·licita seguretat addicional. Prova-ho al telèfon."</string>
- <string name="app_streaming_blocked_message_for_settings_dialog" product="tv" msgid="820334666354451145">"No s\'hi pot accedir des del dispositiu <xliff:g id="DEVICE">%1$s</xliff:g>. Prova-ho al dispositiu Android TV."</string>
- <string name="app_streaming_blocked_message_for_settings_dialog" product="tablet" msgid="3286849551133045896">"No s\'hi pot accedir des del dispositiu <xliff:g id="DEVICE">%1$s</xliff:g>. Prova-ho a la tauleta."</string>
- <string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"No s\'hi pot accedir des del dispositiu <xliff:g id="DEVICE">%1$s</xliff:g>. Prova-ho al telèfon."</string>
+ <string name="app_streaming_blocked_message_for_settings_dialog" product="tv" msgid="820334666354451145">"No s\'hi pot accedir des del teu <xliff:g id="DEVICE">%1$s</xliff:g>. Prova-ho al dispositiu Android TV."</string>
+ <string name="app_streaming_blocked_message_for_settings_dialog" product="tablet" msgid="3286849551133045896">"No s\'hi pot accedir des del teu <xliff:g id="DEVICE">%1$s</xliff:g>. Prova-ho a la tauleta."</string>
+ <string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"No s\'hi pot accedir des del teu <xliff:g id="DEVICE">%1$s</xliff:g>. Prova-ho al telèfon."</string>
<string name="deprecated_target_sdk_message" msgid="5203207875657579953">"Aquesta aplicació es va crear per a una versió antiga d\'Android i pot ser que no funcioni correctament. Prova de cercar actualitzacions o contacta amb el desenvolupador."</string>
<string name="deprecated_target_sdk_app_store" msgid="8456784048558808909">"Cerca actualitzacions"</string>
<string name="new_sms_notification_title" msgid="6528758221319927107">"Tens missatges nous"</string>
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-es/strings.xml b/core/res/res/values-es/strings.xml
index 90f1006..fcbe2b8 100644
--- a/core/res/res/values-es/strings.xml
+++ b/core/res/res/values-es/strings.xml
@@ -1956,9 +1956,9 @@
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tv" msgid="3470977315395784567">"Esta aplicación solicita seguridad adicional. Prueba en tu dispositivo Android TV."</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tablet" msgid="698460091901465092">"Esta aplicación solicita seguridad adicional. Prueba en tu tablet."</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="default" msgid="8552691971910603907">"Esta aplicación solicita seguridad adicional. Prueba en tu teléfono."</string>
- <string name="app_streaming_blocked_message_for_settings_dialog" product="tv" msgid="820334666354451145">"No se puede acceder a este contenido en tu <xliff:g id="DEVICE">%1$s</xliff:g>. Prueba en tu dispositivo Android TV."</string>
+ <string name="app_streaming_blocked_message_for_settings_dialog" product="tv" msgid="820334666354451145">"No se puede acceder desde tu <xliff:g id="DEVICE">%1$s</xliff:g>. Prueba en tu dispositivo Android TV."</string>
<string name="app_streaming_blocked_message_for_settings_dialog" product="tablet" msgid="3286849551133045896">"No se puede acceder a este contenido en tu <xliff:g id="DEVICE">%1$s</xliff:g>. Prueba en tu tablet."</string>
- <string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"No se puede acceder a este contenido en tu <xliff:g id="DEVICE">%1$s</xliff:g>. Prueba en tu teléfono."</string>
+ <string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"No se puede acceder desde tu <xliff:g id="DEVICE">%1$s</xliff:g>. Prueba en tu teléfono."</string>
<string name="deprecated_target_sdk_message" msgid="5203207875657579953">"Esta aplicación se ha diseñado para una versión anterior de Android y es posible que no funcione correctamente. Busca actualizaciones o ponte en contacto con el desarrollador."</string>
<string name="deprecated_target_sdk_app_store" msgid="8456784048558808909">"Buscar actualizaciones"</string>
<string name="new_sms_notification_title" msgid="6528758221319927107">"Tienes mensajes nuevos"</string>
diff --git a/core/res/res/values-eu/strings.xml b/core/res/res/values-eu/strings.xml
index 15b25ad..681cc91 100644
--- a/core/res/res/values-eu/strings.xml
+++ b/core/res/res/values-eu/strings.xml
@@ -763,10 +763,10 @@
<string name="policydesc_watchLogin" product="tv" msgid="2140588224468517507">"Kontrolatu zenbat aldiz idatzi duzun oker pasahitza pantaila desblokeatzen saiatzean, eta blokeatu Android TV gailua edo ezabatu bertako datu guztiak pasahitza gehiegitan idazten baduzu oker."</string>
<string name="policydesc_watchLogin" product="automotive" msgid="7011438994051251521">"Kontrolatu zenbatetan idazten duzun pasahitza oker pantaila desblokeatzen saiatzean eta, gehiegitan idazten bada oker, blokeatu informazio- eta aisia-sistema edo ezabatu hango eduki guztia."</string>
<string name="policydesc_watchLogin" product="default" msgid="4885030206253600299">"Kontrolatu pantaila desblokeatzen saiatzean idatzitako pasahitz oker kopurua, eta blokeatu telefonoa edo ezabatu bere datuak pasahitza gehiegitan oker idazten bada."</string>
- <string name="policydesc_watchLogin_secondaryUser" product="tablet" msgid="2049038943004297474">"Kontrolatu pantaila desblokeatzen saiatzean idatzitako pasahitz oker kopurua, eta blokeatu tableta edo ezabatu erabiltzailearen datuak pasahitza gehiegitan oker idazten bada."</string>
- <string name="policydesc_watchLogin_secondaryUser" product="tv" msgid="8965224107449407052">"Kontrolatu zenbat aldiz idatzi duzun oker pasahitza pantaila desblokeatzen saiatzean, eta blokeatu Android TV gailua edo ezabatu erabiltzailearen datuak pasahitza gehiegitan idazten baduzu oker."</string>
+ <string name="policydesc_watchLogin_secondaryUser" product="tablet" msgid="2049038943004297474">"Kontrolatu pantaila desblokeatzen saiatzean idatzitako pasahitz oker kopurua, eta blokeatu tableta edo ezabatu erabiltzaile-datuak pasahitza gehiegitan oker idazten bada."</string>
+ <string name="policydesc_watchLogin_secondaryUser" product="tv" msgid="8965224107449407052">"Kontrolatu zenbat aldiz idatzi duzun oker pasahitza pantaila desblokeatzen saiatzean, eta blokeatu Android TV gailua edo ezabatu erabiltzaile-datuak pasahitza gehiegitan idazten baduzu oker."</string>
<string name="policydesc_watchLogin_secondaryUser" product="automotive" msgid="7180857406058327941">"Kontrolatu zenbatetan idazten duzun pasahitza oker pantaila desblokeatzen saiatzean eta, gehiegitan idazten bada oker, blokeatu informazio- eta aisia-sistema edo ezabatu profil honetako eduki guztia."</string>
- <string name="policydesc_watchLogin_secondaryUser" product="default" msgid="9177645136475155924">"Kontrolatu pantaila desblokeatzen saiatzean idatzitako pasahitz oker kopurua, eta blokeatu telefonoa edo ezabatu erabiltzailearen datuak pasahitza gehiegitan oker idazten bada."</string>
+ <string name="policydesc_watchLogin_secondaryUser" product="default" msgid="9177645136475155924">"Kontrolatu pantaila desblokeatzen saiatzean idatzitako pasahitz oker kopurua, eta blokeatu telefonoa edo ezabatu erabiltzaile-datuak pasahitza gehiegitan oker idazten bada."</string>
<string name="policylab_resetPassword" msgid="214556238645096520">"Aldatu pantailaren blokeoa"</string>
<string name="policydesc_resetPassword" msgid="4626419138439341851">"Aldatu pantailaren blokeoa."</string>
<string name="policylab_forceLock" msgid="7360335502968476434">"Blokeatu pantaila"</string>
@@ -777,7 +777,7 @@
<string name="policydesc_wipeData" product="automotive" msgid="660804547737323300">"Berrezarri informazio- eta aisia-sistemako jatorrizko datuak abisatu gabe, bertan zegoen eduki guztia ezabatzeko."</string>
<string name="policydesc_wipeData" product="default" msgid="8036084184768379022">"Ezabatu telefonoaren datuak abisatu gabe, jatorrizko datuak berrezarrita."</string>
<string name="policylab_wipeData_secondaryUser" product="automotive" msgid="115034358520328373">"Ezabatu profileko eduki guztia"</string>
- <string name="policylab_wipeData_secondaryUser" product="default" msgid="413813645323433166">"Ezabatu erabiltzailearen datuak"</string>
+ <string name="policylab_wipeData_secondaryUser" product="default" msgid="413813645323433166">"Ezabatu erabiltzaile-datuak"</string>
<string name="policydesc_wipeData_secondaryUser" product="tablet" msgid="2336676480090926470">"Ezabatu erabiltzaileak tabletan dituen datuak abisatu gabe."</string>
<string name="policydesc_wipeData_secondaryUser" product="tv" msgid="2293713284515865200">"Ezabatu erabiltzaileak Android TV gailuan dituen datuak abisatu gabe."</string>
<string name="policydesc_wipeData_secondaryUser" product="automotive" msgid="4658832487305780879">"Ezabatu informazio- eta aisia-sisteman dagoen profil honetako eduki guztia abisatu gabe."</string>
@@ -1134,7 +1134,7 @@
<string name="Midnight" msgid="8176019203622191377">"Gauerdia"</string>
<string name="elapsed_time_short_format_mm_ss" msgid="8689459651807876423">"<xliff:g id="MINUTES">%1$02d</xliff:g>:<xliff:g id="SECONDS">%2$02d</xliff:g>"</string>
<string name="elapsed_time_short_format_h_mm_ss" msgid="2302144714803345056">"<xliff:g id="HOURS">%1$d</xliff:g>:<xliff:g id="MINUTES">%2$02d</xliff:g>:<xliff:g id="SECONDS">%3$02d</xliff:g>"</string>
- <string name="selectAll" msgid="1532369154488982046">"Hautatu guztiak"</string>
+ <string name="selectAll" msgid="1532369154488982046">"Hautatu dena"</string>
<string name="cut" msgid="2561199725874745819">"Ebaki"</string>
<string name="copy" msgid="5472512047143665218">"Kopiatu"</string>
<string name="failed_to_copy_to_clipboard" msgid="725919885138539875">"Ezin izan da kopiatu arbelean"</string>
diff --git a/core/res/res/values-fr-rCA/strings.xml b/core/res/res/values-fr-rCA/strings.xml
index 104808b..b86ce04 100644
--- a/core/res/res/values-fr-rCA/strings.xml
+++ b/core/res/res/values-fr-rCA/strings.xml
@@ -1403,7 +1403,7 @@
<string name="ext_media_checking_notification_message" product="tv" msgid="7986154434946021415">"Analyse de l\'espace de stockage sur le support en cours…"</string>
<string name="ext_media_new_notification_title" msgid="3517407571407687677">"Nouveau périphérique <xliff:g id="NAME">%s</xliff:g>"</string>
<string name="ext_media_new_notification_title" product="automotive" msgid="9085349544984742727">"<xliff:g id="NAME">%s</xliff:g> ne fonctionne pas"</string>
- <string name="ext_media_new_notification_message" msgid="6095403121990786986">"Toucher pour configurer"</string>
+ <string name="ext_media_new_notification_message" msgid="6095403121990786986">"Touchez pour configurer"</string>
<string name="ext_media_new_notification_message" product="tv" msgid="216863352100263668">"Sélectionnez pour configurer"</string>
<string name="ext_media_new_notification_message" product="automotive" msgid="5140127881613227162">"Vous devrez peut-être reformater l\'appareil. Touchez pour l\'éjecter."</string>
<string name="ext_media_ready_notification_message" msgid="7509496364380197369">"Pour stocker des photos, des vidéos, de la musique et plus encore"</string>
@@ -1415,7 +1415,7 @@
<string name="ext_media_unmountable_notification_message" product="automotive" msgid="2274596120715020680">"Vous devrez peut-être reformater l\'appareil. Touchez pour l\'éjecter."</string>
<string name="ext_media_unsupported_notification_title" msgid="3487534182861251401">"<xliff:g id="NAME">%s</xliff:g> détecté"</string>
<string name="ext_media_unsupported_notification_title" product="automotive" msgid="6004193172658722381">"<xliff:g id="NAME">%s</xliff:g> ne fonctionne pas"</string>
- <string name="ext_media_unsupported_notification_message" msgid="8463636521459807981">"Toucher pour configurer ."</string>
+ <string name="ext_media_unsupported_notification_message" msgid="8463636521459807981">"Touchez pour configurer ."</string>
<string name="ext_media_unsupported_notification_message" product="tv" msgid="1595482802187036532">"Sélectionner pour configurer <xliff:g id="NAME">%s</xliff:g> dans un format pris en charge."</string>
<string name="ext_media_unsupported_notification_message" product="automotive" msgid="3412494732736336330">"Vous devrez peut-être reformater l\'appareil"</string>
<string name="ext_media_badremoval_notification_title" msgid="4114625551266196872">"Retrait inattendu de la mémoire « <xliff:g id="NAME">%s</xliff:g> »"</string>
diff --git a/core/res/res/values-fr/strings.xml b/core/res/res/values-fr/strings.xml
index 7f099b0..f8e9efb 100644
--- a/core/res/res/values-fr/strings.xml
+++ b/core/res/res/values-fr/strings.xml
@@ -580,7 +580,7 @@
<string name="biometric_or_screen_lock_dialog_default_subtitle" msgid="159539678371552009">"Utilisez la biométrie ou le verrouillage de l\'écran pour continuer"</string>
<string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Matériel biométrique indisponible"</string>
<string name="biometric_error_user_canceled" msgid="6732303949695293730">"Authentification annulée"</string>
- <string name="biometric_not_recognized" msgid="5106687642694635888">"Non reconnu"</string>
+ <string name="biometric_not_recognized" msgid="5106687642694635888">"Non reconnue"</string>
<string name="biometric_error_canceled" msgid="8266582404844179778">"Authentification annulée"</string>
<string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Aucun code, schéma ni mot de passe n\'est défini"</string>
<string name="biometric_error_generic" msgid="6784371929985434439">"Erreur d\'authentification"</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-hr/strings.xml b/core/res/res/values-hr/strings.xml
index fb95a1f..73cbf93 100644
--- a/core/res/res/values-hr/strings.xml
+++ b/core/res/res/values-hr/strings.xml
@@ -1210,7 +1210,7 @@
<string name="aerr_application_repeated" msgid="7804378743218496566">"Aplikacija <xliff:g id="APPLICATION">%1$s</xliff:g> neprekidno se ruši"</string>
<string name="aerr_process_repeated" msgid="1153152413537954974">"Postupak <xliff:g id="PROCESS">%1$s</xliff:g> neprekidno se ruši"</string>
<string name="aerr_restart" msgid="2789618625210505419">"Ponovo otvori aplikaciju"</string>
- <string name="aerr_report" msgid="3095644466849299308">"Pošalji povratne informacije"</string>
+ <string name="aerr_report" msgid="3095644466849299308">"Pošaljite povratne informacije"</string>
<string name="aerr_close" msgid="3398336821267021852">"Zatvori"</string>
<string name="aerr_mute" msgid="2304972923480211376">"Zanemari do ponovnog pokretanja uređaja"</string>
<string name="aerr_wait" msgid="3198677780474548217">"Čekaj"</string>
@@ -1951,14 +1951,14 @@
<string name="app_streaming_blocked_title_for_settings_dialog" product="tablet" msgid="8222710146267948647">"Postavke tableta nisu dostupne"</string>
<string name="app_streaming_blocked_title_for_settings_dialog" product="default" msgid="6895719984375299791">"Postavke telefona nisu dostupne"</string>
<string name="app_streaming_blocked_message" product="tv" msgid="4003011766528814377">"Trenutačno toj aplikaciji nije moguće pristupiti na vašem uređaju <xliff:g id="DEVICE">%1$s</xliff:g>. Pokušajte joj pristupiti na Android TV uređaju."</string>
- <string name="app_streaming_blocked_message" product="tablet" msgid="4242053045964946062">"Trenutačno toj aplikaciji nije moguće pristupiti na vašem uređaju <xliff:g id="DEVICE">%1$s</xliff:g>. Pokušajte joj pristupiti na svojem tabletu."</string>
- <string name="app_streaming_blocked_message" product="default" msgid="6159168735030739398">"Trenutačno toj aplikaciji nije moguće pristupiti na vašem uređaju <xliff:g id="DEVICE">%1$s</xliff:g>. Pokušajte joj pristupiti na svojem telefonu."</string>
+ <string name="app_streaming_blocked_message" product="tablet" msgid="4242053045964946062">"Trenutačno toj aplikaciji nije moguće pristupiti na vašem uređaju <xliff:g id="DEVICE">%1$s</xliff:g>. Pokušajte joj pristupiti na tabletu."</string>
+ <string name="app_streaming_blocked_message" product="default" msgid="6159168735030739398">"Trenutačno toj aplikaciji nije moguće pristupiti na vašem uređaju <xliff:g id="DEVICE">%1$s</xliff:g>. Pokušajte joj pristupiti na telefonu."</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tv" msgid="3470977315395784567">"Ta aplikacija zahtijeva dodatnu sigurnost. Pokušajte joj pristupiti na Android TV uređaju."</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tablet" msgid="698460091901465092">"Ta aplikacija zahtijeva dodatnu sigurnost. Pokušajte joj pristupiti na tabletu."</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="default" msgid="8552691971910603907">"Ta aplikacija zahtijeva dodatnu sigurnost. Pokušajte joj pristupiti na telefonu."</string>
<string name="app_streaming_blocked_message_for_settings_dialog" product="tv" msgid="820334666354451145">"Toj aplikaciji nije moguće pristupiti na vašem uređaju <xliff:g id="DEVICE">%1$s</xliff:g>. Pokušajte joj pristupiti na Android TV uređaju."</string>
- <string name="app_streaming_blocked_message_for_settings_dialog" product="tablet" msgid="3286849551133045896">"Toj aplikaciji nije moguće pristupiti na vašem uređaju <xliff:g id="DEVICE">%1$s</xliff:g>. Pokušajte joj pristupiti na svojem tabletu."</string>
- <string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"Toj aplikaciji nije moguće pristupiti na vašem uređaju <xliff:g id="DEVICE">%1$s</xliff:g>. Pokušajte joj pristupiti na svojem telefonu."</string>
+ <string name="app_streaming_blocked_message_for_settings_dialog" product="tablet" msgid="3286849551133045896">"Toj aplikaciji nije moguće pristupiti na vašem uređaju <xliff:g id="DEVICE">%1$s</xliff:g>. Pokušajte joj pristupiti na tabletu."</string>
+ <string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"Toj aplikaciji nije moguće pristupiti na vašem uređaju <xliff:g id="DEVICE">%1$s</xliff:g>. Pokušajte joj pristupiti na telefonu."</string>
<string name="deprecated_target_sdk_message" msgid="5203207875657579953">"Ova je aplikacija razvijena za stariju verziju Androida i možda neće funkcionirati pravilno. Potražite ažuriranja ili se obratite razvojnom programeru."</string>
<string name="deprecated_target_sdk_app_store" msgid="8456784048558808909">"Provjeri ažuriranja"</string>
<string name="new_sms_notification_title" msgid="6528758221319927107">"Imate nove poruke"</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-lv/strings.xml b/core/res/res/values-lv/strings.xml
index 688d6b2..13487f7 100644
--- a/core/res/res/values-lv/strings.xml
+++ b/core/res/res/values-lv/strings.xml
@@ -1958,7 +1958,7 @@
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="default" msgid="8552691971910603907">"Šī lietotne pieprasa papildu drošību. Mēģiniet tai piekļūt savā tālrunī."</string>
<string name="app_streaming_blocked_message_for_settings_dialog" product="tv" msgid="820334666354451145">"Šajā ierīcē (<xliff:g id="DEVICE">%1$s</xliff:g>) nevar piekļūt tālvadībai. Mēģiniet tai piekļūt savā Android TV ierīcē."</string>
<string name="app_streaming_blocked_message_for_settings_dialog" product="tablet" msgid="3286849551133045896">"Šajā ierīcē (<xliff:g id="DEVICE">%1$s</xliff:g>) nevar piekļūt tālvadībai. Mēģiniet tai piekļūt savā planšetdatorā."</string>
- <string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"Šajā ierīcē (<xliff:g id="DEVICE">%1$s</xliff:g>) nevar piekļūt tālvadībai. Mēģiniet tai piekļūt savā tālrunī."</string>
+ <string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"Ierīcē <xliff:g id="DEVICE">%1$s</xliff:g> nevar piekļūt šai funkcijai. Mēģiniet tai piekļūt tālrunī."</string>
<string name="deprecated_target_sdk_message" msgid="5203207875657579953">"Šī lietotne tika izstrādāta vecākai Android versijai un var nedarboties pareizi. Meklējiet atjauninājumus vai sazinieties ar izstrādātāju."</string>
<string name="deprecated_target_sdk_app_store" msgid="8456784048558808909">"Meklēt atjauninājumu"</string>
<string name="new_sms_notification_title" msgid="6528758221319927107">"Jums ir jaunas īsziņas."</string>
diff --git a/core/res/res/values-nb/strings.xml b/core/res/res/values-nb/strings.xml
index abc5867..eab1b3e 100644
--- a/core/res/res/values-nb/strings.xml
+++ b/core/res/res/values-nb/strings.xml
@@ -124,7 +124,7 @@
<string name="roamingTextSearching" msgid="5323235489657753486">"Leter etter tjeneste"</string>
<string name="wfcRegErrorTitle" msgid="3193072971584858020">"Kunne ikke konfigurere wifi-anrop"</string>
<string-array name="wfcOperatorErrorAlertMessages">
- <item msgid="468830943567116703">"For å ringe og sende meldinger over Wi-Fi, må du først be operatøren om å konfigurere denne tjenesten. Deretter slår du på wifi-anrop igjen fra Innstillinger. (Feilkode: <xliff:g id="CODE">%1$s</xliff:g>)"</item>
+ <item msgid="468830943567116703">"For å ringe og sende meldinger over Wifi, må du først be operatøren om å konfigurere denne tjenesten. Deretter slår du på wifi-anrop igjen fra Innstillinger. (Feilkode: <xliff:g id="CODE">%1$s</xliff:g>)"</item>
</string-array>
<string-array name="wfcOperatorErrorNotificationMessages">
<item msgid="4795145070505729156">"Problem med å registrere wifi-anrop med operatøren din: <xliff:g id="CODE">%1$s</xliff:g>"</item>
@@ -135,7 +135,7 @@
<string name="wfcSpnFormat_spn_wifi_calling_vo_hyphen" msgid="3836827895369365298">"<xliff:g id="SPN">%s</xliff:g>-Wifi-anrop"</string>
<string name="wfcSpnFormat_wlan_call" msgid="4895315549916165700">"WLAN-anrop"</string>
<string name="wfcSpnFormat_spn_wlan_call" msgid="255919245825481510">"<xliff:g id="SPN">%s</xliff:g> WLAN-anrop"</string>
- <string name="wfcSpnFormat_spn_wifi" msgid="7232899594327126970">"<xliff:g id="SPN">%s</xliff:g> Wi-Fi"</string>
+ <string name="wfcSpnFormat_spn_wifi" msgid="7232899594327126970">"<xliff:g id="SPN">%s</xliff:g> Wifi"</string>
<string name="wfcSpnFormat_wifi_calling_bar_spn" msgid="8383917598312067365">"Wifi-anrop | <xliff:g id="SPN">%s</xliff:g>"</string>
<string name="wfcSpnFormat_spn_vowifi" msgid="6865214948822061486">"<xliff:g id="SPN">%s</xliff:g> VoWifi"</string>
<string name="wfcSpnFormat_wifi_calling" msgid="6178935388378661755">"Wifi-anrop"</string>
@@ -143,9 +143,9 @@
<string name="wfcSpnFormat_wifi_calling_wo_hyphen" msgid="7178561009225028264">"Wifi-anrop"</string>
<string name="wfcSpnFormat_vowifi" msgid="8371335230890725606">"VoWifi"</string>
<string name="wifi_calling_off_summary" msgid="5626710010766902560">"Av"</string>
- <string name="wfc_mode_wifi_preferred_summary" msgid="1035175836270943089">"Ring via Wi-Fi"</string>
+ <string name="wfc_mode_wifi_preferred_summary" msgid="1035175836270943089">"Ring via Wifi"</string>
<string name="wfc_mode_cellular_preferred_summary" msgid="4958965609212575619">"Ring over mobilnettverk"</string>
- <string name="wfc_mode_wifi_only_summary" msgid="104951993894678665">"Bare Wi-Fi"</string>
+ <string name="wfc_mode_wifi_only_summary" msgid="104951993894678665">"Bare Wifi"</string>
<!-- no translation found for crossSimFormat_spn (9125246077491634262) -->
<skip />
<string name="crossSimFormat_spn_cross_sim_calling" msgid="5620807020002879057">"<xliff:g id="SPN">%s</xliff:g>-reserve for anrop"</string>
@@ -516,14 +516,14 @@
<string name="permdesc_changeNetworkState" msgid="649341947816898736">"Lar appen endre innstillingene for nettverkstilknytning."</string>
<string name="permlab_changeTetherState" msgid="9079611809931863861">"endre tilknytningsoppsett"</string>
<string name="permdesc_changeTetherState" msgid="3025129606422533085">"Lar appen endre innstillingene for delt nettforbindelse."</string>
- <string name="permlab_accessWifiState" msgid="5552488500317911052">"se Wi-Fi-tilkoblinger"</string>
- <string name="permdesc_accessWifiState" msgid="6913641669259483363">"Lar appen se informasjon om Wi-Fi-nettverk, f.eks. hvorvidt Wi-Fi er aktivert og navn på tilkoblede Wi-Fi-enheter."</string>
- <string name="permlab_changeWifiState" msgid="7947824109713181554">"koble til og fra Wi-Fi"</string>
- <string name="permdesc_changeWifiState" msgid="7170350070554505384">"Lar appen koble til og fra Wi-Fi-tilgangspunkter, og å gjøre endringer i enhetens konfigurasjon for Wi-Fi-nettverk."</string>
+ <string name="permlab_accessWifiState" msgid="5552488500317911052">"se Wifi-tilkoblinger"</string>
+ <string name="permdesc_accessWifiState" msgid="6913641669259483363">"Lar appen se informasjon om Wifi-nettverk, f.eks. hvorvidt Wifi er aktivert og navn på tilkoblede Wifi-enheter."</string>
+ <string name="permlab_changeWifiState" msgid="7947824109713181554">"koble til og fra wifi"</string>
+ <string name="permdesc_changeWifiState" msgid="7170350070554505384">"Lar appen koble til og fra wifi-tilgangspunkter, og å gjøre endringer i enhetens konfigurasjon for wifi-nettverk."</string>
<string name="permlab_changeWifiMulticastState" msgid="285626875870754696">"tillate multicast for trådløse nettverk"</string>
- <string name="permdesc_changeWifiMulticastState" product="tablet" msgid="191079868596433554">"Lar appen motta pakker som sendes til alle enhetene på et Wi-Fi-nettverk ved hjelp av multikastingsadresser, Dette bruker mer strøm enn modusen uten multikasting."</string>
- <string name="permdesc_changeWifiMulticastState" product="tv" msgid="1336952358450652595">"Lar appen motta pakker som sendes til alle enhetene på et Wi-Fi-nettverk ved hjelp av multikastingsadresser, ikke bare Android TV-enheten din. Dette bruker mer strøm enn modus uten multikasting."</string>
- <string name="permdesc_changeWifiMulticastState" product="default" msgid="8296627590220222740">"Lar appen motta pakker som sendes til alle enhetene på et Wi-Fi-nettverk ved hjelp av multikastingsadresser, Dette bruker mer strøm enn modusen uten multikasting."</string>
+ <string name="permdesc_changeWifiMulticastState" product="tablet" msgid="191079868596433554">"Lar appen motta pakker som sendes til alle enhetene på et Wifi-nettverk ved hjelp av multikastingsadresser, Dette bruker mer strøm enn modusen uten multikasting."</string>
+ <string name="permdesc_changeWifiMulticastState" product="tv" msgid="1336952358450652595">"Lar appen motta pakker som sendes til alle enhetene på et Wifi-nettverk ved hjelp av multikastingsadresser, ikke bare Android TV-enheten din. Dette bruker mer strøm enn modus uten multikasting."</string>
+ <string name="permdesc_changeWifiMulticastState" product="default" msgid="8296627590220222740">"Lar appen motta pakker som sendes til alle enhetene på et Wifi-nettverk ved hjelp av multikastingsadresser, Dette bruker mer strøm enn modusen uten multikasting."</string>
<string name="permlab_bluetoothAdmin" msgid="6490373569441946064">"endre Bluetooth-innstillinger"</string>
<string name="permdesc_bluetoothAdmin" product="tablet" msgid="5370837055438574863">"Lar appen konfigurere det lokale Bluetooth-nettbrettet, samt oppdage og koble sammen med eksterne enheter."</string>
<string name="permdesc_bluetoothAdmin" product="tv" msgid="1623992984547014588">"Lar appen konfigurere Bluetooth på Android TV-enheten din samt oppdage og koble sammen med eksterne enheter."</string>
@@ -546,8 +546,8 @@
<string name="permdesc_bluetooth_advertise" product="default" msgid="6085174451034210183">"Lar appen vise annonser til Bluetooth-enheter i nærheten"</string>
<string name="permlab_uwb_ranging" msgid="8141915781475770665">"fastslå relativ posisjon mellom enheter som bruker ultrabredbånd"</string>
<string name="permdesc_uwb_ranging" msgid="2519723069604307055">"tillate at appen fastslår den relative posisjonen mellom enheter i nærheten som bruker ultrabredbånd"</string>
- <string name="permlab_nearby_wifi_devices" msgid="392774237063608500">"samhandle med Wi-Fi-enheter i nærheten"</string>
- <string name="permdesc_nearby_wifi_devices" msgid="3054307728646332906">"Lar appen annonsere, koble til og fastslå den relative posisjonen til Wi-Fi-enheter i nærheten"</string>
+ <string name="permlab_nearby_wifi_devices" msgid="392774237063608500">"samhandle med wifi-enheter i nærheten"</string>
+ <string name="permdesc_nearby_wifi_devices" msgid="3054307728646332906">"Lar appen annonsere, koble til og fastslå den relative posisjonen til wifi-enheter i nærheten"</string>
<string name="permlab_preferredPaymentInfo" msgid="5274423844767445054">"Informasjon om prioritert NFC-betalingstjeneste"</string>
<string name="permdesc_preferredPaymentInfo" msgid="8583552469807294967">"Gir appen tilgang til informasjon om prioritert NFC-betalingstjeneste, for eksempel registrerte hjelpemidler og destinasjon."</string>
<string name="permlab_nfc" msgid="1904455246837674977">"kontroller overføring av data med NFC-teknologi"</string>
@@ -1293,7 +1293,7 @@
<string name="ringtone_picker_title_alarm" msgid="7438934548339024767">"Alarmlyder"</string>
<string name="ringtone_picker_title_notification" msgid="6387191794719608122">"Varsellyder"</string>
<string name="ringtone_unknown" msgid="5059495249862816475">"Ukjent"</string>
- <string name="wifi_available_sign_in" msgid="381054692557675237">"Logg på Wi-Fi-nettverket"</string>
+ <string name="wifi_available_sign_in" msgid="381054692557675237">"Logg på Wifi-nettverket"</string>
<string name="network_available_sign_in" msgid="1520342291829283114">"Logg på nettverk"</string>
<!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
<skip />
@@ -1576,10 +1576,10 @@
<string name="data_usage_warning_title" msgid="9034893717078325845">"Varsel om databruk"</string>
<string name="data_usage_warning_body" msgid="1669325367188029454">"Du har brukt <xliff:g id="APP">%s</xliff:g> med data"</string>
<string name="data_usage_mobile_limit_title" msgid="3911447354393775241">"Grensen for mobildata er nådd"</string>
- <string name="data_usage_wifi_limit_title" msgid="2069698056520812232">"Datagrensen for Wi-Fi er nådd"</string>
+ <string name="data_usage_wifi_limit_title" msgid="2069698056520812232">"Datagrensen for wifi er nådd"</string>
<string name="data_usage_limit_body" msgid="3567699582000085710">"Data er på pause i resten av syklusen"</string>
<string name="data_usage_mobile_limit_snoozed_title" msgid="101888478915677895">"Over grensen for mobildata"</string>
- <string name="data_usage_wifi_limit_snoozed_title" msgid="1622359254521960508">"Over grensen din for Wi-Fi-data"</string>
+ <string name="data_usage_wifi_limit_snoozed_title" msgid="1622359254521960508">"Over grensen din for wifi-data"</string>
<string name="data_usage_limit_snoozed_body" msgid="545146591766765678">"Du er <xliff:g id="SIZE">%s</xliff:g> over den angitte grensen din"</string>
<string name="data_usage_restricted_title" msgid="126711424380051268">"Bakgrunnsdata er begrenset"</string>
<string name="data_usage_restricted_body" msgid="5338694433686077733">"Trykk for å fjerne begrensningen."</string>
diff --git a/core/res/res/values-pt-rBR/strings.xml b/core/res/res/values-pt-rBR/strings.xml
index c1c0d54..73a5978 100644
--- a/core/res/res/values-pt-rBR/strings.xml
+++ b/core/res/res/values-pt-rBR/strings.xml
@@ -296,7 +296,7 @@
<string name="foreground_service_multiple_separator" msgid="5002287361849863168">"<xliff:g id="LEFT_SIDE">%1$s</xliff:g>, <xliff:g id="RIGHT_SIDE">%2$s</xliff:g>"</string>
<string name="safeMode" msgid="8974401416068943888">"Modo de segurança"</string>
<string name="android_system_label" msgid="5974767339591067210">"Sistema Android"</string>
- <string name="user_owner_label" msgid="8628726904184471211">"Deslize até o perfil pessoal"</string>
+ <string name="user_owner_label" msgid="8628726904184471211">"Mudar para o perfil pessoal"</string>
<string name="managed_profile_label" msgid="7316778766973512382">"Perfil de trabalho"</string>
<string name="permgrouplab_contacts" msgid="4254143639307316920">"Contatos"</string>
<string name="permgroupdesc_contacts" msgid="9163927941244182567">"acesse seus contatos"</string>
@@ -749,7 +749,7 @@
<string name="permdesc_bindCarrierServices" msgid="9185614481967262900">"Permite que o proprietário use serviços da operadora. Não deve ser necessário para apps comuns."</string>
<string name="permlab_access_notification_policy" msgid="5524112842876975537">"acessar \"Não perturbe\""</string>
<string name="permdesc_access_notification_policy" msgid="8538374112403845013">"Permitir que o app leia e grave a configuração \"Não perturbe\"."</string>
- <string name="permlab_startViewPermissionUsage" msgid="1504564328641112341">"iniciar uso da permissão para visualização"</string>
+ <string name="permlab_startViewPermissionUsage" msgid="1504564328641112341">"começar a usar a permissão para ver"</string>
<string name="permdesc_startViewPermissionUsage" msgid="2820325605959586538">"Permite que o sistema inicie o uso de permissão para um app. Não deve ser necessário para apps comuns."</string>
<string name="permlab_startReviewPermissionDecisions" msgid="8690578688476599284">"decisões de permissão da visualização inicial"</string>
<string name="permdesc_startReviewPermissionDecisions" msgid="2775556853503004236">"Autoriza o detentor a iniciar a tela para revisar as decisões de permissão. Não deve ser necessário para apps normais."</string>
diff --git a/core/res/res/values-pt-rPT/strings.xml b/core/res/res/values-pt-rPT/strings.xml
index 1fe6e1c..0efbc73 100644
--- a/core/res/res/values-pt-rPT/strings.xml
+++ b/core/res/res/values-pt-rPT/strings.xml
@@ -164,7 +164,7 @@
<string name="httpErrorAuth" msgid="469553140922938968">"Não foi possível autenticar."</string>
<string name="httpErrorProxyAuth" msgid="7229662162030113406">"A autenticação através do servidor proxy falhou."</string>
<string name="httpErrorConnect" msgid="3295081579893205617">"Não foi possível ligar ao servidor."</string>
- <string name="httpErrorIO" msgid="3860318696166314490">"Não foi possível comunicar com o servidor. Tente novamente mais tarde."</string>
+ <string name="httpErrorIO" msgid="3860318696166314490">"Não foi possível comunicar com o servidor. Tente mais tarde."</string>
<string name="httpErrorTimeout" msgid="7446272815190334204">"Esgotou o tempo limite da ligação ao servidor."</string>
<string name="httpErrorRedirectLoop" msgid="8455757777509512098">"A página contém demasiados redireccionamentos do servidor."</string>
<string name="httpErrorUnsupportedScheme" msgid="2664108769858966374">"O protocolo não é suportado."</string>
@@ -172,7 +172,7 @@
<string name="httpErrorBadUrl" msgid="754447723314832538">"Não foi possível abrir a página porque o URL é inválido."</string>
<string name="httpErrorFile" msgid="3400658466057744084">"Não foi possível aceder ao ficheiro."</string>
<string name="httpErrorFileNotFound" msgid="5191433324871147386">"Não foi possível localizar o ficheiro solicitado."</string>
- <string name="httpErrorTooManyRequests" msgid="2149677715552037198">"Existem demasiados pedidos em processamento. Tente novamente mais tarde."</string>
+ <string name="httpErrorTooManyRequests" msgid="2149677715552037198">"Existem demasiados pedidos em processamento. Tente mais tarde."</string>
<string name="notification_title" msgid="5783748077084481121">"Erro de início de sessão de <xliff:g id="ACCOUNT">%1$s</xliff:g>"</string>
<string name="contentServiceSync" msgid="2341041749565687871">"Sincronização"</string>
<string name="contentServiceSyncNotificationTitle" msgid="5766411446676388623">"Não é possível sincronizar"</string>
@@ -671,7 +671,7 @@
<string name="face_error_no_space" msgid="5649264057026021723">"Não pode guardar novos dados de rostos. Elimine um antigo."</string>
<string name="face_error_canceled" msgid="2164434737103802131">"Operação de rosto cancelada."</string>
<string name="face_error_user_canceled" msgid="5766472033202928373">"Desbloqueio facial cancelado pelo utilizador"</string>
- <string name="face_error_lockout" msgid="7864408714994529437">"Demasiadas tentativas. Tente novamente mais tarde."</string>
+ <string name="face_error_lockout" msgid="7864408714994529437">"Demasiadas tentativas. Tente mais tarde."</string>
<string name="face_error_lockout_permanent" msgid="3277134834042995260">"Demasiadas tentativas. O Desbloqueio facial foi desativado."</string>
<string name="face_error_lockout_screen_lock" msgid="5062609811636860928">"Demasiadas tentativas. Em alternativa, introduza o bloqueio de ecrã."</string>
<string name="face_error_unable_to_process" msgid="5723292697366130070">"Não é possível validar o rosto. Tente novamente."</string>
@@ -1836,7 +1836,7 @@
<string name="restr_pin_create_pin" msgid="917067613896366033">"Crie um PIN para modificar as restrições"</string>
<string name="restr_pin_error_doesnt_match" msgid="7063392698489280556">"Os PINs não correspondem. Tente novamente."</string>
<string name="restr_pin_error_too_short" msgid="1547007808237941065">"O PIN é demasiado pequeno. Deve ter, no mínimo, 4 dígitos."</string>
- <string name="restr_pin_try_later" msgid="5897719962541636727">"Tente novamente mais tarde"</string>
+ <string name="restr_pin_try_later" msgid="5897719962541636727">"Tente mais tarde"</string>
<string name="immersive_cling_title" msgid="2307034298721541791">"Visualização de ecrã inteiro"</string>
<string name="immersive_cling_description" msgid="7092737175345204832">"Para sair, deslize rapidamente para baixo a partir da parte superior."</string>
<string name="immersive_cling_positive" msgid="7047498036346489883">"OK"</string>
diff --git a/core/res/res/values-pt/strings.xml b/core/res/res/values-pt/strings.xml
index c1c0d54..73a5978 100644
--- a/core/res/res/values-pt/strings.xml
+++ b/core/res/res/values-pt/strings.xml
@@ -296,7 +296,7 @@
<string name="foreground_service_multiple_separator" msgid="5002287361849863168">"<xliff:g id="LEFT_SIDE">%1$s</xliff:g>, <xliff:g id="RIGHT_SIDE">%2$s</xliff:g>"</string>
<string name="safeMode" msgid="8974401416068943888">"Modo de segurança"</string>
<string name="android_system_label" msgid="5974767339591067210">"Sistema Android"</string>
- <string name="user_owner_label" msgid="8628726904184471211">"Deslize até o perfil pessoal"</string>
+ <string name="user_owner_label" msgid="8628726904184471211">"Mudar para o perfil pessoal"</string>
<string name="managed_profile_label" msgid="7316778766973512382">"Perfil de trabalho"</string>
<string name="permgrouplab_contacts" msgid="4254143639307316920">"Contatos"</string>
<string name="permgroupdesc_contacts" msgid="9163927941244182567">"acesse seus contatos"</string>
@@ -749,7 +749,7 @@
<string name="permdesc_bindCarrierServices" msgid="9185614481967262900">"Permite que o proprietário use serviços da operadora. Não deve ser necessário para apps comuns."</string>
<string name="permlab_access_notification_policy" msgid="5524112842876975537">"acessar \"Não perturbe\""</string>
<string name="permdesc_access_notification_policy" msgid="8538374112403845013">"Permitir que o app leia e grave a configuração \"Não perturbe\"."</string>
- <string name="permlab_startViewPermissionUsage" msgid="1504564328641112341">"iniciar uso da permissão para visualização"</string>
+ <string name="permlab_startViewPermissionUsage" msgid="1504564328641112341">"começar a usar a permissão para ver"</string>
<string name="permdesc_startViewPermissionUsage" msgid="2820325605959586538">"Permite que o sistema inicie o uso de permissão para um app. Não deve ser necessário para apps comuns."</string>
<string name="permlab_startReviewPermissionDecisions" msgid="8690578688476599284">"decisões de permissão da visualização inicial"</string>
<string name="permdesc_startReviewPermissionDecisions" msgid="2775556853503004236">"Autoriza o detentor a iniciar a tela para revisar as decisões de permissão. Não deve ser necessário para apps normais."</string>
diff --git a/core/res/res/values-ru/strings.xml b/core/res/res/values-ru/strings.xml
index 86115d5..c2fe6dd 100644
--- a/core/res/res/values-ru/strings.xml
+++ b/core/res/res/values-ru/strings.xml
@@ -1953,13 +1953,13 @@
<string name="app_streaming_blocked_title_for_settings_dialog" product="default" msgid="6895719984375299791">"Настройки телефона недоступны"</string>
<string name="app_streaming_blocked_message" product="tv" msgid="4003011766528814377">"Эта функция пока недоступна на устройстве <xliff:g id="DEVICE">%1$s</xliff:g>. Используйте Android TV."</string>
<string name="app_streaming_blocked_message" product="tablet" msgid="4242053045964946062">"Эта функция пока недоступна на устройстве <xliff:g id="DEVICE">%1$s</xliff:g>. Используйте планшет."</string>
- <string name="app_streaming_blocked_message" product="default" msgid="6159168735030739398">"Эта функция пока недоступна на устройстве <xliff:g id="DEVICE">%1$s</xliff:g>. Используйте телефон."</string>
+ <string name="app_streaming_blocked_message" product="default" msgid="6159168735030739398">"На устройстве <xliff:g id="DEVICE">%1$s</xliff:g> эта функция пока недоступна. Используйте телефон."</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tv" msgid="3470977315395784567">"Это приложение запрашивает дополнительные меры защиты. Используйте Android TV."</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tablet" msgid="698460091901465092">"Это приложение запрашивает дополнительные меры защиты. Используйте планшет."</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="default" msgid="8552691971910603907">"Это приложение запрашивает дополнительные меры защиты. Используйте телефон."</string>
<string name="app_streaming_blocked_message_for_settings_dialog" product="tv" msgid="820334666354451145">"Эта функция недоступна на устройстве <xliff:g id="DEVICE">%1$s</xliff:g>. Используйте Android TV."</string>
<string name="app_streaming_blocked_message_for_settings_dialog" product="tablet" msgid="3286849551133045896">"Эта функция недоступна на устройстве <xliff:g id="DEVICE">%1$s</xliff:g>. Используйте планшет."</string>
- <string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"Эта функция недоступна на устройстве <xliff:g id="DEVICE">%1$s</xliff:g>. Используйте телефон."</string>
+ <string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"На устройстве <xliff:g id="DEVICE">%1$s</xliff:g> эта функция недоступна. Используйте телефон."</string>
<string name="deprecated_target_sdk_message" msgid="5203207875657579953">"Это приложение было создано для более ранней версии Android и может работать со сбоями. Проверьте наличие обновлений или свяжитесь с разработчиком."</string>
<string name="deprecated_target_sdk_app_store" msgid="8456784048558808909">"Проверить обновления"</string>
<string name="new_sms_notification_title" msgid="6528758221319927107">"Новые сообщения"</string>
diff --git a/core/res/res/values-te/strings.xml b/core/res/res/values-te/strings.xml
index 131ccd0..46a5ca4 100644
--- a/core/res/res/values-te/strings.xml
+++ b/core/res/res/values-te/strings.xml
@@ -499,7 +499,7 @@
<string name="permlab_setWallpaper" msgid="6959514622698794511">"వాల్పేపర్ను సెట్ చేయడం"</string>
<string name="permdesc_setWallpaper" msgid="2973996714129021397">"సిస్టమ్ వాల్పేపర్ను సెట్ చేయడానికి యాప్ను అనుమతిస్తుంది."</string>
<string name="permlab_setWallpaperHints" msgid="1153485176642032714">"మీ వాల్పేపర్ పరిమాణాన్ని సర్దుబాటు చేయడం"</string>
- <string name="permdesc_setWallpaperHints" msgid="6257053376990044668">"సిస్టమ్ వాల్పేపర్ పరిమాణం సూచనలను సెట్ చేయడానికి యాప్ను అనుమతిస్తుంది."</string>
+ <string name="permdesc_setWallpaperHints" msgid="6257053376990044668">"సిస్టమ్ వాల్పేపర్ సైజ్ సూచనలను సెట్ చేయడానికి యాప్ను అనుమతిస్తుంది."</string>
<string name="permlab_setTimeZone" msgid="7922618798611542432">"సమయ మండలిని సెట్ చేయడం"</string>
<string name="permdesc_setTimeZone" product="tablet" msgid="1788868809638682503">"టాబ్లెట్ యొక్క సమయ మండలిని మార్చడానికి యాప్ను అనుమతిస్తుంది."</string>
<string name="permdesc_setTimeZone" product="tv" msgid="9069045914174455938">"మీ Android TV పరికరం సమయ మండలిని మార్చడానికి యాప్ని అనుమతిస్తుంది."</string>
@@ -560,7 +560,7 @@
<string name="permdesc_postNotification" msgid="5974977162462877075">"నోటిఫికేషన్లను చూపించడానికి యాప్ను అనుమతించండి"</string>
<string name="permlab_useBiometric" msgid="6314741124749633786">"బయోమెట్రిక్ హార్డ్వేర్ని ఉపయోగించు"</string>
<string name="permdesc_useBiometric" msgid="7502858732677143410">"ప్రమాణీకరణ కోసం బయోమెట్రిక్ హార్డ్వేర్ను ఉపయోగించడానికి యాప్ని అనుమతిస్తుంది"</string>
- <string name="permlab_manageFingerprint" msgid="7432667156322821178">"వేలిముద్ర హార్డ్వేర్ని నిర్వహించడానికి అనుమతి"</string>
+ <string name="permlab_manageFingerprint" msgid="7432667156322821178">"వేలిముద్ర హార్డ్వేర్ని మేనేజ్ చేయడానికి అనుమతి"</string>
<string name="permdesc_manageFingerprint" msgid="2025616816437339865">"వినియోగం కోసం వేలిముద్ర టెంప్లేట్లను జోడించే, తొలగించే పద్ధతులను అమలు చేయడానికి యాప్ను అనుమతిస్తుంది."</string>
<string name="permlab_useFingerprint" msgid="1001421069766751922">"వేలిముద్ర హార్డ్వేర్ని ఉపయోగించడానికి అనుమతి"</string>
<string name="permdesc_useFingerprint" msgid="412463055059323742">"ప్రామాణీకరణ కోసం వేలిముద్ర హార్డ్వేర్ను ఉపయోగించడానికి యాప్ను అనుమతిస్తుంది"</string>
@@ -709,7 +709,7 @@
<string name="permlab_register_call_provider" msgid="6135073566140050702">"కొత్త టెలికామ్ కనెక్షన్లను నమోదు చేయడం"</string>
<string name="permdesc_register_call_provider" msgid="4201429251459068613">"కొత్త టెలికామ్ కనెక్షన్లను నమోదు చేయడానికి యాప్ను అనుమతిస్తుంది."</string>
<string name="permlab_connection_manager" msgid="3179365584691166915">"టెలికామ్ కనెక్షన్లను నిర్వహించడం"</string>
- <string name="permdesc_connection_manager" msgid="1426093604238937733">"టెలికామ్ కనెక్షన్లను నిర్వహించడానికి యాప్ను అనుమతిస్తుంది."</string>
+ <string name="permdesc_connection_manager" msgid="1426093604238937733">"టెలికామ్ కనెక్షన్లను మేనేజ్ చేయడానికి యాప్ను అనుమతిస్తుంది."</string>
<string name="permlab_bind_incall_service" msgid="5990625112603493016">"ఇన్-కాల్ స్క్రీన్తో పరస్పర చర్య చేయడం"</string>
<string name="permdesc_bind_incall_service" msgid="4124917526967765162">"వినియోగదారునికి ఇన్-కాల్ స్క్రీన్ ఎప్పుడు, ఎలా కనిపించాలనే దాన్ని నియంత్రించడానికి యాప్ను అనుమతిస్తుంది."</string>
<string name="permlab_bind_connection_service" msgid="5409268245525024736">"టెలిఫోన్ సేవలతో పరస్పర చర్య చేయడం"</string>
@@ -719,7 +719,7 @@
<string name="permlab_readNetworkUsageHistory" msgid="8470402862501573795">"చారిత్రక నెట్వర్క్ వినియోగాన్ని చదవడం"</string>
<string name="permdesc_readNetworkUsageHistory" msgid="1112962304941637102">"నిర్దిష్ట నెట్వర్క్లు మరియు యాప్ల కోసం చారిత్రాత్మక నెట్వర్క్ వినియోగాన్ని చదవడానికి యాప్ను అనుమతిస్తుంది."</string>
<string name="permlab_manageNetworkPolicy" msgid="6872549423152175378">"నెట్వర్క్ విధానాన్ని నిర్వహించడం"</string>
- <string name="permdesc_manageNetworkPolicy" msgid="1865663268764673296">"నెట్వర్క్ విధానాలను నిర్వహించడానికి మరియు యాప్-నిర్దిష్ట నిబంధనలను నిర్వచించడానికి యాప్ను అనుమతిస్తుంది."</string>
+ <string name="permdesc_manageNetworkPolicy" msgid="1865663268764673296">"నెట్వర్క్ విధానాలను మేనేజ్ చేయడానికి మరియు యాప్-నిర్దిష్ట నిబంధనలను నిర్వచించడానికి యాప్ను అనుమతిస్తుంది."</string>
<string name="permlab_modifyNetworkAccounting" msgid="7448790834938749041">"నెట్వర్క్ వినియోగ అకౌంటింగ్ను ఎడిట్ చేయడం"</string>
<string name="permdesc_modifyNetworkAccounting" msgid="5076042642247205390">"యాప్లలో నెట్వర్క్ వినియోగం ఎలా గణించాలనే దాన్ని ఎడిట్ చేయడానికి యాప్ను అనుమతిస్తుంది. సాధారణ యాప్ల ద్వారా ఉపయోగించడానికి ఉద్దేశించినది కాదు."</string>
<string name="permlab_accessNotifications" msgid="7130360248191984741">"నోటిఫికేషన్లను యాక్సెస్ చేయడం"</string>
@@ -1446,7 +1446,7 @@
<string name="ext_media_status_ejecting" msgid="7532403368044013797">"తొలగిస్తోంది…"</string>
<string name="ext_media_status_formatting" msgid="774148701503179906">"ఫార్మాట్ చేస్తోంది..."</string>
<string name="ext_media_status_missing" msgid="6520746443048867314">"చొప్పించబడలేదు"</string>
- <string name="activity_list_empty" msgid="4219430010716034252">"సరిపోలే కార్యాచరణలు కనుగొనబడలేదు."</string>
+ <string name="activity_list_empty" msgid="4219430010716034252">"మ్యాచ్ అయ్యే కార్యాచరణలు కనుగొనబడలేదు."</string>
<string name="permlab_route_media_output" msgid="8048124531439513118">"మీడియా అవుట్పుట్ను మళ్లించడం"</string>
<string name="permdesc_route_media_output" msgid="1759683269387729675">"మీడియా అవుట్పుట్ను ఇతర బాహ్య పరికరాలకు మళ్లించడానికి యాప్ను అనుమతిస్తుంది."</string>
<string name="permlab_readInstallSessions" msgid="7279049337895583621">"ఇన్స్టాల్ సెషన్లను చదవడం"</string>
@@ -1491,8 +1491,8 @@
<string name="notification_ranker_binding_label" msgid="432708245635563763">"నోటిఫికేషన్ ర్యాంకర్ సేవ"</string>
<string name="vpn_title" msgid="5906991595291514182">"VPN సక్రియం చేయబడింది"</string>
<string name="vpn_title_long" msgid="6834144390504619998">"<xliff:g id="APP">%s</xliff:g> ద్వారా VPN సక్రియం చేయబడింది"</string>
- <string name="vpn_text" msgid="2275388920267251078">"నెట్వర్క్ను నిర్వహించడానికి నొక్కండి."</string>
- <string name="vpn_text_long" msgid="278540576806169831">"<xliff:g id="SESSION">%s</xliff:g>కు కనెక్ట్ చేయబడింది. నెట్వర్క్ను నిర్వహించడానికి నొక్కండి."</string>
+ <string name="vpn_text" msgid="2275388920267251078">"నెట్వర్క్ను మేనేజ్ చేయడానికి నొక్కండి."</string>
+ <string name="vpn_text_long" msgid="278540576806169831">"<xliff:g id="SESSION">%s</xliff:g>కు కనెక్ట్ చేయబడింది. నెట్వర్క్ను మేనేజ్ చేయడానికి నొక్కండి."</string>
<string name="vpn_lockdown_connecting" msgid="6096725311950342607">"ఎల్లప్పుడూ-ఆన్లో ఉండే VPN కనెక్ట్ చేయబడుతోంది…"</string>
<string name="vpn_lockdown_connected" msgid="2853127976590658469">"ఎల్లప్పుడూ-ఆన్లో ఉండే VPN కనెక్ట్ చేయబడింది"</string>
<string name="vpn_lockdown_disconnected" msgid="5573611651300764955">"ఎల్లప్పుడూ ఆన్లో ఉండే VPN నుండి డిస్కనెక్ట్ చేయబడింది"</string>
@@ -1723,7 +1723,7 @@
<string name="guest_name" msgid="8502103277839834324">"గెస్ట్"</string>
<string name="error_message_title" msgid="4082495589294631966">"ఎర్రర్"</string>
<string name="error_message_change_not_allowed" msgid="843159705042381454">"ఈ మార్పును మీ నిర్వాహకులు అనుమతించలేదు"</string>
- <string name="app_not_found" msgid="3429506115332341800">"ఈ చర్యను నిర్వహించడానికి యాప్ ఏదీ కనుగొనబడలేదు"</string>
+ <string name="app_not_found" msgid="3429506115332341800">"ఈ చర్యను మేనేజ్ చేయడానికి యాప్ ఏదీ కనుగొనబడలేదు"</string>
<string name="revoke" msgid="5526857743819590458">"ఉపసంహరించండి"</string>
<string name="mediasize_iso_a0" msgid="7039061159929977973">"ISO A0"</string>
<string name="mediasize_iso_a1" msgid="4063589931031977223">"ISO A1"</string>
diff --git a/core/res/res/values-tl/strings.xml b/core/res/res/values-tl/strings.xml
index 7bb0ba9..ea26a15 100644
--- a/core/res/res/values-tl/strings.xml
+++ b/core/res/res/values-tl/strings.xml
@@ -988,7 +988,7 @@
<string name="keyguard_accessibility_unlock_area_collapsed" msgid="4729922043778400434">"Pinaliit ang bahagi ng pag-unlock."</string>
<string name="keyguard_accessibility_widget" msgid="6776892679715699875">"<xliff:g id="WIDGET_INDEX">%1$s</xliff:g> widget."</string>
<string name="keyguard_accessibility_user_selector" msgid="1466067610235696600">"Tagapili ng user"</string>
- <string name="keyguard_accessibility_status" msgid="6792745049712397237">"Katayuan"</string>
+ <string name="keyguard_accessibility_status" msgid="6792745049712397237">"Status"</string>
<string name="keyguard_accessibility_camera" msgid="7862557559464986528">"Camera"</string>
<string name="keygaurd_accessibility_media_controls" msgid="2267379779900620614">"Mga kontrol ng media"</string>
<string name="keyguard_accessibility_widget_reorder_start" msgid="7066213328912939191">"Nagsimula na ang pagbabago ng ayos ng widget."</string>
diff --git a/core/res/res/values-zh-rCN/strings.xml b/core/res/res/values-zh-rCN/strings.xml
index d46bdc8..d0d585a 100644
--- a/core/res/res/values-zh-rCN/strings.xml
+++ b/core/res/res/values-zh-rCN/strings.xml
@@ -1562,7 +1562,7 @@
<string name="content_description_sliding_handle" msgid="982510275422590757">"滑动手柄。触摸并按住。"</string>
<string name="description_target_unlock_tablet" msgid="7431571180065859551">"滑动解锁。"</string>
<string name="action_bar_home_description" msgid="1501655419158631974">"导航首页"</string>
- <string name="action_bar_up_description" msgid="6611579697195026932">"向上导航"</string>
+ <string name="action_bar_up_description" msgid="6611579697195026932">"返回"</string>
<string name="action_menu_overflow_description" msgid="4579536843510088170">"更多选项"</string>
<string name="action_bar_home_description_format" msgid="5087107531331621803">"%1$s:%2$s"</string>
<string name="action_bar_home_subtitle_description_format" msgid="4346835454749569826">"%1$s - %2$s:%3$s"</string>
diff --git a/core/res/res/values-zh-rTW/strings.xml b/core/res/res/values-zh-rTW/strings.xml
index 43de5ba..8f72050 100644
--- a/core/res/res/values-zh-rTW/strings.xml
+++ b/core/res/res/values-zh-rTW/strings.xml
@@ -1950,13 +1950,13 @@
<string name="app_streaming_blocked_title_for_settings_dialog" product="tablet" msgid="8222710146267948647">"無法使用平板電腦設定"</string>
<string name="app_streaming_blocked_title_for_settings_dialog" product="default" msgid="6895719984375299791">"無法使用手機設定"</string>
<string name="app_streaming_blocked_message" product="tv" msgid="4003011766528814377">"目前無法在 <xliff:g id="DEVICE">%1$s</xliff:g> 上存取這個應用程式,請改用 Android TV 裝置。"</string>
- <string name="app_streaming_blocked_message" product="tablet" msgid="4242053045964946062">"目前無法在 <xliff:g id="DEVICE">%1$s</xliff:g> 上存取這個應用程式,請改用平板電腦。"</string>
+ <string name="app_streaming_blocked_message" product="tablet" msgid="4242053045964946062">"目前無法在 <xliff:g id="DEVICE">%1$s</xliff:g> 上存取這個項目,請改用平板電腦。"</string>
<string name="app_streaming_blocked_message" product="default" msgid="6159168735030739398">"目前無法在 <xliff:g id="DEVICE">%1$s</xliff:g> 上存取這個應用程式,請改用手機。"</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tv" msgid="3470977315395784567">"這個應用程式要求進行額外的安全性驗證,請改用 Android TV 裝置。"</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tablet" msgid="698460091901465092">"這個應用程式要求進行額外的安全性驗證,請改用平板電腦。"</string>
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="default" msgid="8552691971910603907">"這個應用程式要求進行額外的安全性驗證,請改用手機。"</string>
<string name="app_streaming_blocked_message_for_settings_dialog" product="tv" msgid="820334666354451145">"無法在 <xliff:g id="DEVICE">%1$s</xliff:g> 上存取這個應用程式,請改用 Android TV 裝置。"</string>
- <string name="app_streaming_blocked_message_for_settings_dialog" product="tablet" msgid="3286849551133045896">"無法在 <xliff:g id="DEVICE">%1$s</xliff:g> 上存取這個應用程式,請改用平板電腦。"</string>
+ <string name="app_streaming_blocked_message_for_settings_dialog" product="tablet" msgid="3286849551133045896">"無法在 <xliff:g id="DEVICE">%1$s</xliff:g> 上存取這個項目,請改用平板電腦。"</string>
<string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"無法在 <xliff:g id="DEVICE">%1$s</xliff:g> 上存取這個應用程式,請改用手機。"</string>
<string name="deprecated_target_sdk_message" msgid="5203207875657579953">"這個應用程式是專為舊版 Android 所打造,因此可能無法正常運作。請嘗試檢查更新,或是與開發人員聯絡。"</string>
<string name="deprecated_target_sdk_app_store" msgid="8456784048558808909">"檢查更新"</string>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 004b5f6..d86aa11 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -9560,6 +9560,12 @@
<attr name="maxCollapsedHeightSmall" format="dimension" />
<!-- Whether the Drawer should be positioned at the top rather than at the bottom. -->
<attr name="showAtTop" format="boolean" />
+ <!-- By default `ResolverDrawerLayout`’s children views with `layout_ignoreOffset` property
+ set to true have a fixed position in the layout that won’t be affected by the drawer’s
+ movements. This property alternates that behavior. It specifies a child view’s id that
+ will push all ignoreOffset siblings below it when the drawer is moved i.e. setting the
+ top limit the ignoreOffset elements. -->
+ <attr name="ignoreOffsetTopLimit" format="reference" />
</declare-styleable>
<declare-styleable name="MessagingLinearLayout">
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 3408f75..881d499 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -658,6 +658,20 @@
-->
</integer-array>
+ <!-- The device states (supplied by DeviceStateManager) that should be treated as half-folded by
+ the display fold controller. Default is empty. -->
+ <integer-array name="config_halfFoldedDeviceStates">
+ <!-- Example:
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ -->
+ </integer-array>
+
+ <!-- Indicates whether the window manager reacts to half-fold device states by overriding
+ rotation. -->
+ <bool name="config_windowManagerHalfFoldAutoRotateOverride">false</bool>
+
<!-- When a device enters any of these states, it should be woken up. States are defined in
device_state_configuration.xml. -->
<integer-array name="config_deviceStatesOnWhichToWakeUp">
@@ -687,7 +701,7 @@
<!-- Indicates the time needed to time out the fold animation if the device stops in half folded
mode. -->
- <integer name="config_unfoldTransitionHalfFoldedTimeout">600</integer>
+ <integer name="config_unfoldTransitionHalfFoldedTimeout">1000</integer>
<!-- Indicates that the device supports having more than one internal display on at the same
time. Only applicable to devices with more than one internal display. If this option is
@@ -2465,6 +2479,13 @@
<integer name="config_attentionMaximumExtension">900000</integer> <!-- 15 minutes. -->
<!-- Is the system user the only user allowed to dream. -->
<bool name="config_dreamsOnlyEnabledForSystemUser">false</bool>
+ <!-- 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) -->
@@ -3561,9 +3582,9 @@
config_sidefpsSkipWaitForPowerVendorAcquireMessage -->
<integer name="config_sidefpsSkipWaitForPowerAcquireMessage">6</integer>
- <!-- This vendor acquired message that will cause the sidefpsKgPowerPress window to be skipped.
- config_sidefpsSkipWaitForPowerOnFingerUp must be true and
- config_sidefpsSkipWaitForPowerAcquireMessage must be BIOMETRIC_ACQUIRED_VENDOR == 6. -->
+ <!-- This vendor acquired message will cause the sidefpsKgPowerPress window to be skipped
+ when config_sidefpsSkipWaitForPowerAcquireMessage == 6 (VENDOR) and the vendor acquire
+ message equals this constant -->
<integer name="config_sidefpsSkipWaitForPowerVendorAcquireMessage">2</integer>
<!-- This config is used to force VoiceInteractionService to start on certain low ram devices.
@@ -5942,4 +5963,8 @@
TODO(b/236022708) Move rear display state to device state config file
-->
<integer name="config_deviceStateRearDisplay">-1</integer>
+
+ <!-- Whether the lock screen is allowed to run its own live wallpaper,
+ different from the home screen wallpaper. -->
+ <bool name="config_independentLockscreenLiveWallpaper">false</bool>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 16798b8..7a7b43a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2235,7 +2235,10 @@
<java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenNotPowered" />
<java-symbol type="integer" name="config_dreamsBatteryLevelDrainCutoff" />
<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" />
@@ -4005,6 +4008,8 @@
<!-- For Foldables -->
<java-symbol type="array" name="config_foldedDeviceStates" />
+ <java-symbol type="array" name="config_halfFoldedDeviceStates" />
+ <java-symbol type="bool" name="config_windowManagerHalfFoldAutoRotateOverride" />
<java-symbol type="array" name="config_deviceStatesOnWhichToWakeUp" />
<java-symbol type="array" name="config_deviceStatesOnWhichToSleep" />
<java-symbol type="string" name="config_foldedArea" />
@@ -4844,6 +4849,7 @@
<java-symbol type="array" name="config_deviceStatesAvailableForAppRequests" />
<java-symbol type="array" name="config_serviceStateLocationAllowedPackages" />
<java-symbol type="integer" name="config_deviceStateRearDisplay"/>
+ <java-symbol type="bool" name="config_independentLockscreenLiveWallpaper"/>
<!-- For app language picker -->
<java-symbol type="string" name="system_locale_title" />
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index f9f3b4c..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,9 +57,12 @@
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;
import android.content.Context;
import android.content.Intent;
import android.content.LocusId;
@@ -67,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;
@@ -78,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;
@@ -88,10 +95,13 @@
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;
+import java.util.List;
import java.util.function.Consumer;
@RunWith(AndroidJUnit4.class)
@@ -216,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);
@@ -531,6 +543,108 @@
}
@Test
+ public void testCallStyle_getSystemActions_forIncomingCall() {
+ PendingIntent answerIntent = createPendingIntent("answer");
+ PendingIntent declineIntent = createPendingIntent("decline");
+ Notification.CallStyle style = Notification.CallStyle.forIncomingCall(
+ new Person.Builder().setName("A Caller").build(),
+ declineIntent,
+ answerIntent
+ );
+ style.setBuilder(new Notification.Builder(mContext, "Channel"));
+
+ List<Notification.Action> actions = style.getActionsListWithSystemActions();
+
+ assertEquals(2, actions.size());
+ assertEquals(declineIntent, actions.get(0).actionIntent);
+ assertEquals(answerIntent, actions.get(1).actionIntent);
+ }
+
+ @Test
+ public void testCallStyle_getSystemActions_forOngoingCall() {
+ PendingIntent hangUpIntent = createPendingIntent("hangUp");
+ Notification.CallStyle style = Notification.CallStyle.forOngoingCall(
+ new Person.Builder().setName("A Caller").build(),
+ hangUpIntent
+ );
+ style.setBuilder(new Notification.Builder(mContext, "Channel"));
+
+ List<Notification.Action> actions = style.getActionsListWithSystemActions();
+
+ assertEquals(1, actions.size());
+ assertEquals(hangUpIntent, actions.get(0).actionIntent);
+ }
+
+ @Test
+ public void testCallStyle_getSystemActions_forIncomingCallWithOtherActions() {
+ PendingIntent answerIntent = createPendingIntent("answer");
+ PendingIntent declineIntent = createPendingIntent("decline");
+ Notification.CallStyle style = Notification.CallStyle.forIncomingCall(
+ new Person.Builder().setName("A Caller").build(),
+ declineIntent,
+ answerIntent
+ );
+ Notification.Action actionToKeep = makeNotificationAction(null);
+ Notification.Action actionToDrop = makeNotificationAction(null);
+ Notification.Builder notifBuilder = new Notification.Builder(mContext, "Channel")
+ .addAction(actionToKeep)
+ .addAction(actionToDrop); //expect to move this action to the end
+ style.setBuilder(notifBuilder); //add a builder with actions
+
+ List<Notification.Action> actions = style.getActionsListWithSystemActions();
+
+ assertEquals(4, actions.size());
+ assertEquals(declineIntent, actions.get(0).actionIntent);
+ assertEquals(actionToKeep, actions.get(1));
+ assertEquals(answerIntent, actions.get(2).actionIntent);
+ assertEquals(actionToDrop, actions.get(3));
+ }
+
+ @Test
+ public void testCallStyle_getSystemActions_forOngoingCallWithOtherActions() {
+ PendingIntent hangUpIntent = createPendingIntent("hangUp");
+ Notification.CallStyle style = Notification.CallStyle.forOngoingCall(
+ new Person.Builder().setName("A Caller").build(),
+ hangUpIntent
+ );
+ Notification.Action firstAction = makeNotificationAction(null);
+ Notification.Action secondAction = makeNotificationAction(null);
+ Notification.Builder notifBuilder = new Notification.Builder(mContext, "Channel")
+ .addAction(firstAction)
+ .addAction(secondAction);
+ style.setBuilder(notifBuilder); //add a builder with actions
+
+ List<Notification.Action> actions = style.getActionsListWithSystemActions();
+
+ assertEquals(3, actions.size());
+ assertEquals(hangUpIntent, actions.get(0).actionIntent);
+ assertEquals(firstAction, actions.get(1));
+ assertEquals(secondAction, actions.get(2));
+ }
+
+ @Test
+ public void testCallStyle_getSystemActions_dropsOldSystemActions() {
+ PendingIntent hangUpIntent = createPendingIntent("decline");
+ Notification.CallStyle style = Notification.CallStyle.forOngoingCall(
+ new Person.Builder().setName("A Caller").build(),
+ hangUpIntent
+ );
+ Bundle actionExtras = new Bundle();
+ actionExtras.putBoolean("key_action_priority", true);
+ Notification.Action oldSystemAction = makeNotificationAction(
+ builder -> builder.addExtras(actionExtras)
+ );
+ Notification.Builder notifBuilder = new Notification.Builder(mContext, "Channel")
+ .addAction(oldSystemAction);
+ style.setBuilder(notifBuilder); //add a builder with actions
+
+ List<Notification.Action> actions = style.getActionsListWithSystemActions();
+
+ assertFalse("Old versions of system actions should be dropped.",
+ actions.contains(oldSystemAction));
+ }
+
+ @Test
public void testBuild_ensureSmallIconIsNotTooBig_resizesIcon() {
Icon hugeIcon = Icon.createWithBitmap(
Bitmap.createBitmap(3000, 3000, Bitmap.Config.ARGB_8888));
@@ -565,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
@@ -676,7 +783,6 @@
assertFalse(notification.isMediaNotification());
}
- @Test
public void validateColorizedPaletteForColor(int rawColor) {
Notification.Colors cDay = new Notification.Colors();
Notification.Colors cNight = new Notification.Colors();
@@ -757,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
}
@@ -781,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 Notification.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
}
@@ -858,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
}
@@ -876,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);
@@ -962,4 +1573,12 @@
}
return actionBuilder.build();
}
+
+ /**
+ * Creates a PendingIntent with the given action.
+ */
+ private PendingIntent createPendingIntent(String action) {
+ return PendingIntent.getActivity(mContext, 0, new Intent(action),
+ PendingIntent.FLAG_MUTABLE);
+ }
}
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/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt b/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt
new file mode 100644
index 0000000..8218b98
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.Bundle
+import android.service.chooser.ChooserTarget
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.R
+import com.android.internal.app.ChooserListAdapter.LoadDirectShareIconTask
+import com.android.internal.app.chooser.SelectableTargetInfo
+import com.android.internal.app.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator
+import com.android.internal.app.chooser.TargetInfo
+import com.android.server.testutils.any
+import com.android.server.testutils.mock
+import com.android.server.testutils.whenever
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ChooserListAdapterTest {
+ private val packageManager = mock<PackageManager> {
+ whenever(resolveActivity(any(), anyInt())).thenReturn(mock())
+ }
+ private val context = InstrumentationRegistry.getInstrumentation().getContext()
+ private val resolverListController = mock<ResolverListController>()
+ private val chooserListCommunicator = mock<ChooserListAdapter.ChooserListCommunicator> {
+ whenever(maxRankedTargets).thenReturn(0)
+ }
+ private val selectableTargetInfoCommunicator =
+ mock<SelectableTargetInfoCommunicator> {
+ whenever(targetIntent).thenReturn(mock())
+ }
+ private val chooserActivityLogger = mock<ChooserActivityLogger>()
+
+ private fun createChooserListAdapter(
+ taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask
+ ) =
+ ChooserListAdapterOverride(
+ context,
+ emptyList(),
+ emptyArray(),
+ emptyList(),
+ false,
+ resolverListController,
+ chooserListCommunicator,
+ selectableTargetInfoCommunicator,
+ packageManager,
+ chooserActivityLogger,
+ taskProvider
+ )
+
+ @Test
+ fun testDirectShareTargetLoadingIconIsStarted() {
+ val view = createView()
+ val viewHolder = ResolverListAdapter.ViewHolder(view)
+ view.tag = viewHolder
+ val targetInfo = createSelectableTargetInfo()
+ val iconTask = mock<LoadDirectShareIconTask>()
+ val testSubject = createChooserListAdapter { iconTask }
+ testSubject.testViewBind(view, targetInfo, 0)
+
+ verify(iconTask, times(1)).loadIcon()
+ }
+
+ @Test
+ fun testOnlyOneTaskPerTarget() {
+ val view = createView()
+ val viewHolderOne = ResolverListAdapter.ViewHolder(view)
+ view.tag = viewHolderOne
+ val targetInfo = createSelectableTargetInfo()
+ val iconTaskOne = mock<LoadDirectShareIconTask>()
+ val testTaskProvider = mock<() -> LoadDirectShareIconTask> {
+ whenever(invoke()).thenReturn(iconTaskOne)
+ }
+ val testSubject = createChooserListAdapter { testTaskProvider.invoke() }
+ testSubject.testViewBind(view, targetInfo, 0)
+
+ val viewHolderTwo = ResolverListAdapter.ViewHolder(view)
+ view.tag = viewHolderTwo
+ whenever(testTaskProvider()).thenReturn(mock())
+
+ testSubject.testViewBind(view, targetInfo, 0)
+
+ verify(iconTaskOne, times(1)).loadIcon()
+ verify(testTaskProvider, times(1)).invoke()
+ }
+
+ private fun createSelectableTargetInfo(): SelectableTargetInfo =
+ SelectableTargetInfo(
+ context,
+ null,
+ createChooserTarget(),
+ 1f,
+ selectableTargetInfoCommunicator,
+ null
+ )
+
+ private fun createChooserTarget(): ChooserTarget =
+ ChooserTarget(
+ "Title",
+ null,
+ 1f,
+ ComponentName("package", "package.Class"),
+ Bundle()
+ )
+
+ private fun createView(): View {
+ val view = FrameLayout(context)
+ TextView(context).apply {
+ id = R.id.text1
+ view.addView(this)
+ }
+ TextView(context).apply {
+ id = R.id.text2
+ view.addView(this)
+ }
+ ImageView(context).apply {
+ id = R.id.icon
+ view.addView(this)
+ }
+ return view
+ }
+}
+
+private class ChooserListAdapterOverride(
+ context: Context?,
+ payloadIntents: List<Intent>?,
+ initialIntents: Array<out Intent>?,
+ rList: List<ResolveInfo>?,
+ filterLastUsed: Boolean,
+ resolverListController: ResolverListController?,
+ chooserListCommunicator: ChooserListCommunicator?,
+ selectableTargetInfoCommunicator: SelectableTargetInfoCommunicator?,
+ packageManager: PackageManager?,
+ chooserActivityLogger: ChooserActivityLogger?,
+ private val taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask
+) : ChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ resolverListController,
+ chooserListCommunicator,
+ selectableTargetInfoCommunicator,
+ packageManager,
+ chooserActivityLogger
+) {
+ override fun createLoadDirectShareIconTask(
+ info: SelectableTargetInfo?
+ ): LoadDirectShareIconTask =
+ taskProvider.invoke(info)
+
+ fun testViewBind(view: View?, info: TargetInfo?, position: Int) {
+ onBindView(view, info, position)
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
index d19f9f5..52feac5 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
@@ -47,6 +47,7 @@
import android.telephony.ModemActivityInfo;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
+import android.util.Log;
import android.util.MutableInt;
import android.util.SparseIntArray;
import android.util.SparseLongArray;
@@ -82,6 +83,7 @@
* com.android.frameworks.coretests/androidx.test.runner.AndroidJUnitRunner
*/
public class BatteryStatsNoteTest extends TestCase {
+ private static final String TAG = BatteryStatsNoteTest.class.getSimpleName();
private static final int UID = 10500;
private static final int ISOLATED_APP_ID = Process.FIRST_ISOLATED_UID + 23;
@@ -2031,6 +2033,115 @@
noRadioProcFlags, lastProcStateChangeFlags.value);
}
+
+
+ @SmallTest
+ public void testNoteMobileRadioPowerStateLocked() {
+ long curr;
+ boolean update;
+ final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
+ final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
+ bi.setOnBatteryInternal(true);
+
+ // Note mobile radio is on.
+ curr = 1000L * (clocks.realtime = clocks.uptime = 1001);
+ bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr,
+ UID);
+
+ // Note mobile radio is still on.
+ curr = 1000L * (clocks.realtime = clocks.uptime = 2001);
+ update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+ curr, UID);
+ assertFalse(
+ "noteMobileRadioPowerStateLocked should not request an update when the power "
+ + "state does not change from HIGH.",
+ update);
+
+ // Note mobile radio is off.
+ curr = 1000L * (clocks.realtime = clocks.uptime = 3001);
+ update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW,
+ curr, UID);
+ assertTrue(
+ "noteMobileRadioPowerStateLocked should request an update when the power state "
+ + "changes from HIGH to LOW.",
+ update);
+
+ // Note mobile radio is still off.
+ curr = 1000L * (clocks.realtime = clocks.uptime = 4001);
+ update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW,
+ curr, UID);
+ assertFalse(
+ "noteMobileRadioPowerStateLocked should not request an update when the power "
+ + "state does not change from LOW.",
+ update);
+
+ // Note mobile radio is on.
+ curr = 1000L * (clocks.realtime = clocks.uptime = 5001);
+ update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+ curr, UID);
+ assertFalse(
+ "noteMobileRadioPowerStateLocked should not request an update when the power "
+ + "state changes from LOW to HIGH.",
+ update);
+ }
+
+ @SmallTest
+ public void testNoteMobileRadioPowerStateLocked_rateLimited() {
+ long curr;
+ boolean update;
+ final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
+ final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
+ bi.setPowerProfile(mock(PowerProfile.class));
+
+ final int txLevelCount = CellSignalStrength.getNumSignalStrengthLevels();
+ final ModemActivityInfo mai = new ModemActivityInfo(0L, 0L, 0L, new int[txLevelCount], 0L);
+
+ final long rateLimit = bi.getMobileRadioPowerStateUpdateRateLimit();
+ if (rateLimit < 0) {
+ Log.w(TAG, "Skipping testNoteMobileRadioPowerStateLocked_rateLimited, rateLimit = "
+ + rateLimit);
+ return;
+ }
+ bi.setOnBatteryInternal(true);
+
+ // Note mobile radio is on.
+ curr = 1000L * (clocks.realtime = clocks.uptime = 1001);
+ bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr,
+ UID);
+
+ clocks.realtime = clocks.uptime = 2001;
+ mai.setTimestamp(clocks.realtime);
+ bi.noteModemControllerActivity(mai, POWER_DATA_UNAVAILABLE,
+ clocks.realtime, clocks.uptime, mNetworkStatsManager);
+
+ // Note mobile radio is off within the rate limit duration.
+ clocks.realtime = clocks.uptime = clocks.realtime + (long) (rateLimit * 0.7);
+ curr = 1000L * clocks.realtime;
+ update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW,
+ curr, UID);
+ assertFalse(
+ "noteMobileRadioPowerStateLocked should not request an update when the power "
+ + "state so soon after a noteModemControllerActivity",
+ update);
+
+ // Note mobile radio is on.
+ clocks.realtime = clocks.uptime = clocks.realtime + (long) (rateLimit * 0.7);
+ curr = 1000L * clocks.realtime;
+ bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr,
+ UID);
+
+ // Note mobile radio is off much later
+ clocks.realtime = clocks.uptime = clocks.realtime + rateLimit;
+ curr = 1000L * clocks.realtime;
+ update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW,
+ curr, UID);
+ assertTrue(
+ "noteMobileRadioPowerStateLocked should request an update when the power state "
+ + "changes from HIGH to LOW much later after a "
+ + "noteModemControllerActivity.",
+ update);
+ }
+
private void setFgState(int uid, boolean fgOn, MockBatteryStatsImpl bi) {
// Note that noteUidProcessStateLocked uses ActivityManager process states.
if (fgOn) {
diff --git a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
index edeb5e9..5ea4f06 100644
--- a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
+++ b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
@@ -114,6 +114,10 @@
return getUidStatsLocked(uid).mOnBatteryScreenOffBackgroundTimeBase;
}
+ public long getMobileRadioPowerStateUpdateRateLimit() {
+ return MOBILE_RADIO_POWER_STATE_UPDATE_FREQ_MS;
+ }
+
public MockBatteryStatsImpl setNetworkStats(NetworkStats networkStats) {
mNetworkStats = networkStats;
return this;
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index b1ecb43..31e2abe 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -1087,6 +1087,12 @@
"group": "WM_DEBUG_FOCUS",
"at": "com\/android\/server\/wm\/WindowState.java"
},
+ "-1043981272": {
+ "message": "Reverting orientation. Rotating to %s from %s rather than %s.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotation.java"
+ },
"-1042574499": {
"message": "Attempted to add Accessibility overlay window with unknown token %s. Aborting.",
"level": "WARN",
@@ -4285,6 +4291,12 @@
"group": "WM_ERROR",
"at": "com\/android\/server\/wm\/WindowManagerService.java"
},
+ "2066210760": {
+ "message": "foldStateChanged: displayId %d, halfFoldStateChanged %s, saved rotation: %d, mUserRotation: %d, mLastSensorRotation: %d, mLastOrientation: %d, mRotation: %d",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotation.java"
+ },
"2070726247": {
"message": "InsetsSource updateVisibility for %s, serverVisible: %s clientVisible: %s",
"level": "DEBUG",
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index fb0a9db..7e9c418 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -41,7 +41,7 @@
// TODO(b/241126279) Introduce constants to better version functionality
@Override
public int getVendorApiLevel() {
- return 1;
+ return 2;
}
/**
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 1174b68..1d513e4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -20,10 +20,11 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OPEN;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE;
-import static android.window.TaskFragmentOrganizer.getTransitionType;
import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
@@ -76,6 +77,7 @@
import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
import androidx.window.extensions.WindowExtensionsProvider;
+import androidx.window.extensions.embedding.TransactionManager.TransactionRecord;
import androidx.window.extensions.layout.WindowLayoutComponentImpl;
import com.android.internal.annotations.VisibleForTesting;
@@ -100,6 +102,10 @@
@GuardedBy("mLock")
final SplitPresenter mPresenter;
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ final TransactionManager mTransactionManager;
+
// Currently applied split configuration.
@GuardedBy("mLock")
private final List<EmbeddingRule> mSplitRules = new ArrayList<>();
@@ -150,6 +156,7 @@
final MainThreadExecutor executor = new MainThreadExecutor();
mHandler = executor.mHandler;
mPresenter = new SplitPresenter(executor, this);
+ mTransactionManager = new TransactionManager(mPresenter);
final ActivityThread activityThread = ActivityThread.currentActivityThread();
final Application application = activityThread.getApplication();
// Register a callback to be notified about activities being created.
@@ -167,7 +174,9 @@
@Override
public void accept(List<CommonFoldingFeature> foldingFeatures) {
synchronized (mLock) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
+ final TransactionRecord transactionRecord = mTransactionManager
+ .startNewTransaction();
+ final WindowContainerTransaction wct = transactionRecord.getTransaction();
for (int i = 0; i < mTaskContainers.size(); i++) {
final TaskContainer taskContainer = mTaskContainers.valueAt(i);
if (!taskContainer.isVisible()) {
@@ -186,7 +195,9 @@
updateContainersInTask(wct, taskContainer);
updateAnimationOverride(taskContainer);
}
- mPresenter.applyTransaction(wct);
+ // The WCT should be applied and merged to the device state change transition if
+ // there is one.
+ transactionRecord.apply(false /* shouldApplyIndependently */);
}
}
}
@@ -256,7 +267,9 @@
@Override
public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
synchronized (mLock) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
+ final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(
+ transaction.getTransactionToken());
+ final WindowContainerTransaction wct = transactionRecord.getTransaction();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
for (TaskFragmentTransaction.Change change : changes) {
final int taskId = change.getTaskId();
@@ -307,8 +320,7 @@
// Notify the server, and the server should apply and merge the
// WindowContainerTransaction to the active sync to finish the TaskFragmentTransaction.
- mPresenter.onTransactionHandled(transaction.getTransactionToken(), wct,
- getTransitionType(wct), false /* shouldApplyIndependently */);
+ transactionRecord.apply(false /* shouldApplyIndependently */);
updateCallbackIfNecessary();
}
}
@@ -333,6 +345,7 @@
container.setInfo(wct, taskFragmentInfo);
if (container.isFinished()) {
+ mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else {
// Update with the latest Task configuration.
@@ -368,15 +381,18 @@
// Do not finish the dependents if the last activity is reparented to PiP.
// Instead, the original split should be cleanup, and the dependent may be
// expanded to fullscreen.
+ mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
cleanupForEnterPip(wct, container);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else if (taskFragmentInfo.isTaskClearedForReuse()) {
// Do not finish the dependents if this TaskFragment was cleared due to
// launching activity in the Task.
+ mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else if (!container.isWaitingActivityAppear()) {
// Do not finish the container before the expected activity appear until
// timeout.
+ mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */);
}
} else if (wasInPip && isInPip) {
@@ -571,6 +587,7 @@
container.setInfo(wct, taskFragmentInfo);
container.clearPendingAppearedActivities();
if (container.isEmpty()) {
+ mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
}
break;
@@ -1009,11 +1026,10 @@
*/
@GuardedBy("mLock")
void onTaskFragmentAppearEmptyTimeout(@NonNull TaskFragmentContainer container) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- onTaskFragmentAppearEmptyTimeout(wct, container);
+ final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
+ onTaskFragmentAppearEmptyTimeout(transactionRecord.getTransaction(), container);
// Can be applied independently as a timeout callback.
- mPresenter.applyTransaction(wct, getTransitionType(wct),
- true /* shouldApplyIndependently */);
+ transactionRecord.apply(true /* shouldApplyIndependently */);
}
/**
@@ -1023,6 +1039,7 @@
@GuardedBy("mLock")
void onTaskFragmentAppearEmptyTimeout(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container) {
+ mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
}
@@ -1405,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.
@@ -1562,6 +1584,7 @@
* @param isOnCreated whether this happens during the primary activity onCreated.
*/
@VisibleForTesting
+ @GuardedBy("mLock")
@Nullable
Bundle getPlaceholderOptions(@NonNull Activity primaryActivity, boolean isOnCreated) {
// Setting avoid move to front will also skip the animation. We only want to do that when
@@ -1569,6 +1592,8 @@
// Check if the primary is resumed or if this is called when the primary is onCreated
// (not resumed yet).
if (isOnCreated || primaryActivity.isResumed()) {
+ // Only set trigger type if the launch happens in foreground.
+ mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_OPEN);
return null;
}
final ActivityOptions options = ActivityOptions.makeBasic();
@@ -1595,6 +1620,8 @@
if (SplitPresenter.shouldShowSplit(splitAttributes)) {
return false;
}
+
+ mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, splitContainer.getSecondaryContainer(),
false /* shouldFinishDependent */);
return true;
@@ -1621,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();
@@ -1639,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;
@@ -1905,23 +1909,26 @@
// that we don't launch it if an activity itself already requested something to be
// launched to side.
synchronized (mLock) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- SplitController.this.onActivityCreated(wct, activity);
+ final TransactionRecord transactionRecord = mTransactionManager
+ .startNewTransaction();
+ transactionRecord.setOriginType(TRANSIT_OPEN);
+ SplitController.this.onActivityCreated(transactionRecord.getTransaction(),
+ activity);
// The WCT should be applied and merged to the activity launch transition.
- mPresenter.applyTransaction(wct, getTransitionType(wct),
- false /* shouldApplyIndependently */);
+ transactionRecord.apply(false /* shouldApplyIndependently */);
}
}
@Override
public void onActivityConfigurationChanged(@NonNull Activity activity) {
synchronized (mLock) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- SplitController.this.onActivityConfigurationChanged(wct, activity);
+ final TransactionRecord transactionRecord = mTransactionManager
+ .startNewTransaction();
+ SplitController.this.onActivityConfigurationChanged(
+ transactionRecord.getTransaction(), activity);
// The WCT should be applied and merged to the Task change transition so that the
// placeholder is launched in the same transition.
- mPresenter.applyTransaction(wct, getTransitionType(wct),
- false /* shouldApplyIndependently */);
+ transactionRecord.apply(false /* shouldApplyIndependently */);
}
}
@@ -1977,7 +1984,10 @@
}
synchronized (mLock) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
+ final TransactionRecord transactionRecord = mTransactionManager
+ .startNewTransaction();
+ transactionRecord.setOriginType(TRANSIT_OPEN);
+ final WindowContainerTransaction wct = transactionRecord.getTransaction();
final TaskFragmentContainer launchedInTaskFragment;
if (launchingActivity != null) {
final int taskId = getTaskId(launchingActivity);
@@ -1990,13 +2000,14 @@
if (launchedInTaskFragment != null) {
// Make sure the WCT is applied immediately instead of being queued so that the
// TaskFragment will be ready before activity attachment.
- mPresenter.applyTransaction(wct, getTransitionType(wct),
- false /* shouldApplyIndependently */);
+ transactionRecord.apply(false /* shouldApplyIndependently */);
// Amend the request to let the WM know that the activity should be placed in
// the dedicated container.
options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
launchedInTaskFragment.getTaskFragmentToken());
mCurrentIntent = intent;
+ } else {
+ transactionRecord.abort();
}
}
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/src/androidx/window/extensions/embedding/TransactionManager.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java
new file mode 100644
index 0000000..0071fea
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_NONE;
+
+import android.os.IBinder;
+import android.view.WindowManager.TransitionType;
+import android.window.TaskFragmentOrganizer;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Responsible for managing the current {@link WindowContainerTransaction} as a response to device
+ * state changes and app interactions.
+ *
+ * A typical use flow:
+ * 1. Call {@link #startNewTransaction} to start tracking the changes.
+ * 2. Use {@link TransactionRecord#setOriginType(int)} (int)} to record the type of operation that
+ * will start a new transition on system server.
+ * 3. Use {@link #getCurrentTransactionRecord()} to get current {@link TransactionRecord} for
+ * changes.
+ * 4. Call {@link TransactionRecord#apply(boolean)} to request the system server to apply changes in
+ * the current {@link WindowContainerTransaction}, or call {@link TransactionRecord#abort()} to
+ * dispose the current one.
+ *
+ * Note:
+ * There should be only one transaction at a time. The caller should not call
+ * {@link #startNewTransaction} again before calling {@link TransactionRecord#apply(boolean)} or
+ * {@link TransactionRecord#abort()} to the previous transaction.
+ */
+class TransactionManager {
+
+ @NonNull
+ private final TaskFragmentOrganizer mOrganizer;
+
+ @Nullable
+ private TransactionRecord mCurrentTransaction;
+
+ TransactionManager(@NonNull TaskFragmentOrganizer organizer) {
+ mOrganizer = organizer;
+ }
+
+ @NonNull
+ TransactionRecord startNewTransaction() {
+ return startNewTransaction(null /* taskFragmentTransactionToken */);
+ }
+
+ /**
+ * Starts tracking the changes in a new {@link WindowContainerTransaction}. Caller can call
+ * {@link #getCurrentTransactionRecord()} later to continue adding change to the current
+ * transaction until {@link TransactionRecord#apply(boolean)} or
+ * {@link TransactionRecord#abort()} is called.
+ * @param taskFragmentTransactionToken {@link android.window.TaskFragmentTransaction
+ * #getTransactionToken()} if this is a response to a
+ * {@link android.window.TaskFragmentTransaction}.
+ */
+ @NonNull
+ TransactionRecord startNewTransaction(@Nullable IBinder taskFragmentTransactionToken) {
+ if (mCurrentTransaction != null) {
+ mCurrentTransaction = null;
+ throw new IllegalStateException(
+ "The previous transaction has not been applied or aborted,");
+ }
+ mCurrentTransaction = new TransactionRecord(taskFragmentTransactionToken);
+ return mCurrentTransaction;
+ }
+
+ /**
+ * Gets the current {@link TransactionRecord} started from {@link #startNewTransaction}.
+ */
+ @NonNull
+ TransactionRecord getCurrentTransactionRecord() {
+ if (mCurrentTransaction == null) {
+ throw new IllegalStateException("startNewTransaction() is not invoked before calling"
+ + " getCurrentTransactionRecord().");
+ }
+ return mCurrentTransaction;
+ }
+
+ /** The current transaction. The manager should only handle one transaction at a time. */
+ class TransactionRecord {
+ /**
+ * {@link WindowContainerTransaction} containing the current change.
+ * @see #startNewTransaction(IBinder)
+ * @see #apply (boolean)
+ */
+ @NonNull
+ private final WindowContainerTransaction mTransaction = new WindowContainerTransaction();
+
+ /**
+ * If the current transaction is a response to a
+ * {@link android.window.TaskFragmentTransaction}, this is the
+ * {@link android.window.TaskFragmentTransaction#getTransactionToken()}.
+ * @see #startNewTransaction(IBinder)
+ */
+ @Nullable
+ private final IBinder mTaskFragmentTransactionToken;
+
+ /**
+ * To track of the origin type of the current {@link #mTransaction}. When
+ * {@link #apply (boolean)} to start a new transition, this is the type to request.
+ * @see #setOriginType(int)
+ * @see #getTransactionTransitionType()
+ */
+ @TransitionType
+ private int mOriginType = TRANSIT_NONE;
+
+ TransactionRecord(@Nullable IBinder taskFragmentTransactionToken) {
+ mTaskFragmentTransactionToken = taskFragmentTransactionToken;
+ }
+
+ @NonNull
+ WindowContainerTransaction getTransaction() {
+ ensureCurrentTransaction();
+ return mTransaction;
+ }
+
+ /**
+ * Sets the {@link TransitionType} that triggers this transaction. If there are multiple
+ * calls, only the first call will be respected as the "origin" type.
+ */
+ void setOriginType(@TransitionType int type) {
+ ensureCurrentTransaction();
+ if (mOriginType != TRANSIT_NONE) {
+ // Skip if the origin type has already been set.
+ return;
+ }
+ mOriginType = type;
+ }
+
+ /**
+ * Requests the system server to apply the current transaction started from
+ * {@link #startNewTransaction}.
+ * @param shouldApplyIndependently If {@code true}, the {@link #mCurrentTransaction} will
+ * request a new transition, which will be queued until the
+ * sync engine is free if there is any other active sync.
+ * If {@code false}, the {@link #startNewTransaction} will
+ * be directly applied to the active sync.
+ */
+ void apply(boolean shouldApplyIndependently) {
+ ensureCurrentTransaction();
+ if (mTaskFragmentTransactionToken != null) {
+ // If this is a response to a TaskFragmentTransaction.
+ mOrganizer.onTransactionHandled(mTaskFragmentTransactionToken, mTransaction,
+ getTransactionTransitionType(), shouldApplyIndependently);
+ } else {
+ mOrganizer.applyTransaction(mTransaction, getTransactionTransitionType(),
+ shouldApplyIndependently);
+ }
+ dispose();
+ }
+
+ /** Called when there is no need to {@link #apply(boolean)} the current transaction. */
+ void abort() {
+ ensureCurrentTransaction();
+ dispose();
+ }
+
+ private void dispose() {
+ TransactionManager.this.mCurrentTransaction = null;
+ }
+
+ private void ensureCurrentTransaction() {
+ if (TransactionManager.this.mCurrentTransaction != this) {
+ throw new IllegalStateException(
+ "This transaction has already been apply() or abort().");
+ }
+ }
+
+ /**
+ * Gets the {@link TransitionType} that we will request transition with for the
+ * current {@link WindowContainerTransaction}.
+ */
+ @VisibleForTesting
+ @TransitionType
+ int getTransactionTransitionType() {
+ // Use TRANSIT_CHANGE as default if there is not opening/closing window.
+ return mOriginType != TRANSIT_NONE ? mOriginType : TRANSIT_CHANGE;
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index 0fb6ff8..b516e140 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -117,9 +117,7 @@
if (mWindowLayoutChangeListeners.containsKey(context)
// In theory this method can be called on the same consumer with different context.
|| mWindowLayoutChangeListeners.containsValue(consumer)) {
- throw new IllegalArgumentException(
- "Context or Consumer has already been registered for WindowLayoutInfo"
- + " callback.");
+ return;
}
if (!context.isUiContext()) {
throw new IllegalArgumentException("Context must be a UI Context, which should be"
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 25d0347..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,9 @@
private SplitController mSplitController;
private SplitPresenter mSplitPresenter;
+ private Consumer<List<SplitInfo>> mEmbeddingCallback;
+ private List<SplitInfo> mSplitInfos;
+ private TransactionManager mTransactionManager;
@Before
public void setUp() {
@@ -140,8 +144,17 @@
.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();
activityConfig.windowConfiguration.setBounds(TASK_BOUNDS);
@@ -212,6 +225,8 @@
@Test
public void testOnTaskFragmentAppearEmptyTimeout() {
+ // Setup to make sure a transaction record is started.
+ mTransactionManager.startNewTransaction();
final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
doCallRealMethod().when(mSplitController).onTaskFragmentAppearEmptyTimeout(any(), any());
mSplitController.onTaskFragmentAppearEmptyTimeout(mTransaction, tf);
@@ -324,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();
@@ -615,6 +654,8 @@
@Test
public void testResolveActivityToContainer_placeholderRule_notInTaskFragment() {
+ // Setup to make sure a transaction record is started.
+ mTransactionManager.startNewTransaction();
setupPlaceholderRule(mActivity);
final SplitPlaceholderRule placeholderRule =
(SplitPlaceholderRule) mSplitController.getSplitRules().get(0);
@@ -647,6 +688,8 @@
@Test
public void testResolveActivityToContainer_placeholderRule_inTopMostTaskFragment() {
+ // Setup to make sure a transaction record is started.
+ mTransactionManager.startNewTransaction();
setupPlaceholderRule(mActivity);
final SplitPlaceholderRule placeholderRule =
(SplitPlaceholderRule) mSplitController.getSplitRules().get(0);
@@ -679,6 +722,8 @@
@Test
public void testResolveActivityToContainer_placeholderRule_inSecondarySplit() {
+ // Setup to make sure a transaction record is started.
+ mTransactionManager.startNewTransaction();
setupPlaceholderRule(mActivity);
final SplitPlaceholderRule placeholderRule =
(SplitPlaceholderRule) mSplitController.getSplitRules().get(0);
@@ -961,6 +1006,8 @@
@Test
public void testGetPlaceholderOptions() {
+ // Setup to make sure a transaction record is started.
+ mTransactionManager.startNewTransaction();
doReturn(true).when(mActivity).isResumed();
assertNull(mSplitController.getPlaceholderOptions(mActivity, false /* isOnCreated */));
@@ -1147,18 +1194,71 @@
+ "of other properties",
SplitController.haveSamePresentation(splitRule1, splitRule2,
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;
@@ -1166,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;
}
@@ -1257,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/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java
new file mode 100644
index 0000000..62006bd
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java
@@ -0,0 +1,204 @@
+/*
+ * 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 androidx.window.extensions.embedding;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.clearInvocations;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+import android.window.TaskFragmentOrganizer;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.window.extensions.embedding.TransactionManager.TransactionRecord;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Test class for {@link TransactionManager}.
+ *
+ * Build/Install/Run:
+ * atest WMJetpackUnitTests:TransactionManagerTest
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TransactionManagerTest {
+
+ @Mock
+ private TaskFragmentOrganizer mOrganizer;
+ private TransactionManager mTransactionManager;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mTransactionManager = new TransactionManager(mOrganizer);
+ }
+
+ @Test
+ public void testStartNewTransaction() {
+ mTransactionManager.startNewTransaction();
+
+ // Throw exception if #startNewTransaction is called twice without #apply() or #abort().
+ assertThrows(IllegalStateException.class, mTransactionManager::startNewTransaction);
+
+ // Allow to start new after #apply() the last transaction.
+ TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+ transactionRecord = mTransactionManager.startNewTransaction();
+
+ // Allow to start new after #abort() the last transaction.
+ transactionRecord.abort();
+ mTransactionManager.startNewTransaction();
+ }
+
+ @Test
+ public void testSetTransactionOriginType() {
+ // Return TRANSIT_CHANGE if there is no trigger type set.
+ TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
+
+ assertEquals(TRANSIT_CHANGE, transactionRecord.getTransactionTransitionType());
+
+ // Return the first set type.
+ mTransactionManager.getCurrentTransactionRecord().abort();
+ transactionRecord = mTransactionManager.startNewTransaction();
+ transactionRecord.setOriginType(TRANSIT_OPEN);
+
+ assertEquals(TRANSIT_OPEN, transactionRecord.getTransactionTransitionType());
+
+ transactionRecord.setOriginType(TRANSIT_CLOSE);
+
+ assertEquals(TRANSIT_OPEN, transactionRecord.getTransactionTransitionType());
+
+ // Reset when #startNewTransaction().
+ transactionRecord.abort();
+ transactionRecord = mTransactionManager.startNewTransaction();
+
+ assertEquals(TRANSIT_CHANGE, transactionRecord.getTransactionTransitionType());
+ }
+
+ @Test
+ public void testGetCurrentTransactionRecord() {
+ // Throw exception if #getTransaction is called without calling #startNewTransaction().
+ assertThrows(IllegalStateException.class, mTransactionManager::getCurrentTransactionRecord);
+
+ TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
+ assertNotNull(transactionRecord);
+
+ // Same WindowContainerTransaction should be returned.
+ assertSame(transactionRecord, mTransactionManager.getCurrentTransactionRecord());
+
+ // Reset after #abort().
+ transactionRecord.abort();
+ assertThrows(IllegalStateException.class, mTransactionManager::getCurrentTransactionRecord);
+
+ // New WindowContainerTransaction after #startNewTransaction().
+ mTransactionManager.startNewTransaction();
+ assertNotEquals(transactionRecord, mTransactionManager.getCurrentTransactionRecord());
+
+ // Reset after #apply().
+ mTransactionManager.getCurrentTransactionRecord().apply(
+ false /* shouldApplyIndependently */);
+ assertThrows(IllegalStateException.class, mTransactionManager::getCurrentTransactionRecord);
+ }
+
+ @Test
+ public void testApply() {
+ // #applyTransaction(false)
+ TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
+ int transitionType = transactionRecord.getTransactionTransitionType();
+ WindowContainerTransaction wct = transactionRecord.getTransaction();
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+
+ verify(mOrganizer).applyTransaction(wct, transitionType,
+ false /* shouldApplyIndependently */);
+
+ // #applyTransaction(true)
+ clearInvocations(mOrganizer);
+ transactionRecord = mTransactionManager.startNewTransaction();
+ transitionType = transactionRecord.getTransactionTransitionType();
+ wct = transactionRecord.getTransaction();
+ transactionRecord.apply(true /* shouldApplyIndependently */);
+
+ verify(mOrganizer).applyTransaction(wct, transitionType,
+ true /* shouldApplyIndependently */);
+
+ // #onTransactionHandled(false)
+ clearInvocations(mOrganizer);
+ IBinder token = new Binder();
+ transactionRecord = mTransactionManager.startNewTransaction(token);
+ transitionType = transactionRecord.getTransactionTransitionType();
+ wct = transactionRecord.getTransaction();
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+
+ verify(mOrganizer).onTransactionHandled(token, wct, transitionType,
+ false /* shouldApplyIndependently */);
+
+ // #onTransactionHandled(true)
+ clearInvocations(mOrganizer);
+ token = new Binder();
+ transactionRecord = mTransactionManager.startNewTransaction(token);
+ transitionType = transactionRecord.getTransactionTransitionType();
+ wct = transactionRecord.getTransaction();
+ transactionRecord.apply(true /* shouldApplyIndependently */);
+
+ verify(mOrganizer).onTransactionHandled(token, wct, transitionType,
+ true /* shouldApplyIndependently */);
+
+ // Throw exception if there is any more interaction.
+ final TransactionRecord record = transactionRecord;
+ assertThrows(IllegalStateException.class,
+ () -> record.apply(false /* shouldApplyIndependently */));
+ assertThrows(IllegalStateException.class,
+ () -> record.apply(true /* shouldApplyIndependently */));
+ assertThrows(IllegalStateException.class,
+ record::abort);
+ }
+
+ @Test
+ public void testAbort() {
+ final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
+ transactionRecord.abort();
+
+ // Throw exception if there is any more interaction.
+ verifyNoMoreInteractions(mOrganizer);
+ assertThrows(IllegalStateException.class,
+ () -> transactionRecord.apply(false /* shouldApplyIndependently */));
+ assertThrows(IllegalStateException.class,
+ () -> transactionRecord.apply(true /* shouldApplyIndependently */));
+ assertThrows(IllegalStateException.class,
+ transactionRecord::abort);
+ }
+}
diff --git a/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml
index 66e5b43..5ecba38 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml
@@ -22,8 +22,8 @@
>
<group android:scaleX="0.5"
android:scaleY="0.5"
- android:translateX="8.0"
- android:translateY="8.0" >
+ android:translateX="4.0"
+ android:translateY="4.0" >
<path
android:fillColor="@android:color/black"
android:pathData="MM24,40.3 L7.7,24 24,7.7 26.8,10.45 15.3,22H40.3V26H15.3L26.8,37.5Z"/>
diff --git a/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml b/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml
index 53a8bb1..416287d 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml
@@ -17,4 +17,5 @@
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
+ <corners android:radius="20dp" />
</shape>
diff --git a/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml
index 851cbf2..cf9e632 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml
@@ -21,8 +21,8 @@
android:viewportHeight="32.0">
<group android:scaleX="0.5"
android:scaleY="0.5"
- android:translateX="8.0"
- android:translateY="8.0" >
+ android:translateX="4.0"
+ android:translateY="4.0" >
<path
android:fillColor="@android:color/black"
android:pathData="M12.45,38.35 L9.65,35.55 21.2,24 9.65,12.45 12.45,9.65 24,21.2 35.55,9.65 38.35,12.45 26.8,24 38.35,35.55 35.55,38.35 24,26.8Z"/>
diff --git a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
index ee0f466..c9f2623 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
@@ -18,6 +18,8 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
- <path
- android:fillColor="@android:color/black" android:pathData="M3,5V3H21V5Z"/>
+ <group android:translateY="8.0">
+ <path
+ android:fillColor="@android:color/black" android:pathData="M3,5V3H21V5Z"/>
+ </group>
</vector>
diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml
index cc60074..f9d153f 100644
--- a/libs/WindowManager/Shell/res/values-af/strings.xml
+++ b/libs/WindowManager/Shell/res/values-af/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Maksimeer"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Maak klein"</string>
<string name="close_button_text" msgid="2913281996024033299">"Maak toe"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Terug"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Handvatsel"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml
index 7aa8770..17b1d1b 100644
--- a/libs/WindowManager/Shell/res/values-am/strings.xml
+++ b/libs/WindowManager/Shell/res/values-am/strings.xml
@@ -84,4 +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>
+ <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 2d0d970..2babc3e 100644
--- a/libs/WindowManager/Shell/res/values-ar/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ar/strings.xml
@@ -84,4 +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>
+ <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 42153ff..fe43cd6 100644
--- a/libs/WindowManager/Shell/res/values-as/strings.xml
+++ b/libs/WindowManager/Shell/res/values-as/strings.xml
@@ -37,14 +37,14 @@
<string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"গৌণ ডিছপ্লেত এপ্ লঞ্চ কৰিব নোৱাৰি।"</string>
<string name="accessibility_divider" msgid="703810061635792791">"স্প্লিট স্ক্ৰীনৰ বিভাজক"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"বাওঁফালৰ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string>
- <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"বাওঁফালৰ স্ক্ৰীণখন ৭০% কৰক"</string>
- <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"বাওঁফালৰ স্ক্ৰীণখন ৫০% কৰক"</string>
- <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"বাওঁফালৰ স্ক্ৰীণখন ৩০% কৰক"</string>
+ <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"বাওঁফালৰ স্ক্ৰীনখন ৭০% কৰক"</string>
+ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"বাওঁফালৰ স্ক্ৰীনখন ৫০% কৰক"</string>
+ <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"বাওঁফালৰ স্ক্ৰীনখন ৩০% কৰক"</string>
<string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"সোঁফালৰ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string>
<string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"শীৰ্ষ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string>
- <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"শীর্ষ স্ক্ৰীণখন ৭০% কৰক"</string>
- <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"শীর্ষ স্ক্ৰীণখন ৫০% কৰক"</string>
- <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"শীর্ষ স্ক্ৰীণখন ৩০% কৰক"</string>
+ <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"শীর্ষ স্ক্ৰীনখন ৭০% কৰক"</string>
+ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"শীর্ষ স্ক্ৰীনখন ৫০% কৰক"</string>
+ <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"শীর্ষ স্ক্ৰীনখন ৩০% কৰক"</string>
<string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"তলৰ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string>
<string name="one_handed_tutorial_title" msgid="4583241688067426350">"এখন হাতেৰে ব্যৱহাৰ কৰা ম’ড ব্যৱহাৰ কৰা"</string>
<string name="one_handed_tutorial_description" msgid="3486582858591353067">"বাহিৰ হ’বলৈ স্ক্ৰীনখনৰ একেবাৰে তলৰ পৰা ওপৰলৈ ছোৱাইপ কৰক অথবা এপ্টোৰ ওপৰত যিকোনো ঠাইত টিপক"</string>
@@ -84,4 +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>
+ <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 5da5a97..c22f542 100644
--- a/libs/WindowManager/Shell/res/values-az/strings.xml
+++ b/libs/WindowManager/Shell/res/values-az/strings.xml
@@ -84,4 +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>
+ <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-b+sr+Latn/strings.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
index aff2b9f..4dda1db 100644
--- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Uvećajte"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Umanjite"</string>
<string name="close_button_text" msgid="2913281996024033299">"Zatvorite"</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-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml
index e325e51..cf5394b 100644
--- a/libs/WindowManager/Shell/res/values-be/strings.xml
+++ b/libs/WindowManager/Shell/res/values-be/strings.xml
@@ -84,4 +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>
+ <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 3bedc86..c525f52 100644
--- a/libs/WindowManager/Shell/res/values-bg/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bg/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Манипулатор"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml
index 35e5b38..b652d383 100644
--- a/libs/WindowManager/Shell/res/values-bn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bn/strings.xml
@@ -84,4 +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>
+ <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 34b282b..bec284d 100644
--- a/libs/WindowManager/Shell/res/values-bs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bs/strings.xml
@@ -84,4 +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">"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 0279917..c84bb48 100644
--- a/libs/WindowManager/Shell/res/values-ca/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ca/strings.xml
@@ -84,4 +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>
+ <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-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml
index d58100b..17f7374 100644
--- a/libs/WindowManager/Shell/res/values-cs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-cs/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Maximalizovat"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Minimalizovat"</string>
<string name="close_button_text" msgid="2913281996024033299">"Zavřít"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Zpět"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Úchyt"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml
index abb9cea..60c1f85 100644
--- a/libs/WindowManager/Shell/res/values-da/strings.xml
+++ b/libs/WindowManager/Shell/res/values-da/strings.xml
@@ -84,4 +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>
+ <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 a579394..b57f0c8 100644
--- a/libs/WindowManager/Shell/res/values-de/strings.xml
+++ b/libs/WindowManager/Shell/res/values-de/strings.xml
@@ -84,4 +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>
+ <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-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml
index 248cff8..2f92946 100644
--- a/libs/WindowManager/Shell/res/values-el/strings.xml
+++ b/libs/WindowManager/Shell/res/values-el/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"Πίσω"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Λαβή"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
index 9c798b8..9c88e78 100644
--- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Maximise"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Minimise"</string>
<string name="close_button_text" msgid="2913281996024033299">"Close"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Back"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Handle"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
index 9c798b8..9c88e78 100644
--- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Maximise"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Minimise"</string>
<string name="close_button_text" msgid="2913281996024033299">"Close"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Back"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Handle"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
index 9c798b8..9c88e78 100644
--- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Maximise"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Minimise"</string>
<string name="close_button_text" msgid="2913281996024033299">"Close"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Back"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Handle"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
index 9c798b8..9c88e78 100644
--- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Maximise"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Minimise"</string>
<string name="close_button_text" msgid="2913281996024033299">"Close"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Back"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Handle"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
index 5ff5c58..646b3c5 100644
--- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Maximize"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Minimize"</string>
<string name="close_button_text" msgid="2913281996024033299">"Close"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Back"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Handle"</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 08a7f49..aed03ac 100644
--- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
@@ -84,4 +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>
+ <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 139232c..bddc2c3 100644
--- a/libs/WindowManager/Shell/res/values-es/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es/strings.xml
@@ -84,4 +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>
+ <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 d6f3580..696a520 100644
--- a/libs/WindowManager/Shell/res/values-et/strings.xml
+++ b/libs/WindowManager/Shell/res/values-et/strings.xml
@@ -84,4 +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>
+ <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-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml
index e0efadd6..d1fca9b 100644
--- a/libs/WindowManager/Shell/res/values-eu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-eu/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Maximizatu"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Minimizatu"</string>
<string name="close_button_text" msgid="2913281996024033299">"Itxi"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Atzera"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Kontu-izena"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml
index 267dd20..7550a09 100644
--- a/libs/WindowManager/Shell/res/values-fa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fa/strings.xml
@@ -84,4 +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>
+ <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 30321a2..a79adf8 100644
--- a/libs/WindowManager/Shell/res/values-fi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fi/strings.xml
@@ -84,4 +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>
+ <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-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
index 1278997..4c59b99 100644
--- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"Retour"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Identifiant"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml
index 84c8618..865e2dc 100644
--- a/libs/WindowManager/Shell/res/values-fr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr/strings.xml
@@ -84,4 +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>
+ <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 59ec2de..47e6696 100644
--- a/libs/WindowManager/Shell/res/values-gl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-gl/strings.xml
@@ -84,4 +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>
+ <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-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml
index 85d98ed..5d52c58 100644
--- a/libs/WindowManager/Shell/res/values-gu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-gu/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"પાછળ"</string>
+ <string name="handle_text" msgid="1766582106752184456">"હૅન્ડલ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml
index bd90b7e..2c643de 100644
--- a/libs/WindowManager/Shell/res/values-hi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hi/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"वापस जाएं"</string>
+ <string name="handle_text" msgid="1766582106752184456">"हैंडल"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml
index 41df0df..32ab87c 100644
--- a/libs/WindowManager/Shell/res/values-hr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hr/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Maksimiziraj"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Minimiziraj"</string>
<string name="close_button_text" msgid="2913281996024033299">"Zatvori"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Natrag"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Pokazivač"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml
index 9736a69..dba7f4e 100644
--- a/libs/WindowManager/Shell/res/values-hu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hu/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Teljes méret"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Kis méret"</string>
<string name="close_button_text" msgid="2913281996024033299">"Bezárás"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Vissza"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Fogópont"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml
index 3c64494..d3bca33 100644
--- a/libs/WindowManager/Shell/res/values-hy/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hy/strings.xml
@@ -84,4 +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>
+ <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 61f3903..f157fcf 100644
--- a/libs/WindowManager/Shell/res/values-in/strings.xml
+++ b/libs/WindowManager/Shell/res/values-in/strings.xml
@@ -84,4 +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>
+ <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 044348c..ead1757 100644
--- a/libs/WindowManager/Shell/res/values-is/strings.xml
+++ b/libs/WindowManager/Shell/res/values-is/strings.xml
@@ -84,4 +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>
+ <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-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml
index ca97693..4e8ac62 100644
--- a/libs/WindowManager/Shell/res/values-it/strings.xml
+++ b/libs/WindowManager/Shell/res/values-it/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Ingrandisci"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Riduci a icona"</string>
<string name="close_button_text" msgid="2913281996024033299">"Chiudi"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Indietro"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Handle"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml
index 67de8a0..12c962d 100644
--- a/libs/WindowManager/Shell/res/values-iw/strings.xml
+++ b/libs/WindowManager/Shell/res/values-iw/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"חזרה"</string>
+ <string name="handle_text" msgid="1766582106752184456">"נקודת אחיזה"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml
index cc8527e..d9f514a 100644
--- a/libs/WindowManager/Shell/res/values-ja/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ja/strings.xml
@@ -84,4 +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>
+ <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 0ef4563..b284361 100644
--- a/libs/WindowManager/Shell/res/values-ka/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ka/strings.xml
@@ -84,4 +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>
+ <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 6ba9a68..2d842b8 100644
--- a/libs/WindowManager/Shell/res/values-kk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-kk/strings.xml
@@ -84,4 +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>
+ <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 03682d8..3243a64 100644
--- a/libs/WindowManager/Shell/res/values-km/strings.xml
+++ b/libs/WindowManager/Shell/res/values-km/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"ថយក្រោយ"</string>
+ <string name="handle_text" msgid="1766582106752184456">"ឈ្មោះអ្នកប្រើប្រាស់"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml
index 167a38e..57c7124 100644
--- a/libs/WindowManager/Shell/res/values-kn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-kn/strings.xml
@@ -84,4 +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>
+ <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 1175a0e..3a655b8 100644
--- a/libs/WindowManager/Shell/res/values-ko/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ko/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"뒤로"</string>
+ <string name="handle_text" msgid="1766582106752184456">"핸들"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml
index b047863..ab51ca0 100644
--- a/libs/WindowManager/Shell/res/values-ky/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ky/strings.xml
@@ -84,4 +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>
+ <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 00bf7f4..b800e3e 100644
--- a/libs/WindowManager/Shell/res/values-lo/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lo/strings.xml
@@ -84,4 +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>
+ <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 3981793..94339a4 100644
--- a/libs/WindowManager/Shell/res/values-lt/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lt/strings.xml
@@ -84,4 +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>
+ <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 3599c14..d282453 100644
--- a/libs/WindowManager/Shell/res/values-lv/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lv/strings.xml
@@ -84,4 +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>
+ <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-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml
index 6c41b0c..64ce3f6 100644
--- a/libs/WindowManager/Shell/res/values-mk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mk/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Прекар"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml
index 76dfc0d9..5db8d6d 100644
--- a/libs/WindowManager/Shell/res/values-ml/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ml/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"മടങ്ങുക"</string>
+ <string name="handle_text" msgid="1766582106752184456">"ഹാൻഡിൽ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml
index a8bd85e..be02be1 100644
--- a/libs/WindowManager/Shell/res/values-mn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mn/strings.xml
@@ -84,4 +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>
+ <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 5874812..779cf5c 100644
--- a/libs/WindowManager/Shell/res/values-mr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mr/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"मागे जा"</string>
+ <string name="handle_text" msgid="1766582106752184456">"हँडल"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml
index 88270df..85d380e 100644
--- a/libs/WindowManager/Shell/res/values-ms/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ms/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Maksimumkan"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Minimumkan"</string>
<string name="close_button_text" msgid="2913281996024033299">"Tutup"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Kembali"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Pemegang"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml
index 9a2d1ba..517098d 100644
--- a/libs/WindowManager/Shell/res/values-my/strings.xml
+++ b/libs/WindowManager/Shell/res/values-my/strings.xml
@@ -66,7 +66,7 @@
<string name="bubbles_user_education_description" msgid="4215862563054175407">"စကားဝိုင်းအသစ်များကို မျောနေသည့် သင်္ကေတများ သို့မဟုတ် ပူဖောင်းကွက်များအဖြစ် မြင်ရပါမည်။ ပူဖောင်းကွက်ကိုဖွင့်ရန် တို့ပါ။ ရွှေ့ရန် ၎င်းကို ဖိဆွဲပါ။"</string>
<string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"ပူဖောင်းကွက်ကို အချိန်မရွေး ထိန်းချုပ်ရန်"</string>
<string name="bubbles_user_education_manage" msgid="3460756219946517198">"ဤအက်ပ်မှနေ၍ ပူဖောင်းများကို ပိတ်ရန်အတွက် \'စီမံရန်\' ကို တို့ပါ"</string>
- <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ရပြီ"</string>
+ <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"နားလည်ပြီ"</string>
<string name="bubble_overflow_empty_title" msgid="2397251267073294968">"လတ်တလော ပူဖောင်းကွက်များ မရှိပါ"</string>
<string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"လတ်တလော ပူဖောင်းကွက်များနှင့် ပိတ်လိုက်သော ပူဖောင်းကွက်များကို ဤနေရာတွင် မြင်ရပါမည်"</string>
<string name="notification_bubble_title" msgid="6082910224488253378">"ပူဖောင်းဖောက်သံ"</string>
@@ -79,9 +79,11 @@
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ကြည့်ပြီး ပိုမိုလုပ်ဆောင်ပါ"</string>
<string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"မျက်နှာပြင် ခွဲ၍ပြသနိုင်ရန် နောက်အက်ပ်တစ်ခုကို ဖိဆွဲပါ"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"နေရာပြန်ချရန် အက်ပ်အပြင်ဘက်ကို နှစ်ချက်တို့ပါ"</string>
- <string name="letterbox_education_got_it" msgid="4057634570866051177">"ရပြီ"</string>
+ <string name="letterbox_education_got_it" msgid="4057634570866051177">"နားလည်ပြီ"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"နောက်ထပ်အချက်အလက်များအတွက် ချဲ့နိုင်သည်။"</string>
<string name="maximize_button_text" msgid="1650859196290301963">"ချဲ့ရန်"</string>
<string name="minimize_button_text" msgid="271592547935841753">"ချုံ့ရန်"</string>
<string name="close_button_text" msgid="2913281996024033299">"ပိတ်ရန်"</string>
+ <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 ec9e635d..0ceee2d 100644
--- a/libs/WindowManager/Shell/res/values-nb/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nb/strings.xml
@@ -84,4 +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>
+ <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 dc7d98d..7ba49e5 100644
--- a/libs/WindowManager/Shell/res/values-ne/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ne/strings.xml
@@ -84,4 +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>
+ <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 5bae2a3..dd3ebe4 100644
--- a/libs/WindowManager/Shell/res/values-nl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nl/strings.xml
@@ -84,4 +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>
+ <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-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml
index c5ef4f1..52280a1 100644
--- a/libs/WindowManager/Shell/res/values-or/strings.xml
+++ b/libs/WindowManager/Shell/res/values-or/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"ପଛକୁ ଫେରନ୍ତୁ"</string>
+ <string name="handle_text" msgid="1766582106752184456">"ହେଣ୍ଡେଲ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml
index f026205..c3056ca 100644
--- a/libs/WindowManager/Shell/res/values-pa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pa/strings.xml
@@ -84,4 +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>
+ <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 22c0c37..56a6fb1 100644
--- a/libs/WindowManager/Shell/res/values-pl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pl/strings.xml
@@ -84,4 +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>
+ <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-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
index 0bbffb3..3cb708d 100644
--- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
@@ -84,4 +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">"Fechar"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Voltar"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Alça"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
index 07dd4d7..e5be578 100644
--- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
@@ -84,4 +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">"Fechar"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Anterior"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Indicador"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml
index 0bbffb3..3cb708d 100644
--- a/libs/WindowManager/Shell/res/values-pt/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt/strings.xml
@@ -84,4 +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">"Fechar"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Voltar"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Alça"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml
index 0aae6a5..c03e043 100644
--- a/libs/WindowManager/Shell/res/values-ro/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ro/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Maximizează"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Minimizează"</string>
<string name="close_button_text" msgid="2913281996024033299">"Închide"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Înapoi"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Ghidaj"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml
index f1ff000..b96caf2 100644
--- a/libs/WindowManager/Shell/res/values-ru/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ru/strings.xml
@@ -84,4 +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>
+ <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 39bd260..a1ec3b5 100644
--- a/libs/WindowManager/Shell/res/values-si/strings.xml
+++ b/libs/WindowManager/Shell/res/values-si/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"ආපසු"</string>
+ <string name="handle_text" msgid="1766582106752184456">"හැඬලය"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml
index 8859231..e3dafb6 100644
--- a/libs/WindowManager/Shell/res/values-sk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sk/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Maximalizovať"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Minimalizovať"</string>
<string name="close_button_text" msgid="2913281996024033299">"Zavrieť"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Späť"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Rukoväť"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml
index 22fe0f8..2f995e5 100644
--- a/libs/WindowManager/Shell/res/values-sl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sl/strings.xml
@@ -84,4 +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>
+ <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 2a3671b..3d9bde4 100644
--- a/libs/WindowManager/Shell/res/values-sq/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sq/strings.xml
@@ -84,4 +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>
+ <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-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml
index 09471037..b00c4f4 100644
--- a/libs/WindowManager/Shell/res/values-sr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sr/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Идентификатор"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml
index 70282a4b..b39fd04 100644
--- a/libs/WindowManager/Shell/res/values-sv/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sv/strings.xml
@@ -84,4 +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>
+ <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 1aab85c..f4d4cee 100644
--- a/libs/WindowManager/Shell/res/values-sw/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sw/strings.xml
@@ -84,4 +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>
+ <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 befc8eb..6d050c2 100644
--- a/libs/WindowManager/Shell/res/values-ta/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ta/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"பின்செல்லும்"</string>
+ <string name="handle_text" msgid="1766582106752184456">"ஹேண்டில்"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml
index a3e21f7..91c411f 100644
--- a/libs/WindowManager/Shell/res/values-te/strings.xml
+++ b/libs/WindowManager/Shell/res/values-te/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"వెనుకకు"</string>
+ <string name="handle_text" msgid="1766582106752184456">"హ్యాండిల్"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml
index 63d3bb8..ee362dc 100644
--- a/libs/WindowManager/Shell/res/values-th/strings.xml
+++ b/libs/WindowManager/Shell/res/values-th/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"กลับ"</string>
+ <string name="handle_text" msgid="1766582106752184456">"แฮนเดิล"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml
index 50334f5..d8203656 100644
--- a/libs/WindowManager/Shell/res/values-tl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-tl/strings.xml
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"I-maximize"</string>
<string name="minimize_button_text" msgid="271592547935841753">"I-minimize"</string>
<string name="close_button_text" msgid="2913281996024033299">"Isara"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Bumalik"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Handle"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml
index e1ea0df..025e2e6 100644
--- a/libs/WindowManager/Shell/res/values-tr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-tr/strings.xml
@@ -84,4 +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>
+ <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 9e713c7..97bb680 100644
--- a/libs/WindowManager/Shell/res/values-uk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-uk/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Маркер"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml
index 596cf4b..883026a 100644
--- a/libs/WindowManager/Shell/res/values-ur/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ur/strings.xml
@@ -84,4 +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>
+ <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 275940a..ce73bd5 100644
--- a/libs/WindowManager/Shell/res/values-uz/strings.xml
+++ b/libs/WindowManager/Shell/res/values-uz/strings.xml
@@ -84,4 +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>
+ <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 24fb1ca..511db6f 100644
--- a/libs/WindowManager/Shell/res/values-vi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-vi/strings.xml
@@ -84,4 +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>
+ <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 e52a7b5..16cbf12 100644
--- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"返回"</string>
+ <string name="handle_text" msgid="1766582106752184456">"处理"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
index eed1e52..5a497d0 100644
--- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
@@ -84,4 +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>
+ <string name="back_button_text" msgid="1469718707134137085">"返去"</string>
+ <string name="handle_text" msgid="1766582106752184456">"控點"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
index f9f28ab..2c2ce33 100644
--- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
@@ -84,4 +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>
+ <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 ddb6a4c..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>
@@ -84,4 +84,6 @@
<string name="maximize_button_text" msgid="1650859196290301963">"Khulisa"</string>
<string name="minimize_button_text" msgid="271592547935841753">"Nciphisa"</string>
<string name="close_button_text" msgid="2913281996024033299">"Vala"</string>
+ <string name="back_button_text" msgid="1469718707134137085">"Emuva"</string>
+ <string name="handle_text" msgid="1766582106752184456">"Isibambo"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 0bc7085..3ee20ea 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -321,4 +321,21 @@
<!-- The smaller size of the dismiss target (shrinks when something is in the target). -->
<dimen name="floating_dismiss_circle_small">120dp</dimen>
+
+ <!-- The thickness of shadows of a window that has focus in DIP. -->
+ <dimen name="freeform_decor_shadow_focused_thickness">20dp</dimen>
+
+ <!-- The thickness of shadows of a window that doesn't have focus in DIP. -->
+ <dimen name="freeform_decor_shadow_unfocused_thickness">5dp</dimen>
+
+ <!-- Height of button (32dp) + 2 * margin (5dp each). -->
+ <dimen name="freeform_decor_caption_height">42dp</dimen>
+
+ <!-- Width of buttons (64dp) + handle (128dp) + padding (24dp total). -->
+ <dimen name="freeform_decor_caption_width">216dp</dimen>
+
+ <dimen name="freeform_resize_handle">30dp</dimen>
+
+ <dimen name="freeform_resize_corner">44dp</dimen>
+
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 591e347..215308d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -130,6 +130,10 @@
if (!cropRect.intersect(mWholeAnimationBounds)) {
// Hide the surface when it is outside of the animation area.
t.setAlpha(mLeash, 0);
+ } else if (mAnimation.hasExtension()) {
+ // Allow the surface to be shown in its original bounds in case we want to use edge
+ // extensions.
+ cropRect.union(mChange.getEndAbsBounds());
}
// cropRect is in absolute coordinate, so we need to translate it to surface top left.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index 756d802..490975c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -21,6 +21,7 @@
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
import android.animation.Animator;
@@ -45,6 +46,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.function.Consumer;
/** To run the ActivityEmbedding animations. */
class ActivityEmbeddingAnimationRunner {
@@ -65,10 +67,31 @@
void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction) {
+ // There may be some surface change that we want to apply after the start transaction is
+ // applied to make sure the surface is ready.
+ final List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks =
+ new ArrayList<>();
final Animator animator = createAnimator(info, startTransaction, finishTransaction,
- () -> mController.onAnimationFinished(transition));
- startTransaction.apply();
- animator.start();
+ () -> mController.onAnimationFinished(transition), postStartTransactionCallbacks);
+
+ // Start the animation.
+ if (!postStartTransactionCallbacks.isEmpty()) {
+ // postStartTransactionCallbacks require that the start transaction is already
+ // applied to run otherwise they may result in flickers and UI inconsistencies.
+ startTransaction.apply(true /* sync */);
+
+ // Run tasks that require startTransaction to already be applied
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ for (Consumer<SurfaceControl.Transaction> postStartTransactionCallback :
+ postStartTransactionCallbacks) {
+ postStartTransactionCallback.accept(t);
+ }
+ t.apply();
+ animator.start();
+ } else {
+ startTransaction.apply();
+ animator.start();
+ }
}
/**
@@ -85,9 +108,13 @@
Animator createAnimator(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Runnable animationFinishCallback) {
- final List<ActivityEmbeddingAnimationAdapter> adapters =
- createAnimationAdapters(info, startTransaction, finishTransaction);
+ @NonNull Runnable animationFinishCallback,
+ @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks) {
+ final List<ActivityEmbeddingAnimationAdapter> adapters = createAnimationAdapters(info,
+ startTransaction);
+ addEdgeExtensionIfNeeded(startTransaction, finishTransaction, postStartTransactionCallbacks,
+ adapters);
+ addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters);
long duration = 0;
for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
duration = Math.max(duration, adapter.getDurationHint());
@@ -131,8 +158,7 @@
*/
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters(
- @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
boolean isChangeTransition = false;
for (TransitionInfo.Change change : info.getChanges()) {
if (change.hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)) {
@@ -148,25 +174,23 @@
return createChangeAnimationAdapters(info, startTransaction);
}
if (Transitions.isClosingType(info.getType())) {
- return createCloseAnimationAdapters(info, startTransaction, finishTransaction);
+ return createCloseAnimationAdapters(info);
}
- return createOpenAnimationAdapters(info, startTransaction, finishTransaction);
+ return createOpenAnimationAdapters(info);
}
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters(
- @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
- return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction,
- true /* isOpening */, mAnimationSpec::loadOpenAnimation);
+ @NonNull TransitionInfo info) {
+ return createOpenCloseAnimationAdapters(info, true /* isOpening */,
+ mAnimationSpec::loadOpenAnimation);
}
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters(
- @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
- return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction,
- false /* isOpening */, mAnimationSpec::loadCloseAnimation);
+ @NonNull TransitionInfo info) {
+ return createOpenCloseAnimationAdapters(info, false /* isOpening */,
+ mAnimationSpec::loadCloseAnimation);
}
/**
@@ -175,8 +199,7 @@
*/
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createOpenCloseAnimationAdapters(
- @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction, boolean isOpening,
+ @NonNull TransitionInfo info, boolean isOpening,
@NonNull AnimationProvider animationProvider) {
// We need to know if the change window is only a partial of the whole animation screen.
// If so, we will need to adjust it to make the whole animation screen looks like one.
@@ -200,8 +223,7 @@
final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
for (TransitionInfo.Change change : openingChanges) {
final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
- info, change, startTransaction, finishTransaction, animationProvider,
- openingWholeScreenBounds);
+ info, change, animationProvider, openingWholeScreenBounds);
if (isOpening) {
adapter.overrideLayer(offsetLayer++);
}
@@ -209,8 +231,7 @@
}
for (TransitionInfo.Change change : closingChanges) {
final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
- info, change, startTransaction, finishTransaction, animationProvider,
- closingWholeScreenBounds);
+ info, change, animationProvider, closingWholeScreenBounds);
if (!isOpening) {
adapter.overrideLayer(offsetLayer++);
}
@@ -219,20 +240,51 @@
return adapters;
}
+ /** Adds edge extension to the surfaces that have such an animation property. */
+ private void addEdgeExtensionIfNeeded(@NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks,
+ @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) {
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ final Animation animation = adapter.mAnimation;
+ if (!animation.hasExtension()) {
+ continue;
+ }
+ final TransitionInfo.Change change = adapter.mChange;
+ if (Transitions.isOpeningType(adapter.mChange.getMode())) {
+ // Need to screenshot after startTransaction is applied otherwise activity
+ // may not be visible or ready yet.
+ postStartTransactionCallbacks.add(
+ t -> edgeExtendWindow(change, animation, t, finishTransaction));
+ } else {
+ // Can screenshot now (before startTransaction is applied)
+ edgeExtendWindow(change, animation, startTransaction, finishTransaction);
+ }
+ }
+ }
+
+ /** Adds background color to the transition if any animation has such a property. */
+ private void addBackgroundColorIfNeeded(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) {
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ final int backgroundColor = getTransitionBackgroundColorIfSet(info, adapter.mChange,
+ adapter.mAnimation, 0 /* defaultColor */);
+ if (backgroundColor != 0) {
+ // We only need to show one color.
+ addBackgroundToTransition(info.getRootLeash(), backgroundColor, startTransaction,
+ finishTransaction);
+ return;
+ }
+ }
+ }
+
@NonNull
private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter(
@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
@NonNull AnimationProvider animationProvider, @NonNull Rect wholeAnimationBounds) {
final Animation animation = animationProvider.get(info, change, wholeAnimationBounds);
- // We may want to show a background color for open/close transition.
- final int backgroundColor = getTransitionBackgroundColorIfSet(info, change, animation,
- 0 /* defaultColor */);
- if (backgroundColor != 0) {
- addBackgroundToTransition(info.getRootLeash(), backgroundColor, startTransaction,
- finishTransaction);
- }
return new ActivityEmbeddingAnimationAdapter(animation, change, change.getLeash(),
wholeAnimationBounds);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index eb6ac76..58b2366 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -181,15 +181,15 @@
@NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
final boolean isEnter = Transitions.isOpeningType(change.getMode());
final Animation animation;
- // TODO(b/207070762): Implement edgeExtension version
if (shouldShowBackdrop(info, change)) {
animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
? com.android.internal.R.anim.task_fragment_clear_top_open_enter
: com.android.internal.R.anim.task_fragment_clear_top_open_exit);
} else {
+ // Use the same edge extension animation as regular activity open.
animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
- ? com.android.internal.R.anim.task_fragment_open_enter
- : com.android.internal.R.anim.task_fragment_open_exit);
+ ? com.android.internal.R.anim.activity_open_enter
+ : com.android.internal.R.anim.activity_open_exit);
}
// Use the whole animation bounds instead of the change bounds, so that when multiple change
// targets are opening at the same time, the animation applied to each will be the same.
@@ -205,15 +205,15 @@
@NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
final boolean isEnter = Transitions.isOpeningType(change.getMode());
final Animation animation;
- // TODO(b/207070762): Implement edgeExtension version
if (shouldShowBackdrop(info, change)) {
animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
? com.android.internal.R.anim.task_fragment_clear_top_close_enter
: com.android.internal.R.anim.task_fragment_clear_top_close_exit);
} else {
+ // Use the same edge extension animation as regular activity close.
animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
- ? com.android.internal.R.anim.task_fragment_close_enter
- : com.android.internal.R.anim.task_fragment_close_exit);
+ ? com.android.internal.R.anim.activity_close_enter
+ : com.android.internal.R.anim.activity_close_exit);
}
// Use the whole animation bounds instead of the change bounds, so that when multiple change
// targets are closing at the same time, the animation applied to each will be the same.
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/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 93413db..725b205 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -28,10 +28,6 @@
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_BOTTOM;
-import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_LEFT;
-import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_NONE;
-import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_RIGHT;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_BLOCKED;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_GROUP_CANCELLED;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_INVALID_INTENT;
@@ -41,6 +37,7 @@
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED;
+import static com.android.wm.shell.floating.FloatingTasksController.SHOW_FLOATING_TASKS_AS_BUBBLES;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
@@ -59,10 +56,8 @@
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
-import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Binder;
-import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -126,18 +121,6 @@
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;
- // TODO(b/173386799) keep in sync with Launcher3, not hooked up to anything
- public static final String EXTRA_TASKBAR_CREATED = "taskbarCreated";
- public static final String EXTRA_BUBBLE_OVERFLOW_OPENED = "bubbleOverflowOpened";
- public static final String EXTRA_TASKBAR_VISIBLE = "taskbarVisible";
- public static final String EXTRA_TASKBAR_POSITION = "taskbarPosition";
- public static final String EXTRA_TASKBAR_ICON_SIZE = "taskbarIconSize";
- public static final String EXTRA_TASKBAR_BUBBLE_XY = "taskbarBubbleXY";
- public static final String EXTRA_TASKBAR_SIZE = "taskbarSize";
- public static final String LEFT_POSITION = "Left";
- public static final String RIGHT_POSITION = "Right";
- public static final String BOTTOM_POSITION = "Bottom";
-
// Should match with PhoneWindowManager
private static final String SYSTEM_DIALOG_REASON_KEY = "reason";
private static final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav";
@@ -470,52 +453,6 @@
mBubbleData.setExpanded(true);
}
- /** Called when any taskbar state changes (e.g. visibility, position, sizes). */
- private void onTaskbarChanged(Bundle b) {
- if (b == null) {
- return;
- }
- boolean isVisible = b.getBoolean(EXTRA_TASKBAR_VISIBLE, false /* default */);
- String position = b.getString(EXTRA_TASKBAR_POSITION, RIGHT_POSITION /* default */);
- @BubblePositioner.TaskbarPosition int taskbarPosition = TASKBAR_POSITION_NONE;
- switch (position) {
- case LEFT_POSITION:
- taskbarPosition = TASKBAR_POSITION_LEFT;
- break;
- case RIGHT_POSITION:
- taskbarPosition = TASKBAR_POSITION_RIGHT;
- break;
- case BOTTOM_POSITION:
- taskbarPosition = TASKBAR_POSITION_BOTTOM;
- break;
- }
- int[] itemPosition = b.getIntArray(EXTRA_TASKBAR_BUBBLE_XY);
- int iconSize = b.getInt(EXTRA_TASKBAR_ICON_SIZE);
- int taskbarSize = b.getInt(EXTRA_TASKBAR_SIZE);
- Log.w(TAG, "onTaskbarChanged:"
- + " isVisible: " + isVisible
- + " position: " + position
- + " itemPosition: " + itemPosition[0] + "," + itemPosition[1]
- + " iconSize: " + iconSize);
- PointF point = new PointF(itemPosition[0], itemPosition[1]);
- mBubblePositioner.setPinnedLocation(isVisible ? point : null);
- mBubblePositioner.updateForTaskbar(iconSize, taskbarPosition, isVisible, taskbarSize);
- if (mStackView != null) {
- if (isVisible && b.getBoolean(EXTRA_TASKBAR_CREATED, false /* default */)) {
- // If taskbar was created, add and remove the window so that bubbles display on top
- removeFromWindowManagerMaybe();
- addToWindowManagerMaybe();
- }
- mStackView.updateStackPosition();
- mBubbleIconFactory = new BubbleIconFactory(mContext);
- mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(mContext);
- mStackView.onDisplaySizeChanged();
- }
- if (b.getBoolean(EXTRA_BUBBLE_OVERFLOW_OPENED, false)) {
- openBubbleOverflow();
- }
- }
-
/**
* Called when the status bar has become visible or invisible (either permanently or
* temporarily).
@@ -654,6 +591,11 @@
}
mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
}
+ if (SHOW_FLOATING_TASKS_AS_BUBBLES && mBubblePositioner.isLargeScreen()) {
+ mBubblePositioner.setUsePinnedLocation(true);
+ } else {
+ mBubblePositioner.setUsePinnedLocation(false);
+ }
addToWindowManagerMaybe();
}
@@ -1732,13 +1674,6 @@
}
@Override
- public void onTaskbarChanged(Bundle b) {
- mMainExecutor.execute(() -> {
- BubbleController.this.onTaskbarChanged(b);
- });
- }
-
- @Override
public boolean handleDismissalInterception(BubbleEntry entry,
@Nullable List<BubbleEntry> children, IntConsumer removeCallback,
Executor callbackExecutor) {
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/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index dbad5df..07c5852 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -713,6 +713,9 @@
* is being shown.
*/
public PointF getDefaultStartPosition() {
+ if (mPinLocation != null) {
+ return mPinLocation;
+ }
// Start on the left if we're in LTR, right otherwise.
final boolean startOnLeft =
mContext.getResources().getConfiguration().getLayoutDirection()
@@ -766,11 +769,18 @@
}
/**
- * In some situations bubbles will be pinned to a specific onscreen location. This sets the
- * location to anchor the stack to.
+ * In some situations bubbles will be pinned to a specific onscreen location. This sets whether
+ * bubbles should be pinned or not.
*/
- public void setPinnedLocation(PointF point) {
- mPinLocation = point;
+ public void setUsePinnedLocation(boolean usePinnedLocation) {
+ if (usePinnedLocation) {
+ mShowingInTaskbar = true;
+ mPinLocation = new PointF(mPositionRect.right - mBubbleSize,
+ mPositionRect.bottom - mBubbleSize);
+ } else {
+ mPinLocation = null;
+ mShowingInTaskbar = false;
+ }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index be100bb..6efad09 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -613,16 +613,11 @@
mBubbleContainer.setActiveController(mStackAnimationController);
hideFlyoutImmediate();
- if (mPositioner.showingInTaskbar()) {
- // In taskbar, the stack isn't draggable so we shouldn't dispatch touch events.
- mMagnetizedObject = null;
- } else {
- // Save the magnetized stack so we can dispatch touch events to it.
- mMagnetizedObject = mStackAnimationController.getMagnetizedStack();
- mMagnetizedObject.clearAllTargets();
- mMagnetizedObject.addTarget(mMagneticTarget);
- mMagnetizedObject.setMagnetListener(mStackMagnetListener);
- }
+ // Save the magnetized stack so we can dispatch touch events to it.
+ mMagnetizedObject = mStackAnimationController.getMagnetizedStack();
+ mMagnetizedObject.clearAllTargets();
+ mMagnetizedObject.addTarget(mMagneticTarget);
+ mMagnetizedObject.setMagnetListener(mStackMagnetListener);
mIsDraggingStack = true;
@@ -641,10 +636,7 @@
public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
float viewInitialY, float dx, float dy) {
// If we're expanding or collapsing, ignore all touch events.
- if (mIsExpansionAnimating
- // Also ignore events if we shouldn't be draggable.
- || (mPositioner.showingInTaskbar() && !mIsExpanded)
- || mShowedUserEducationInTouchListenerActive) {
+ if (mIsExpansionAnimating || mShowedUserEducationInTouchListenerActive) {
return;
}
@@ -661,7 +653,7 @@
// bubble since it's stuck to the target.
if (!passEventToMagnetizedObject(ev)) {
updateBubbleShadows(true /* showForAllBubbles */);
- if (mBubbleData.isExpanded() || mPositioner.showingInTaskbar()) {
+ if (mBubbleData.isExpanded()) {
mExpandedAnimationController.dragBubbleOut(
v, viewInitialX + dx, viewInitialY + dy);
} else {
@@ -678,9 +670,7 @@
public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
float viewInitialY, float dx, float dy, float velX, float velY) {
// If we're expanding or collapsing, ignore all touch events.
- if (mIsExpansionAnimating
- // Also ignore events if we shouldn't be draggable.
- || (mPositioner.showingInTaskbar() && !mIsExpanded)) {
+ if (mIsExpansionAnimating) {
return;
}
if (mShowedUserEducationInTouchListenerActive) {
@@ -696,6 +686,8 @@
// Re-show the expanded view if we hid it.
showExpandedViewIfNeeded();
+ } else if (mPositioner.showingInTaskbar()) {
+ mStackAnimationController.snapStackBack();
} else {
// Fling the stack to the edge, and save whether or not it's going to end up on
// the left side of the screen.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index b3104b5..7f891ec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -23,7 +23,6 @@
import android.app.NotificationChannel;
import android.content.pm.UserInfo;
-import android.os.Bundle;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.RankingMap;
@@ -114,9 +113,6 @@
@Nullable
Bubble getBubbleWithShortcutId(String shortcutId);
- /** Called for any taskbar changes. */
- void onTaskbarChanged(Bundle b);
-
/**
* We intercept notification entries (including group summaries) dismissed by the user when
* there is an active bubble associated with it. We do this so that developers can still
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
index 961722b..0ee0ea6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
@@ -417,6 +417,17 @@
}
/**
+ * Snaps the stack back to the previous resting position.
+ */
+ public void snapStackBack() {
+ if (mLayout == null) {
+ return;
+ }
+ PointF p = getStackPositionAlongNearestHorizontalEdge();
+ springStackAfterFling(p.x, p.y);
+ }
+
+ /**
* Where the stack would be if it were snapped to the nearest horizontal edge (left or right).
*/
public PointF getStackPositionAlongNearestHorizontalEdge() {
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/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 33761d2..2b36b4c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -452,14 +452,17 @@
@NonNull Transitions.TransitionFinishCallback finishCallback,
@NonNull TaskInfo taskInfo, @Nullable TransitionInfo.Change pipTaskChange) {
TransitionInfo.Change pipChange = pipTaskChange;
- if (pipChange == null) {
+ if (mCurrentPipTaskToken == null) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: There is no existing PiP Task for TRANSIT_EXIT_PIP", TAG);
+ } else if (pipChange == null) {
// The pipTaskChange is null, this can happen if we are reparenting the PIP activity
// back to its original Task. In that case, we should animate the activity leash
- // instead, which should be the only non-task, independent, TRANSIT_CHANGE window.
+ // instead, which should be the change whose last parent is the recorded PiP Task.
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
- if (change.getTaskInfo() == null && change.getMode() == TRANSIT_CHANGE
- && TransitionInfo.isIndependent(change, info)) {
+ if (mCurrentPipTaskToken.equals(change.getLastParent())) {
+ // Find the activity that is exiting PiP.
pipChange = change;
break;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 30124a5..616d447 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -745,6 +745,15 @@
// Directly move PiP to its final destination bounds without animation.
mPipTaskOrganizer.scheduleFinishResizePip(postChangeBounds);
}
+
+ // if the pip window size is beyond allowed bounds user resize to normal bounds
+ if (mPipBoundsState.getBounds().width() < mPipBoundsState.getMinSize().x
+ || mPipBoundsState.getBounds().width() > mPipBoundsState.getMaxSize().x
+ || mPipBoundsState.getBounds().height() < mPipBoundsState.getMinSize().y
+ || mPipBoundsState.getBounds().height() > mPipBoundsState.getMaxSize().y) {
+ mTouchHandler.userResizeTo(mPipBoundsState.getNormalBounds(), snapFraction);
+ }
+
} else {
updateDisplayLayout.run();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index 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/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
index 89d85e4..41ff0b3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
@@ -96,6 +96,7 @@
private final Rect mDisplayBounds = new Rect();
private final Function<Rect, Rect> mMovementBoundsSupplier;
private final Runnable mUpdateMovementBoundsRunnable;
+ private final Consumer<Rect> mUpdateResizeBoundsCallback;
private int mDelta;
private float mTouchSlop;
@@ -137,6 +138,13 @@
mPhonePipMenuController = menuActivityController;
mPipUiEventLogger = pipUiEventLogger;
mPinchResizingAlgorithm = new PipPinchResizingAlgorithm();
+
+ mUpdateResizeBoundsCallback = (rect) -> {
+ mUserResizeBounds.set(rect);
+ mMotionHelper.synchronizePinnedStackBounds();
+ mUpdateMovementBoundsRunnable.run();
+ resetState();
+ };
}
public void init() {
@@ -508,15 +516,50 @@
}
}
+ private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) {
+ final int leftEdge = bounds.left;
+
+
+ final int fromLeft = Math.abs(leftEdge - movementBounds.left);
+ final int fromRight = Math.abs(movementBounds.right - leftEdge);
+
+ // The PIP will be snapped to either the right or left edge, so calculate which one
+ // is closest to the current position.
+ final int newLeft = fromLeft < fromRight
+ ? movementBounds.left : movementBounds.right;
+
+ bounds.offsetTo(newLeft, mLastResizeBounds.top);
+ }
+
+ /**
+ * Resizes the pip window and updates user-resized bounds.
+ *
+ * @param bounds target bounds to resize to
+ * @param snapFraction snap fraction to apply after resizing
+ */
+ void userResizeTo(Rect bounds, float snapFraction) {
+ Rect finalBounds = new Rect(bounds);
+
+ // get the current movement bounds
+ final Rect movementBounds = mPipBoundsAlgorithm.getMovementBounds(finalBounds);
+
+ // snap the target bounds to the either left or right edge, by choosing the closer one
+ snapToMovementBoundsEdge(finalBounds, movementBounds);
+
+ // apply the requested snap fraction onto the target bounds
+ mPipBoundsAlgorithm.applySnapFraction(finalBounds, snapFraction);
+
+ // resize from current bounds to target bounds without animation
+ mPipTaskOrganizer.scheduleUserResizePip(mPipBoundsState.getBounds(), finalBounds, null);
+ // set the flag that pip has been resized
+ mPipBoundsState.setHasUserResizedPip(true);
+
+ // finish the resize operation and update the state of the bounds
+ mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback);
+ }
+
private void finishResize() {
if (!mLastResizeBounds.isEmpty()) {
- final Consumer<Rect> callback = (rect) -> {
- mUserResizeBounds.set(mLastResizeBounds);
- mMotionHelper.synchronizePinnedStackBounds();
- mUpdateMovementBoundsRunnable.run();
- resetState();
- };
-
// Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped
// position correctly. Drag-resize does not need to move, so just finalize resize.
if (mOngoingPinchToResize) {
@@ -526,24 +569,23 @@
|| mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) {
resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y);
}
- final int leftEdge = mLastResizeBounds.left;
- final Rect movementBounds =
- mPipBoundsAlgorithm.getMovementBounds(mLastResizeBounds);
- final int fromLeft = Math.abs(leftEdge - movementBounds.left);
- final int fromRight = Math.abs(movementBounds.right - leftEdge);
- // The PIP will be snapped to either the right or left edge, so calculate which one
- // is closest to the current position.
- final int newLeft = fromLeft < fromRight
- ? movementBounds.left : movementBounds.right;
- mLastResizeBounds.offsetTo(newLeft, mLastResizeBounds.top);
+
+ // get the current movement bounds
+ final Rect movementBounds = mPipBoundsAlgorithm
+ .getMovementBounds(mLastResizeBounds);
+
+ // snap mLastResizeBounds to the correct edge based on movement bounds
+ snapToMovementBoundsEdge(mLastResizeBounds, movementBounds);
+
final float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
mLastResizeBounds, movementBounds);
mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction);
mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds,
- PINCH_RESIZE_SNAP_DURATION, mAngle, callback);
+ PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback);
} else {
mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds,
- PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, callback);
+ PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE,
+ mUpdateResizeBoundsCallback);
}
final float magnetRadiusPercent = (float) mLastResizeBounds.width() / mMinSize.x / 2.f;
mPipDismissTargetHandler
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index 1f3f31e..975d4bb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -825,6 +825,16 @@
}
/**
+ * Resizes the pip window and updates user resized bounds
+ *
+ * @param bounds target bounds to resize to
+ * @param snapFraction snap fraction to apply after resizing
+ */
+ void userResizeTo(Rect bounds, float snapFraction) {
+ mPipResizeGestureHandler.userResizeTo(bounds, snapFraction);
+ }
+
+ /**
* Gesture controlling normal movement of the PIP.
*/
private class DefaultPipTouchGesture extends PipTouchGesture {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
index b71cc32..1a6c1d6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
@@ -16,7 +16,7 @@
package com.android.wm.shell.recents;
-import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
import com.android.wm.shell.recents.IRecentTasksListener;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
@@ -44,5 +44,5 @@
/**
* Gets the set of running tasks.
*/
- ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) = 4;
+ RunningTaskInfo[] getRunningTasks(int maxNum) = 4;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
index 59f7233..e8f58fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
@@ -16,7 +16,7 @@
package com.android.wm.shell.recents;
-import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
/**
* Listener interface that Launcher attaches to SystemUI to get split-screen callbacks.
@@ -31,10 +31,10 @@
/**
* Called when a running task appears.
*/
- void onRunningTaskAppeared(in ActivityManager.RunningTaskInfo taskInfo);
+ void onRunningTaskAppeared(in RunningTaskInfo taskInfo);
/**
* Called when a running task vanishes.
*/
- void onRunningTaskVanished(in ActivityManager.RunningTaskInfo taskInfo);
-}
\ No newline at end of file
+ void onRunningTaskVanished(in RunningTaskInfo taskInfo);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 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/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 9102bd3..e2ac01f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -207,6 +207,7 @@
private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
private DefaultMixedHandler mMixedHandler;
+ private final Toast mSplitUnsupportedToast;
private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks =
new SplitWindowManager.ParentContainerCallbacks() {
@@ -300,6 +301,8 @@
mDisplayLayout = new DisplayLayout(displayController.getDisplayLayout(displayId));
transitions.addHandler(this);
mTaskOrganizer.addFocusListener(this);
+ mSplitUnsupportedToast = Toast.makeText(mContext,
+ R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT);
}
@VisibleForTesting
@@ -329,6 +332,8 @@
mDisplayController.addDisplayWindowListener(this);
mDisplayLayout = new DisplayLayout();
transitions.addHandler(this);
+ mSplitUnsupportedToast = Toast.makeText(mContext,
+ R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT);
}
public void setMixedHandler(DefaultMixedHandler mixedHandler) {
@@ -470,6 +475,7 @@
mMainExecutor.execute(() ->
exitSplitScreen(mMainStage.getChildCount() == 0
? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
+ mSplitUnsupportedToast.show();
} else {
// Switch the split position if launching as MULTIPLE_TASK failed.
if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
@@ -736,6 +742,7 @@
mMainExecutor.execute(() ->
exitSplitScreen(mMainStage.getChildCount() == 0
? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
+ mSplitUnsupportedToast.show();
} else {
mSyncQueue.queue(evictWct);
}
@@ -2287,13 +2294,11 @@
@Override
public void onNoLongerSupportMultiWindow() {
if (mMainStage.isActive()) {
- final Toast splitUnsupportedToast = Toast.makeText(mContext,
- R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT);
final boolean isMainStage = mMainStageListener == this;
if (!ENABLE_SHELL_TRANSITIONS) {
StageCoordinator.this.exitSplitScreen(isMainStage ? mMainStage : mSideStage,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
- splitUnsupportedToast.show();
+ mSplitUnsupportedToast.show();
return;
}
@@ -2302,7 +2307,7 @@
prepareExitSplitScreen(stageType, wct);
mSplitTransitions.startDismissTransition(wct,StageCoordinator.this, stageType,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
- splitUnsupportedToast.show();
+ mSplitUnsupportedToast.show();
}
}
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/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index dbb2948..af79386 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -59,6 +59,7 @@
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN;
import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
import static com.android.wm.shell.transition.TransitionAnimationHelper.sDisableCustomTaskAnimationProperty;
@@ -76,10 +77,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.graphics.Canvas;
import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -89,7 +87,6 @@
import android.os.UserHandle;
import android.util.ArrayMap;
import android.view.Choreographer;
-import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.view.WindowManager;
@@ -525,123 +522,6 @@
}
}
- private void edgeExtendWindow(TransitionInfo.Change change,
- Animation a, SurfaceControl.Transaction startTransaction,
- SurfaceControl.Transaction finishTransaction) {
- // Do not create edge extension surface for transfer starting window change.
- // The app surface could be empty thus nothing can draw on the hardware renderer, which will
- // block this thread when calling Surface#unlockCanvasAndPost.
- if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
- return;
- }
- final Transformation transformationAtStart = new Transformation();
- a.getTransformationAt(0, transformationAtStart);
- final Transformation transformationAtEnd = new Transformation();
- a.getTransformationAt(1, transformationAtEnd);
-
- // We want to create an extension surface that is the maximal size and the animation will
- // take care of cropping any part that overflows.
- final Insets maxExtensionInsets = Insets.min(
- transformationAtStart.getInsets(), transformationAtEnd.getInsets());
-
- final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(),
- change.getEndAbsBounds().height());
- final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(),
- change.getEndAbsBounds().width());
- if (maxExtensionInsets.left < 0) {
- final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- -maxExtensionInsets.left, targetSurfaceHeight);
- final int xPos = maxExtensionInsets.left;
- final int yPos = 0;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Left Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.top < 0) {
- final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1);
- final Rect extensionRect = new Rect(0, 0,
- targetSurfaceWidth, -maxExtensionInsets.top);
- final int xPos = 0;
- final int yPos = maxExtensionInsets.top;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Top Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.right < 0) {
- final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0,
- targetSurfaceWidth, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- -maxExtensionInsets.right, targetSurfaceHeight);
- final int xPos = targetSurfaceWidth;
- final int yPos = 0;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Right Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.bottom < 0) {
- final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1,
- targetSurfaceWidth, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- targetSurfaceWidth, -maxExtensionInsets.bottom);
- final int xPos = maxExtensionInsets.left;
- final int yPos = targetSurfaceHeight;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Bottom Edge Extension", startTransaction, finishTransaction);
- }
- }
-
- private SurfaceControl createExtensionSurface(SurfaceControl surfaceToExtend, Rect edgeBounds,
- Rect extensionRect, int xPos, int yPos, String layerName,
- SurfaceControl.Transaction startTransaction,
- SurfaceControl.Transaction finishTransaction) {
- final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder()
- .setName(layerName)
- .setParent(surfaceToExtend)
- .setHidden(true)
- .setCallsite("DefaultTransitionHandler#startAnimation")
- .setOpaque(true)
- .setBufferSize(extensionRect.width(), extensionRect.height())
- .build();
-
- SurfaceControl.LayerCaptureArgs captureArgs =
- new SurfaceControl.LayerCaptureArgs.Builder(surfaceToExtend)
- .setSourceCrop(edgeBounds)
- .setFrameScale(1)
- .setPixelFormat(PixelFormat.RGBA_8888)
- .setChildrenOnly(true)
- .setAllowProtected(true)
- .build();
- final SurfaceControl.ScreenshotHardwareBuffer edgeBuffer =
- SurfaceControl.captureLayers(captureArgs);
-
- if (edgeBuffer == null) {
- ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- "Failed to capture edge of window.");
- return null;
- }
-
- android.graphics.BitmapShader shader =
- new android.graphics.BitmapShader(edgeBuffer.asBitmap(),
- android.graphics.Shader.TileMode.CLAMP,
- android.graphics.Shader.TileMode.CLAMP);
- final Paint paint = new Paint();
- paint.setShader(shader);
-
- final Surface surface = new Surface(edgeExtensionLayer);
- Canvas c = surface.lockHardwareCanvas();
- c.drawRect(extensionRect, paint);
- surface.unlockCanvasAndPost(c);
- surface.release();
-
- startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
- startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
- startTransaction.setVisibility(edgeExtensionLayer, true);
- finishTransaction.remove(edgeExtensionLayer);
-
- return edgeExtensionLayer;
- }
-
@Nullable
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@@ -739,12 +619,13 @@
// Animation length is already expected to be scaled.
va.overrideDurationScale(1.0f);
va.setDuration(anim.computeDurationHint());
- va.addUpdateListener(animation -> {
+ final ValueAnimator.AnimatorUpdateListener updateListener = animation -> {
final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime());
applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix,
position, cornerRadius, clipRect);
- });
+ };
+ va.addUpdateListener(updateListener);
final Runnable finisher = () -> {
applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix,
@@ -757,20 +638,30 @@
});
};
va.addListener(new AnimatorListenerAdapter() {
+ // It is possible for the end/cancel to be called more than once, which may cause
+ // issues if the animating surface has already been released. Track the finished
+ // state here to skip duplicate callbacks. See b/252872225.
private boolean mFinished = false;
@Override
public void onAnimationEnd(Animator animation) {
- if (mFinished) return;
- mFinished = true;
- finisher.run();
+ onFinish();
}
@Override
public void onAnimationCancel(Animator animation) {
+ onFinish();
+ }
+
+ private void onFinish() {
if (mFinished) return;
mFinished = true;
finisher.run();
+ // The update listener can continue to be called after the animation has ended if
+ // end() is called manually again before the finisher removes the animation.
+ // Remove it manually here to prevent animating a released surface.
+ // See b/252872225.
+ va.removeUpdateListener(updateListener);
}
});
animations.add(va);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index efee6f40..b75c552 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -24,6 +24,7 @@
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.view.WindowManager.transitTypeToString;
+import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
@@ -34,10 +35,19 @@
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.Shader;
import android.os.SystemProperties;
+import android.view.Surface;
import android.view.SurfaceControl;
import android.view.animation.Animation;
+import android.view.animation.Transformation;
import android.window.TransitionInfo;
import com.android.internal.R;
@@ -217,4 +227,126 @@
.show(animationBackgroundSurface);
finishTransaction.remove(animationBackgroundSurface);
}
+
+ /**
+ * Adds edge extension surface to the given {@code change} for edge extension animation.
+ */
+ public static void edgeExtendWindow(@NonNull TransitionInfo.Change change,
+ @NonNull Animation a, @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ // Do not create edge extension surface for transfer starting window change.
+ // The app surface could be empty thus nothing can draw on the hardware renderer, which will
+ // block this thread when calling Surface#unlockCanvasAndPost.
+ if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
+ return;
+ }
+ final Transformation transformationAtStart = new Transformation();
+ a.getTransformationAt(0, transformationAtStart);
+ final Transformation transformationAtEnd = new Transformation();
+ a.getTransformationAt(1, transformationAtEnd);
+
+ // We want to create an extension surface that is the maximal size and the animation will
+ // take care of cropping any part that overflows.
+ final Insets maxExtensionInsets = Insets.min(
+ transformationAtStart.getInsets(), transformationAtEnd.getInsets());
+
+ final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(),
+ change.getEndAbsBounds().height());
+ final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(),
+ change.getEndAbsBounds().width());
+ if (maxExtensionInsets.left < 0) {
+ final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight);
+ final Rect extensionRect = new Rect(0, 0,
+ -maxExtensionInsets.left, targetSurfaceHeight);
+ final int xPos = maxExtensionInsets.left;
+ final int yPos = 0;
+ createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
+ "Left Edge Extension", startTransaction, finishTransaction);
+ }
+
+ if (maxExtensionInsets.top < 0) {
+ final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1);
+ final Rect extensionRect = new Rect(0, 0,
+ targetSurfaceWidth, -maxExtensionInsets.top);
+ final int xPos = 0;
+ final int yPos = maxExtensionInsets.top;
+ createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
+ "Top Edge Extension", startTransaction, finishTransaction);
+ }
+
+ if (maxExtensionInsets.right < 0) {
+ final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0,
+ targetSurfaceWidth, targetSurfaceHeight);
+ final Rect extensionRect = new Rect(0, 0,
+ -maxExtensionInsets.right, targetSurfaceHeight);
+ final int xPos = targetSurfaceWidth;
+ final int yPos = 0;
+ createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
+ "Right Edge Extension", startTransaction, finishTransaction);
+ }
+
+ if (maxExtensionInsets.bottom < 0) {
+ final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1,
+ targetSurfaceWidth, targetSurfaceHeight);
+ final Rect extensionRect = new Rect(0, 0,
+ targetSurfaceWidth, -maxExtensionInsets.bottom);
+ final int xPos = maxExtensionInsets.left;
+ final int yPos = targetSurfaceHeight;
+ createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
+ "Bottom Edge Extension", startTransaction, finishTransaction);
+ }
+ }
+
+ /**
+ * Takes a screenshot of {@code surfaceToExtend}'s edge and extends it for edge extension
+ * animation.
+ */
+ private static SurfaceControl createExtensionSurface(@NonNull SurfaceControl surfaceToExtend,
+ @NonNull Rect edgeBounds, @NonNull Rect extensionRect, int xPos, int yPos,
+ @NonNull String layerName, @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder()
+ .setName(layerName)
+ .setParent(surfaceToExtend)
+ .setHidden(true)
+ .setCallsite("TransitionAnimationHelper#createExtensionSurface")
+ .setOpaque(true)
+ .setBufferSize(extensionRect.width(), extensionRect.height())
+ .build();
+
+ final SurfaceControl.LayerCaptureArgs captureArgs =
+ new SurfaceControl.LayerCaptureArgs.Builder(surfaceToExtend)
+ .setSourceCrop(edgeBounds)
+ .setFrameScale(1)
+ .setPixelFormat(PixelFormat.RGBA_8888)
+ .setChildrenOnly(true)
+ .setAllowProtected(true)
+ .build();
+ final SurfaceControl.ScreenshotHardwareBuffer edgeBuffer =
+ SurfaceControl.captureLayers(captureArgs);
+
+ if (edgeBuffer == null) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Failed to capture edge of window.");
+ return null;
+ }
+
+ final BitmapShader shader = new BitmapShader(edgeBuffer.asBitmap(),
+ Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+ final Paint paint = new Paint();
+ paint.setShader(shader);
+
+ final Surface surface = new Surface(edgeExtensionLayer);
+ final Canvas c = surface.lockHardwareCanvas();
+ c.drawRect(extensionRect, paint);
+ surface.unlockCanvasAndPost(c);
+ surface.release();
+
+ startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
+ startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
+ startTransaction.setVisibility(edgeExtensionLayer, true);
+ finishTransaction.remove(edgeExtensionLayer);
+
+ return edgeExtensionLayer;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index d1bc738..db1f19a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -322,6 +322,11 @@
boolean isOpening = isOpeningType(info.getType());
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
+ if ((change.getFlags() & TransitionInfo.FLAG_IS_SYSTEM_WINDOW) != 0) {
+ // Currently system windows are controlled by WindowState, so don't change their
+ // surfaces. Otherwise their window tokens could be hidden unexpectedly.
+ continue;
+ }
final SurfaceControl leash = change.getLeash();
final int mode = info.getChanges().get(i).getMode();
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 beace75..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
@@ -21,7 +21,6 @@
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
-import android.graphics.Rect;
import android.graphics.drawable.VectorDrawable;
import android.os.Handler;
import android.view.Choreographer;
@@ -43,22 +42,6 @@
* The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't.
*/
public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
- // The thickness of shadows of a window that has focus in DIP.
- private static final int DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP = 20;
- // The thickness of shadows of a window that doesn't have focus in DIP.
- private static final int DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP = 5;
-
- // Height of button (32dp) + 2 * margin (5dp each)
- private static final int DECOR_CAPTION_HEIGHT_IN_DIP = 42;
- // Width of buttons (64dp) + handle (128dp) + padding (24dp total)
- private static final int DECOR_CAPTION_WIDTH_IN_DIP = 216;
- private static final int RESIZE_HANDLE_IN_DIP = 30;
- private static final int RESIZE_CORNER_IN_DIP = 44;
-
- private static final Rect EMPTY_OUTSET = new Rect();
- private static final Rect RESIZE_HANDLE_OUTSET = new Rect(
- RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP);
-
private final Handler mHandler;
private final Choreographer mChoreographer;
private final SyncTransactionQueue mSyncQueue;
@@ -69,6 +52,7 @@
private DragResizeInputListener mDragResizeListener;
+ private RelayoutParams mRelayoutParams = new RelayoutParams();
private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
new WindowDecoration.RelayoutResult<>();
@@ -114,19 +98,32 @@
void relayout(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
- final int shadowRadiusDp = taskInfo.isFocused
- ? DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP : DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP;
- final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode()
- == WindowConfiguration.WINDOWING_MODE_FREEFORM;
- final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable;
- final Rect outset = isDragResizeable ? RESIZE_HANDLE_OUTSET : EMPTY_OUTSET;
+ final int shadowRadiusID = taskInfo.isFocused
+ ? R.dimen.freeform_decor_shadow_focused_thickness
+ : R.dimen.freeform_decor_shadow_unfocused_thickness;
+ final boolean isFreeform =
+ taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
+ final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
final WindowContainerTransaction wct = new WindowContainerTransaction();
- relayout(taskInfo, R.layout.caption_window_decoration, oldRootView,
- DECOR_CAPTION_HEIGHT_IN_DIP, DECOR_CAPTION_WIDTH_IN_DIP, outset, shadowRadiusDp,
- startT, finishT, wct, mResult);
+
+ int outsetLeftId = R.dimen.freeform_resize_handle;
+ int outsetTopId = R.dimen.freeform_resize_handle;
+ int outsetRightId = R.dimen.freeform_resize_handle;
+ int outsetBottomId = R.dimen.freeform_resize_handle;
+
+ mRelayoutParams.reset();
+ mRelayoutParams.mRunningTaskInfo = taskInfo;
+ mRelayoutParams.mLayoutResId = R.layout.caption_window_decoration;
+ mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
+ mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width;
+ mRelayoutParams.mShadowRadiusId = shadowRadiusID;
+ if (isDragResizeable) {
+ mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
+ }
+ relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
mTaskOrganizer.applyTransaction(wct);
@@ -167,10 +164,12 @@
}
int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()).getScaledTouchSlop();
-
+ int resize_handle = mResult.mRootView.getResources()
+ .getDimensionPixelSize(R.dimen.freeform_resize_handle);
+ int resize_corner = mResult.mRootView.getResources()
+ .getDimensionPixelSize(R.dimen.freeform_resize_corner);
mDragResizeListener.setGeometry(
- mResult.mWidth, mResult.mHeight, (int) (mResult.mDensity * RESIZE_HANDLE_IN_DIP),
- (int) (mResult.mDensity * RESIZE_CORNER_IN_DIP), touchSlop);
+ mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop);
}
/**
@@ -218,7 +217,7 @@
View handle = caption.findViewById(R.id.caption_handle);
VectorDrawable handleBackground = (VectorDrawable) handle.getBackground();
handleBackground.setTintList(buttonTintColor);
- caption.setBackgroundColor(v == View.VISIBLE ? Color.WHITE : Color.TRANSPARENT);
+ caption.getBackground().setTint(v == View.VISIBLE ? Color.WHITE : Color.TRANSPARENT);
}
private void closeDragResizeListener() {
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 bf863ea..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
@@ -19,11 +19,11 @@
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
-import android.util.DisplayMetrics;
import android.view.Display;
import android.view.InsetsState;
import android.view.LayoutInflater;
@@ -91,7 +91,7 @@
SurfaceControl mTaskBackgroundSurface;
SurfaceControl mCaptionContainerSurface;
- private CaptionWindowManager mCaptionWindowManager;
+ private WindowlessWindowManager mCaptionWindowManager;
private SurfaceControlViewHost mViewHost;
private final Rect mCaptionInsetsRect = new Rect();
@@ -142,15 +142,14 @@
*/
abstract void relayout(RunningTaskInfo taskInfo);
- void relayout(RunningTaskInfo taskInfo, int layoutResId, T rootView, float captionHeightDp,
- float captionWidthDp, Rect outsetsDp, float shadowRadiusDp,
- SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- WindowContainerTransaction wct, RelayoutResult<T> outResult) {
+ void relayout(RelayoutParams params, SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView,
+ RelayoutResult<T> outResult) {
outResult.reset();
final Configuration oldTaskConfig = mTaskInfo.getConfiguration();
- if (taskInfo != null) {
- mTaskInfo = taskInfo;
+ if (params.mRunningTaskInfo != null) {
+ mTaskInfo = params.mRunningTaskInfo;
}
if (!mTaskInfo.isVisible) {
@@ -159,7 +158,7 @@
return;
}
- if (rootView == null && layoutResId == 0) {
+ if (rootView == null && params.mLayoutResId == 0) {
throw new IllegalArgumentException("layoutResId and rootView can't both be invalid.");
}
@@ -176,15 +175,15 @@
return;
}
mDecorWindowContext = mContext.createConfigurationContext(taskConfig);
- if (layoutResId != 0) {
- outResult.mRootView =
- (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null);
+ if (params.mLayoutResId != 0) {
+ outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext)
+ .inflate(params.mLayoutResId, null);
}
}
if (outResult.mRootView == null) {
- outResult.mRootView =
- (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null);
+ outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext)
+ .inflate(params.mLayoutResId , null);
}
// DecorationContainerSurface
@@ -200,18 +199,19 @@
}
final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
- outResult.mDensity = taskConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
- final int decorContainerOffsetX = -(int) (outsetsDp.left * outResult.mDensity);
- final int decorContainerOffsetY = -(int) (outsetsDp.top * outResult.mDensity);
+ final Resources resources = mDecorWindowContext.getResources();
+ final int decorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId);
+ final int decorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId);
outResult.mWidth = taskBounds.width()
- + (int) (outsetsDp.right * outResult.mDensity)
+ + loadDimensionPixelSize(resources, params.mOutsetRightId)
- decorContainerOffsetX;
outResult.mHeight = taskBounds.height()
- + (int) (outsetsDp.bottom * outResult.mDensity)
+ + loadDimensionPixelSize(resources, params.mOutsetBottomId)
- decorContainerOffsetY;
startT.setPosition(
mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY)
- .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight)
+ .setWindowCrop(mDecorationContainerSurface,
+ outResult.mWidth, outResult.mHeight)
// TODO(b/244455401): Change the z-order when it's better organized
.setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1)
.show(mDecorationContainerSurface);
@@ -226,12 +226,13 @@
.build();
}
- float shadowRadius = outResult.mDensity * shadowRadiusDp;
+ float shadowRadius = loadDimension(resources, params.mShadowRadiusId);
int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor();
mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f;
mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f;
mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f;
- startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height())
+ startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(),
+ taskBounds.height())
.setShadowRadius(mTaskBackgroundSurface, shadowRadius)
.setColor(mTaskBackgroundSurface, mTmpColor)
// TODO(b/244455401): Change the z-order when it's better organized
@@ -248,8 +249,8 @@
.build();
}
- final int captionHeight = (int) Math.ceil(captionHeightDp * outResult.mDensity);
- final int captionWidth = (int) Math.ceil(captionWidthDp * outResult.mDensity);
+ final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
+ final int captionWidth = loadDimensionPixelSize(resources, params.mCaptionWidthId);
//Prevent caption from going offscreen if task is too high up
final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2;
@@ -264,8 +265,9 @@
if (mCaptionWindowManager == null) {
// Put caption under a container surface because ViewRootImpl sets the destination frame
// of windowless window layers and BLASTBufferQueue#update() doesn't support offset.
- mCaptionWindowManager = new CaptionWindowManager(
- mTaskInfo.getConfiguration(), mCaptionContainerSurface);
+ mCaptionWindowManager = new WindowlessWindowManager(
+ mTaskInfo.getConfiguration(), mCaptionContainerSurface,
+ null /* hostInputToken */);
}
// Caption view
@@ -289,8 +291,10 @@
// Caption insets
mCaptionInsetsRect.set(taskBounds);
- mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight - captionYPos;
- wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, CAPTION_INSETS_TYPES);
+ mCaptionInsetsRect.bottom =
+ mCaptionInsetsRect.top + captionHeight - captionYPos;
+ wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect,
+ CAPTION_INSETS_TYPES);
} else {
startT.hide(mCaptionContainerSurface);
}
@@ -365,34 +369,67 @@
releaseViews();
}
+ private static int loadDimensionPixelSize(Resources resources, int resourceId) {
+ if (resourceId == Resources.ID_NULL) {
+ return 0;
+ }
+ return resources.getDimensionPixelSize(resourceId);
+ }
+
+ private static float loadDimension(Resources resources, int resourceId) {
+ if (resourceId == Resources.ID_NULL) {
+ return 0;
+ }
+ return resources.getDimension(resourceId);
+ }
+
+ static class RelayoutParams{
+ RunningTaskInfo mRunningTaskInfo;
+ int mLayoutResId;
+ int mCaptionHeightId;
+ int mCaptionWidthId;
+ int mShadowRadiusId;
+
+ int mOutsetTopId;
+ int mOutsetBottomId;
+ int mOutsetLeftId;
+ int mOutsetRightId;
+
+ void setOutsets(int leftId, int topId, int rightId, int bottomId) {
+ mOutsetLeftId = leftId;
+ mOutsetTopId = topId;
+ mOutsetRightId = rightId;
+ mOutsetBottomId = bottomId;
+ }
+
+ void reset() {
+ mLayoutResId = Resources.ID_NULL;
+ mCaptionHeightId = Resources.ID_NULL;
+ mCaptionWidthId = Resources.ID_NULL;
+ mShadowRadiusId = Resources.ID_NULL;
+
+ mOutsetTopId = Resources.ID_NULL;
+ mOutsetBottomId = Resources.ID_NULL;
+ mOutsetLeftId = Resources.ID_NULL;
+ mOutsetRightId = Resources.ID_NULL;
+ }
+ }
+
static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
int mWidth;
int mHeight;
- float mDensity;
T mRootView;
void reset() {
mWidth = 0;
mHeight = 0;
- mDensity = 0;
mRootView = null;
}
}
- private static class CaptionWindowManager extends WindowlessWindowManager {
- CaptionWindowManager(Configuration config, SurfaceControl rootSurface) {
- super(config, rootSurface, null /* hostInputToken */);
- }
-
- @Override
- public void setConfiguration(Configuration configuration) {
- super.setConfiguration(configuration);
- }
- }
-
interface SurfaceControlViewHostFactory {
default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
return new SurfaceControlViewHost(c, d, wmm);
}
}
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml
new file mode 100644
index 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/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
index 98b5912..79070b1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -40,6 +40,8 @@
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import java.util.ArrayList;
+
/**
* Tests for {@link ActivityEmbeddingAnimationRunner}.
*
@@ -62,13 +64,13 @@
final TransitionInfo.Change embeddingChange = createChange();
embeddingChange.setFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
info.addChange(embeddingChange);
- doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
+ doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any());
mAnimRunner.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction);
final ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class);
verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), eq(mFinishTransaction),
- finishCallback.capture());
+ finishCallback.capture(), any());
verify(mStartTransaction).apply();
verify(mAnimator).start();
verifyNoMoreInteractions(mFinishTransaction);
@@ -88,7 +90,8 @@
info.addChange(embeddingChange);
final Animator animator = mAnimRunner.createAnimator(
info, mStartTransaction, mFinishTransaction,
- () -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */));
+ () -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */),
+ new ArrayList());
// The animation should be empty when it is behind starting window.
assertEquals(0, animator.getDuration());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
index 3792e83..54a12ab 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
@@ -56,13 +56,12 @@
@Mock
SurfaceControl.Transaction mFinishTransaction;
@Mock
- Transitions.TransitionFinishCallback mFinishCallback;
- @Mock
Animator mAnimator;
ActivityEmbeddingController mController;
ActivityEmbeddingAnimationRunner mAnimRunner;
ActivityEmbeddingAnimationSpec mAnimSpec;
+ Transitions.TransitionFinishCallback mFinishCallback;
@CallSuper
@Before
@@ -75,9 +74,11 @@
assertNotNull(mAnimRunner);
mAnimSpec = mAnimRunner.mAnimationSpec;
assertNotNull(mAnimSpec);
+ mFinishCallback = (wct, wctCB) -> {};
spyOn(mController);
spyOn(mAnimRunner);
spyOn(mAnimSpec);
+ spyOn(mFinishCallback);
}
/** Creates a mock {@link TransitionInfo.Change}. */
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
index baecf6f..4d98b6b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
@@ -55,7 +55,7 @@
@Before
public void setup() {
super.setUp();
- doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
+ doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any());
}
@Test
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/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index dba037d..3bd2ae7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.pip.phone;
+import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -55,6 +56,7 @@
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class PipResizeGestureHandlerTest extends ShellTestCase {
+ private static final float DEFAULT_SNAP_FRACTION = 2.0f;
private static final int STEP_SIZE = 40;
private final MotionEvent.PointerProperties[] mPp = new MotionEvent.PointerProperties[2];
@@ -196,6 +198,51 @@
< mPipBoundsState.getBounds().width());
}
+ @Test
+ public void testUserResizeTo() {
+ // resizing the bounds to normal bounds at first
+ mPipResizeGestureHandler.userResizeTo(mPipBoundsState.getNormalBounds(),
+ DEFAULT_SNAP_FRACTION);
+
+ assertPipBoundsUserResizedTo(mPipBoundsState.getNormalBounds());
+
+ verify(mPipTaskOrganizer, times(1))
+ .scheduleUserResizePip(any(), any(), any());
+
+ verify(mPipTaskOrganizer, times(1))
+ .scheduleFinishResizePip(any(), any());
+
+ // bounds with max size
+ final Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x,
+ mPipBoundsState.getMaxSize().y);
+
+ // resizing the bounds to maximum bounds the second time
+ mPipResizeGestureHandler.userResizeTo(maxBounds, DEFAULT_SNAP_FRACTION);
+
+ assertPipBoundsUserResizedTo(maxBounds);
+
+ // another call to scheduleUserResizePip() and scheduleFinishResizePip() makes
+ // the total number of invocations 2 for each method
+ verify(mPipTaskOrganizer, times(2))
+ .scheduleUserResizePip(any(), any(), any());
+
+ verify(mPipTaskOrganizer, times(2))
+ .scheduleFinishResizePip(any(), any());
+ }
+
+ private void assertPipBoundsUserResizedTo(Rect bounds) {
+ // check user-resized bounds
+ assertEquals(mPipResizeGestureHandler.getUserResizeBounds().width(), bounds.width());
+ assertEquals(mPipResizeGestureHandler.getUserResizeBounds().height(), bounds.height());
+
+ // check if the bounds are the same
+ assertEquals(mPipBoundsState.getBounds().width(), bounds.width());
+ assertEquals(mPipBoundsState.getBounds().height(), bounds.height());
+
+ // a flag should be set to indicate pip has been resized by the user
+ assertTrue(mPipBoundsState.hasUserResizedPip());
+ }
+
private MotionEvent obtainMotionEvent(int action, int topLeft, int bottomRight) {
final MotionEvent.PointerCoords[] pc = new MotionEvent.PointerCoords[2];
for (int i = 0; i < 2; i++) {
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 fa62b9c..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,11 +50,13 @@
import android.window.WindowContainerTransaction;
import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.tests.R;
import org.junit.Before;
import org.junit.Test;
@@ -76,13 +78,9 @@
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class WindowDecorationTests extends ShellTestCase {
- private static final int CAPTION_HEIGHT_DP = 32;
- private static final int CAPTION_WIDTH_DP = 216;
- private static final int SHADOW_RADIUS_DP = 5;
private static final Rect TASK_BOUNDS = new Rect(100, 300, 400, 400);
private static final Point TASK_POSITION_IN_PARENT = new Point(40, 60);
- private final Rect mOutsetsDp = new Rect();
private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult =
new WindowDecoration.RelayoutResult<>();
@@ -104,6 +102,7 @@
private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>();
private SurfaceControl.Transaction mMockSurfaceControlStartT;
private SurfaceControl.Transaction mMockSurfaceControlFinishT;
+ private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams();
@Before
public void setUp() {
@@ -147,7 +146,11 @@
// Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
// 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- mOutsetsDp.set(10, 20, 30, 40);
+ mRelayoutParams.setOutsets(
+ R.dimen.test_window_decor_left_outset,
+ R.dimen.test_window_decor_top_outset,
+ R.dimen.test_window_decor_right_outset,
+ R.dimen.test_window_decor_bottom_outset);
final SurfaceControl taskSurface = mock(SurfaceControl.class);
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -197,8 +200,11 @@
// Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
// 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- mOutsetsDp.set(10, 20, 30, 40);
-
+ mRelayoutParams.setOutsets(
+ R.dimen.test_window_decor_left_outset,
+ R.dimen.test_window_decor_top_outset,
+ R.dimen.test_window_decor_right_outset,
+ R.dimen.test_window_decor_bottom_outset);
final SurfaceControl taskSurface = mock(SurfaceControl.class);
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -226,16 +232,17 @@
verify(mMockSurfaceControlStartT).show(captionContainerSurface);
verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
+
verify(mMockSurfaceControlViewHost)
.setView(same(mMockView),
argThat(lp -> lp.height == 64
- && lp.width == 300
+ && lp.width == 432
&& (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0));
if (ViewRootImpl.CAPTION_ON_SHELL) {
verify(mMockView).setTaskFocusState(true);
verify(mMockWindowContainerTransaction)
.addRectInsetsProvider(taskInfo.token,
- new Rect(100, 300, 400, 364),
+ new Rect(100, 300, 400, 332),
new int[] { InsetsState.ITYPE_CAPTION_BAR });
}
@@ -248,7 +255,6 @@
assertEquals(380, mRelayoutResult.mWidth);
assertEquals(220, mRelayoutResult.mHeight);
- assertEquals(2, mRelayoutResult.mDensity, 0.f);
}
@Test
@@ -287,7 +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;
- mOutsetsDp.set(10, 20, 30, 40);
+ mRelayoutParams.setOutsets(
+ R.dimen.test_window_decor_left_outset,
+ R.dimen.test_window_decor_top_outset,
+ R.dimen.test_window_decor_right_outset,
+ R.dimen.test_window_decor_bottom_outset);
final SurfaceControl taskSurface = mock(SurfaceControl.class);
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -358,7 +368,8 @@
private TestWindowDecoration createWindowDecoration(
ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) {
- return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer,
+ return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockDisplayController, mMockShellTaskOrganizer,
taskInfo, testSurface,
new MockObjectSupplier<>(mMockSurfaceControlBuilders,
() -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))),
@@ -410,9 +421,13 @@
@Override
void relayout(ActivityManager.RunningTaskInfo taskInfo) {
- relayout(null /* taskInfo */, 0 /* layoutResId */, mMockView, CAPTION_HEIGHT_DP,
- CAPTION_WIDTH_DP, mOutsetsDp, SHADOW_RADIUS_DP, mMockSurfaceControlStartT,
- mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mRelayoutResult);
+ mRelayoutParams.mLayoutResId = 0;
+ mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height;
+ mRelayoutParams.mCaptionWidthId = R.dimen.test_freeform_decor_caption_width;
+ mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius;
+
+ relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
+ mMockWindowContainerTransaction, mMockView, mRelayoutResult);
}
}
}
diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
index 2fe7b16..262f5f1 100644
--- a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
+++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
@@ -100,16 +100,12 @@
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
+ Log.d(TAG, "onConnectionStateChange() status: " + status + ", newState: " + newState);
String intentAction;
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.d(TAG, "Connected to GATT server.");
Log.d(TAG, "Attempting to start service discovery:" +
mBluetoothGatt.discoverServices());
- if (!mBluetoothGatt.requestMtu(MAX_PACKET_SIZE)) {
- Log.e(TAG, "request mtu failed");
- mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
- mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
- }
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.i(TAG, "Disconnected from GATT server.");
close();
@@ -118,6 +114,7 @@
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ Log.d(TAG, "onServicesDiscovered() status: " + status);
if (status == BluetoothGatt.GATT_SUCCESS) {
BluetoothGattService service = gatt.getService(MIDI_SERVICE);
if (service != null) {
@@ -137,9 +134,14 @@
// Specification says to read the characteristic first and then
// switch to receiving notifications
mBluetoothGatt.readCharacteristic(characteristic);
- }
- openBluetoothDevice(mBluetoothDevice);
+ // Request higher MTU size
+ if (!gatt.requestMtu(MAX_PACKET_SIZE)) {
+ Log.e(TAG, "request mtu failed");
+ mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
+ mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
+ }
+ }
}
} else {
Log.e(TAG, "onServicesDiscovered received: " + status);
@@ -235,13 +237,13 @@
System.arraycopy(buffer, 0, mCachedBuffer, 0, count);
if (DEBUG) {
- logByteArray("Sent ", mCharacteristic.getValue(), 0,
- mCharacteristic.getValue().length);
+ logByteArray("Sent ", mCachedBuffer, 0, mCachedBuffer.length);
}
- if (mBluetoothGatt.writeCharacteristic(mCharacteristic, mCachedBuffer,
- mCharacteristic.getWriteType()) != BluetoothGatt.GATT_SUCCESS) {
- Log.w(TAG, "could not write characteristic to Bluetooth GATT");
+ int result = mBluetoothGatt.writeCharacteristic(mCharacteristic, mCachedBuffer,
+ mCharacteristic.getWriteType());
+ if (result != BluetoothGatt.GATT_SUCCESS) {
+ Log.w(TAG, "could not write characteristic to Bluetooth GATT. result: " + result);
return false;
}
@@ -254,6 +256,10 @@
mBluetoothDevice = device;
mService = service;
+ // Set a small default packet size in case there is an issue with configuring MTUs.
+ mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
+ mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
+
mBluetoothGatt = mBluetoothDevice.connectGatt(context, false, mGattCallback);
mContext = context;
diff --git a/packages/CompanionDeviceManager/res/layout/list_item_device.xml b/packages/CompanionDeviceManager/res/layout/list_item_device.xml
index db54ae3..d4439f9 100644
--- a/packages/CompanionDeviceManager/res/layout/list_item_device.xml
+++ b/packages/CompanionDeviceManager/res/layout/list_item_device.xml
@@ -39,7 +39,6 @@
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="24dp"
- android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
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/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
index 5c9ab7b..4e7e367 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
@@ -350,7 +350,7 @@
}
return;
}
- if (DEBUG) Log.i(TAG, "onDeviceFound() " + device.toShortString() + " - New device.");
+ Log.i(TAG, "onDeviceFound() " + device.toShortString() + " - New device.");
// First: make change.
mDevicesFound.add(device);
@@ -363,9 +363,9 @@
});
}
- private void onDeviceLost(@Nullable DeviceFilterPair<?> device) {
+ private void onDeviceLost(@NonNull DeviceFilterPair<?> device) {
runOnMainThread(() -> {
- if (DEBUG) Log.i(TAG, "onDeviceLost(), device=" + device.toShortString());
+ Log.i(TAG, "onDeviceLost(), device=" + device.toShortString());
// First: make change.
mDevicesFound.remove(device);
diff --git a/packages/PackageInstaller/res/values-as/strings.xml b/packages/PackageInstaller/res/values-as/strings.xml
index dd776a9..8405335 100644
--- a/packages/PackageInstaller/res/values-as/strings.xml
+++ b/packages/PackageInstaller/res/values-as/strings.xml
@@ -28,11 +28,11 @@
<string name="install_confirm_question_update" msgid="3348888852318388584">"আপুনি এই এপ্টো আপডে’ট কৰিবলৈ বিচাৰেনে?"</string>
<string name="install_failed" msgid="5777824004474125469">"এপ্ ইনষ্টল কৰা হোৱা নাই।"</string>
<string name="install_failed_blocked" msgid="8512284352994752094">"পেকেজটোৰ ইনষ্টল অৱৰোধ কৰা হৈছে।"</string>
- <string name="install_failed_conflict" msgid="3493184212162521426">"এপটো ইনষ্টল কৰিব পৰা নগ\'ল কাৰণ ইয়াৰ সৈতে আগৰে পৰা থকা এটা পেকেজৰ সংঘাত হৈছে।"</string>
- <string name="install_failed_incompatible" product="tablet" msgid="6019021440094927928">"আপোনাৰ টেবলেটৰ সৈতে খাপ নোখোৱাৰ বাবে এপটো ইনষ্টল কৰা নহ\'ল।"</string>
- <string name="install_failed_incompatible" product="tv" msgid="2890001324362291683">"আপোনাৰ টিভিত এই এপটো নচলে"</string>
- <string name="install_failed_incompatible" product="default" msgid="7254630419511645826">"আপোনাৰ ফ\'নৰ সৈতে খাপ নোখোৱাৰ বাবে এপটো ইনষ্টল কৰা নহ\'ল।"</string>
- <string name="install_failed_invalid_apk" msgid="8581007676422623930">"পেকেজটো মান্য নোহোৱাৰ বাবে এপটো ইনষ্টল কৰা নহ\'ল।"</string>
+ <string name="install_failed_conflict" msgid="3493184212162521426">"এপ্টো ইনষ্টল কৰিব পৰা নগ\'ল কাৰণ ইয়াৰ সৈতে আগৰে পৰা থকা এটা পেকেজৰ সংঘাত হৈছে।"</string>
+ <string name="install_failed_incompatible" product="tablet" msgid="6019021440094927928">"আপোনাৰ টেবলেটৰ সৈতে খাপ নোখোৱাৰ বাবে এপ্টো ইনষ্টল কৰা নহ\'ল।"</string>
+ <string name="install_failed_incompatible" product="tv" msgid="2890001324362291683">"আপোনাৰ টিভিত এই এপ্টো নচলে"</string>
+ <string name="install_failed_incompatible" product="default" msgid="7254630419511645826">"আপোনাৰ ফ\'নৰ সৈতে খাপ নোখোৱাৰ বাবে এপ্টো ইনষ্টল কৰা নহ\'ল।"</string>
+ <string name="install_failed_invalid_apk" msgid="8581007676422623930">"পেকেজটো মান্য নোহোৱাৰ বাবে এপ্টো ইনষ্টল কৰা নহ\'ল।"</string>
<string name="install_failed_msg" product="tablet" msgid="6298387264270562442">"আপোনাৰ টে\'বলেটত <xliff:g id="APP_NAME">%1$s</xliff:g> ইনষ্টল কৰিব পৰা নগ\'ল৷"</string>
<string name="install_failed_msg" product="tv" msgid="1920009940048975221">"আপোনাৰ টিভিত <xliff:g id="APP_NAME">%1$s</xliff:g> ইনষ্টল কৰিব পৰা নগ\'ল।"</string>
<string name="install_failed_msg" product="default" msgid="6484461562647915707">"আপোনাৰ ফ\'নত <xliff:g id="APP_NAME">%1$s</xliff:g> ইনষ্টল কৰিব পৰা নগ\'ল৷"</string>
@@ -44,21 +44,21 @@
<string name="manage_applications" msgid="5400164782453975580">"এপ্ পৰিচালনা"</string>
<string name="out_of_space_dlg_title" msgid="4156690013884649502">"খালী ঠাই নাই"</string>
<string name="out_of_space_dlg_text" msgid="8727714096031856231">"<xliff:g id="APP_NAME">%1$s</xliff:g> ইনষ্টল কৰিব পৰা নগ\'ল। কিছু খালী ঠাই উলিয়াই আকৌ চেষ্টা কৰক৷"</string>
- <string name="app_not_found_dlg_title" msgid="5107924008597470285">"এপটো পোৱা নগ\'ল"</string>
- <string name="app_not_found_dlg_text" msgid="5219983779377811611">"ইনষ্টল কৰি ৰখা এপৰ তালিকাত এই এপটো পোৱা নগ\'ল।"</string>
+ <string name="app_not_found_dlg_title" msgid="5107924008597470285">"এপ্টো পোৱা নগ\'ল"</string>
+ <string name="app_not_found_dlg_text" msgid="5219983779377811611">"ইনষ্টল কৰি ৰখা এপৰ তালিকাত এই এপ্টো পোৱা নগ\'ল।"</string>
<string name="user_is_not_allowed_dlg_title" msgid="6915293433252210232">"অনুমতি নাই"</string>
<string name="user_is_not_allowed_dlg_text" msgid="3468447791330611681">"বর্তমানৰ ব্যৱহাৰকাৰীজনক এইটো আনইনষ্টল কৰিবলৈ অনুমতি দিয়া হোৱা নাই।"</string>
<string name="generic_error_dlg_title" msgid="5863195085927067752">"আসোঁৱাহ"</string>
<string name="generic_error_dlg_text" msgid="5287861443265795232">"এপ্ আনইনষ্টল কৰিব পৰা নগ\'ল।"</string>
<string name="uninstall_application_title" msgid="4045420072401428123">"এপ্ আনইনষ্টল কৰক"</string>
<string name="uninstall_update_title" msgid="824411791011583031">"আপডে’ট আনইনষ্টল কৰক"</string>
- <string name="uninstall_activity_text" msgid="1928194674397770771">"<xliff:g id="ACTIVITY_NAME">%1$s</xliff:g> হৈছে তলৰ এপটোৰ এটা অংশ:"</string>
+ <string name="uninstall_activity_text" msgid="1928194674397770771">"<xliff:g id="ACTIVITY_NAME">%1$s</xliff:g> হৈছে তলৰ এপ্টোৰ এটা অংশ:"</string>
<string name="uninstall_application_text" msgid="3816830743706143980">"আপুনি এই এপ্টো আনইনষ্টল কৰিব বিচাৰে নেকি?"</string>
- <string name="uninstall_application_text_all_users" msgid="575491774380227119">"আপুনি "<b>"সকলো"</b>" ব্যৱহাৰকাৰীৰ বাবে এই এপটো আনইনষ্টল কৰিব বিচাৰেনে? এপ্লিকেশ্বন আৰু ইয়াৰ ডেটা ডিভাইচটোত থকা "<b>"সকলো"</b>" ব্যৱহাৰকাৰীৰ পৰা আঁতৰোৱা হ\'ব৷"</string>
- <string name="uninstall_application_text_user" msgid="498072714173920526">"আপুনি ব্যৱহাৰকাৰীৰ <xliff:g id="USERNAME">%1$s</xliff:g> বাবে এই এপটো আনইনষ্টল কৰিব বিচাৰেনে?"</string>
+ <string name="uninstall_application_text_all_users" msgid="575491774380227119">"আপুনি "<b>"সকলো"</b>" ব্যৱহাৰকাৰীৰ বাবে এই এপ্টো আনইনষ্টল কৰিব বিচাৰেনে? এপ্লিকেশ্বন আৰু ইয়াৰ ডেটা ডিভাইচটোত থকা "<b>"সকলো"</b>" ব্যৱহাৰকাৰীৰ পৰা আঁতৰোৱা হ\'ব৷"</string>
+ <string name="uninstall_application_text_user" msgid="498072714173920526">"আপুনি ব্যৱহাৰকাৰীৰ <xliff:g id="USERNAME">%1$s</xliff:g> বাবে এই এপ্টো আনইনষ্টল কৰিব বিচাৰেনে?"</string>
<string name="uninstall_application_text_current_user_work_profile" msgid="8788387739022366193">"আপুনি নিজৰ কৰ্মস্থানৰ প্ৰ’ফাইলৰ পৰা এই এপ্টো আনইনষ্টল কৰিব বিচাৰেনে?"</string>
<string name="uninstall_update_text" msgid="863648314632448705">"এই এপ্টোৰ ফেক্টৰী সংস্কৰণ ব্যৱহাৰ কৰিব বিচাৰেনে? আটাইবোৰ ডেটা মচা হ\'ব।"</string>
- <string name="uninstall_update_text_multiuser" msgid="8992883151333057227">"এই এপটোৰ ফেক্টৰী সংস্কৰণ ব্যৱহাৰ কৰিব বিচাৰেনে? সকলো ডেটা মচা হ\'ব। কর্মস্থানৰ প্ৰফাইল থকা ব্যৱহাৰকাৰীৰ লগতে ডিভাইচটোৰ সকলো ব্যৱহাৰকাৰীৰ ওপৰত ইয়াৰ প্ৰভাৱ পৰিব।"</string>
+ <string name="uninstall_update_text_multiuser" msgid="8992883151333057227">"এই এপ্টোৰ ফেক্টৰী সংস্কৰণ ব্যৱহাৰ কৰিব বিচাৰেনে? সকলো ডেটা মচা হ\'ব। কর্মস্থানৰ প্ৰফাইল থকা ব্যৱহাৰকাৰীৰ লগতে ডিভাইচটোৰ সকলো ব্যৱহাৰকাৰীৰ ওপৰত ইয়াৰ প্ৰভাৱ পৰিব।"</string>
<string name="uninstall_keep_data" msgid="7002379587465487550">"এপৰ ডেটাৰ <xliff:g id="SIZE">%1$s</xliff:g> ৰাখক"</string>
<string name="uninstalling_notification_channel" msgid="840153394325714653">"আনইনষ্টল কৰি থকা হৈছে"</string>
<string name="uninstall_failure_notification_channel" msgid="1136405866767576588">"যিবোৰ আনইনষ্টল পৰা নগ\'ল"</string>
@@ -70,9 +70,9 @@
<string name="uninstall_failed_app" msgid="5506028705017601412">"<xliff:g id="PACKAGE_LABEL">%1$s</xliff:g> আনইনষ্টল কৰিব পৰা নগ\'ল।"</string>
<string name="uninstall_failed_device_policy_manager" msgid="785293813665540305">"ডিভাইচৰ সক্ৰিয় প্ৰশাসক এপ্ আনইনষ্টল কৰিব নোৱাৰি"</string>
<string name="uninstall_failed_device_policy_manager_of_user" msgid="4813104025494168064">"<xliff:g id="USERNAME">%1$s</xliff:g>ৰ সক্ৰিয় ডিভাইচৰ প্ৰশাসকীয় এপ্ আনইনষ্টল কৰিব নোৱাৰি"</string>
- <string name="uninstall_all_blocked_profile_owner" msgid="2009393666026751501">"এই এপটো কিছুসংখ্যক ব্যৱহাৰকাৰী বা প্ৰ\'ফাইলৰ বাবে প্ৰয়োজনীয় আৰু বাকীসকলৰ বাবে ইয়াক আনইনষ্টল কৰা হৈছে"</string>
- <string name="uninstall_blocked_profile_owner" msgid="6373897407002404848">"আপোনাৰ প্ৰ\'ফাইলৰ বাবে এই এপটোৰ প্ৰয়োজন আছে গতিকে আনইনষ্টল কৰিব পৰা নাযায়।"</string>
- <string name="uninstall_blocked_device_owner" msgid="6724602931761073901">"এই এপটো আনইনষ্টল কৰিব পৰা নাযায় কাৰণ আপোনাৰ ডিভাইচৰ প্ৰশাসকে এই এপ্ ৰখাটো বাধ্যতামূলক কৰি ৰাখিছে।"</string>
+ <string name="uninstall_all_blocked_profile_owner" msgid="2009393666026751501">"এই এপ্টো কিছুসংখ্যক ব্যৱহাৰকাৰী বা প্ৰ\'ফাইলৰ বাবে প্ৰয়োজনীয় আৰু বাকীসকলৰ বাবে ইয়াক আনইনষ্টল কৰা হৈছে"</string>
+ <string name="uninstall_blocked_profile_owner" msgid="6373897407002404848">"আপোনাৰ প্ৰ\'ফাইলৰ বাবে এই এপ্টোৰ প্ৰয়োজন আছে গতিকে আনইনষ্টল কৰিব পৰা নাযায়।"</string>
+ <string name="uninstall_blocked_device_owner" msgid="6724602931761073901">"এই এপ্টো আনইনষ্টল কৰিব পৰা নাযায় কাৰণ আপোনাৰ ডিভাইচৰ প্ৰশাসকে এই এপ্ ৰখাটো বাধ্যতামূলক কৰি ৰাখিছে।"</string>
<string name="manage_device_administrators" msgid="3092696419363842816">"ডিভাইচৰ প্ৰশাসক এপসমূহ পৰিচালনা কৰক"</string>
<string name="manage_users" msgid="1243995386982560813">"ব্যৱহাৰকাৰী পৰিচালনা কৰক"</string>
<string name="uninstall_failed_msg" msgid="2176744834786696012">"<xliff:g id="APP_NAME">%1$s</xliff:g> আনইনষ্টল কৰিব নোৱাৰি।"</string>
@@ -84,9 +84,9 @@
<string name="untrusted_external_source_warning" product="tablet" msgid="7067510047443133095">"আপোনাৰ সুৰক্ষাৰ বাবে আপোনাৰ টেবলেটটোক বৰ্তমান এই উৎসটোৰ পৰা অজ্ঞাত এপ্ ইনষ্টল কৰাৰ অনুমতি দিয়া হোৱা নাই। আপুনি এইটো ছেটিঙত সলনি কৰিব পাৰে।"</string>
<string name="untrusted_external_source_warning" product="tv" msgid="7057271609532508035">"আপোনাৰ সুৰক্ষাৰ বাবে আপোনাৰ টিভিটোক বৰ্তমান এই উৎসটোৰ পৰা অজ্ঞাত এপ্ ইনষ্টল কৰাৰ অনুমতি দিয়া হোৱা নাই। আপুনি এইটো ছেটিঙত সলনি কৰিব পাৰে।"</string>
<string name="untrusted_external_source_warning" product="default" msgid="8444191224459138919">"আপোনাৰ সুৰক্ষাৰ বাবে আপোনাৰ ফ’নটোক বৰ্তমান এই উৎসটোৰ পৰা অজ্ঞাত এপ্ ইনষ্টল কৰাৰ অনুমতি দিয়া হোৱা নাই। আপুনি এইটো ছেটিঙত সলনি কৰিব পাৰে।"</string>
- <string name="anonymous_source_warning" product="default" msgid="2784902545920822500">"আপোনাৰ ফ\'ন আৰু ব্যক্তিগত ডেটা অজ্ঞাত এপৰ আক্ৰমণৰ বলি হোৱাৰ সম্ভাৱনা অধিক। আপুনি এই এপটো ইনষ্টল কৰি এপটোৰ ব্যৱহাৰৰ ফলত আপোনাৰ টিভিত হ\'ব পৰা যিকোনো ক্ষতি বা ডেটা ক্ষয়ৰ বাবে আপুনি নিজে দায়ী হ\'ব বুলি সন্মতি দিয়ে।"</string>
- <string name="anonymous_source_warning" product="tablet" msgid="3939101621438855516">"আপোনাৰ টেবলেট আৰু ব্যক্তিগত ডেটা অজ্ঞাত এপৰ আক্ৰমণৰ বলি হোৱাৰ সম্ভাৱনা অধিক। আপুনি এই এপটো ইনষ্টল কৰি এপটোৰ ব্যৱহাৰৰ ফলত আপোনাৰ টিভিত হ\'ব পৰা যিকোনো ক্ষতি বা ডেটা ক্ষয়ৰ বাবে আপুনি নিজে দায়ী হ\'ব বুলি সন্মতি দিয়ে।"</string>
- <string name="anonymous_source_warning" product="tv" msgid="5599483539528168566">"আপোনাৰ টিভি আৰু ব্যক্তিগত ডেটা অজ্ঞাত এপৰ আক্ৰমণৰ বলি হোৱাৰ সম্ভাৱনা অধিক। আপুনি এই এপটো ইনষ্টল কৰি এপটোৰ ব্যৱহাৰৰ ফলত আপোনাৰ টিভিত হ\'ব পৰা যিকোনো ক্ষতি বা ডেটা ক্ষয়ৰ বাবে আপুনি নিজে দায়ী হ\'ব বুলি সন্মতি দিয়ে।"</string>
+ <string name="anonymous_source_warning" product="default" msgid="2784902545920822500">"আপোনাৰ ফ\'ন আৰু ব্যক্তিগত ডেটা অজ্ঞাত এপৰ আক্ৰমণৰ বলি হোৱাৰ সম্ভাৱনা অধিক। আপুনি এই এপ্টো ইনষ্টল কৰি এপ্টোৰ ব্যৱহাৰৰ ফলত আপোনাৰ টিভিত হ\'ব পৰা যিকোনো ক্ষতি বা ডেটা ক্ষয়ৰ বাবে আপুনি নিজে দায়ী হ\'ব বুলি সন্মতি দিয়ে।"</string>
+ <string name="anonymous_source_warning" product="tablet" msgid="3939101621438855516">"আপোনাৰ টেবলেট আৰু ব্যক্তিগত ডেটা অজ্ঞাত এপৰ আক্ৰমণৰ বলি হোৱাৰ সম্ভাৱনা অধিক। আপুনি এই এপ্টো ইনষ্টল কৰি এপ্টোৰ ব্যৱহাৰৰ ফলত আপোনাৰ টিভিত হ\'ব পৰা যিকোনো ক্ষতি বা ডেটা ক্ষয়ৰ বাবে আপুনি নিজে দায়ী হ\'ব বুলি সন্মতি দিয়ে।"</string>
+ <string name="anonymous_source_warning" product="tv" msgid="5599483539528168566">"আপোনাৰ টিভি আৰু ব্যক্তিগত ডেটা অজ্ঞাত এপৰ আক্ৰমণৰ বলি হোৱাৰ সম্ভাৱনা অধিক। আপুনি এই এপ্টো ইনষ্টল কৰি এপ্টোৰ ব্যৱহাৰৰ ফলত আপোনাৰ টিভিত হ\'ব পৰা যিকোনো ক্ষতি বা ডেটা ক্ষয়ৰ বাবে আপুনি নিজে দায়ী হ\'ব বুলি সন্মতি দিয়ে।"</string>
<string name="anonymous_source_continue" msgid="4375745439457209366">"অব্যাহত ৰাখক"</string>
<string name="external_sources_settings" msgid="4046964413071713807">"ছেটিং"</string>
<string name="wear_app_channel" msgid="1960809674709107850">"ৱেৰ এপসমূহ ইনষ্টল/আনইনষ্টল কৰি থকা হৈছে"</string>
diff --git a/packages/SettingsLib/res/values-am/strings.xml b/packages/SettingsLib/res/values-am/strings.xml
index 734eb33..6bb7595 100644
--- a/packages/SettingsLib/res/values-am/strings.xml
+++ b/packages/SettingsLib/res/values-am/strings.xml
@@ -209,7 +209,7 @@
<string name="tts_engine_settings_button" msgid="477155276199968948">"የፍርግም ቅንብሮችን ያስጀምሩ"</string>
<string name="tts_engine_preference_section_title" msgid="3861562305498624904">"የተመረጠ ፍርግም"</string>
<string name="tts_general_section_title" msgid="8919671529502364567">"አጠቃላይ"</string>
- <string name="tts_reset_speech_pitch_title" msgid="7149398585468413246">"የንግግር ድምጽ ውፍረት ዳግም አስጀምር"</string>
+ <string name="tts_reset_speech_pitch_title" msgid="7149398585468413246">"የንግግር ድምፅ ውፍረት ዳግም አስጀምር"</string>
<string name="tts_reset_speech_pitch_summary" msgid="6822904157021406449">"ጽሑፉ የሚነገርበትን የድምጽ ውፍረት ወደ ነባሪ ዳግም አስጀምር።"</string>
<string-array name="tts_rate_entries">
<item msgid="9004239613505400644">"በጣም ቀርፋፋ"</item>
@@ -407,7 +407,7 @@
<string name="force_resizable_activities" msgid="7143612144399959606">"እንቅስቃሴዎች ዳግመኛ እንዲመጣጠኑ አስገድድ"</string>
<string name="force_resizable_activities_summary" msgid="2490382056981583062">"የዝርዝር ሰነድ እሴቶች ምንም ይሁኑ ምን ለበርካታ መስኮቶች ሁሉንም እንቅስቃሴዎች መጠናቸው የሚቀየሩ እንዲሆኑ ያደርጋቸዋል።"</string>
<string name="enable_freeform_support" msgid="7599125687603914253">"የነጻ ቅርጽ መስኮቶችን ያንቁ"</string>
- <string name="enable_freeform_support_summary" msgid="1822862728719276331">"የሙከራ ነጻ መልክ መስኮቶች ድጋፍን አንቃ"</string>
+ <string name="enable_freeform_support_summary" msgid="1822862728719276331">"የሙከራ ነፃ መልክ መስኮቶች ድጋፍን አንቃ"</string>
<string name="desktop_mode" msgid="2389067840550544462">"የዴስክቶፕ ሁነታ"</string>
<string name="local_backup_password_title" msgid="4631017948933578709">"የዴስክቶፕ መጠባበቂያ ይለፍ ቃል"</string>
<string name="local_backup_password_summary_none" msgid="7646898032616361714">"ዴስክቶፕ ሙሉ ምትኬዎች በአሁኑ ሰዓት አልተጠበቁም"</string>
diff --git a/packages/SettingsLib/res/values-as/strings.xml b/packages/SettingsLib/res/values-as/strings.xml
index c4e9dd4..976949c 100644
--- a/packages/SettingsLib/res/values-as/strings.xml
+++ b/packages/SettingsLib/res/values-as/strings.xml
@@ -268,7 +268,7 @@
<string name="keep_screen_on" msgid="1187161672348797558">"জাগ্ৰত কৰি ৰাখক"</string>
<string name="keep_screen_on_summary" msgid="1510731514101925829">"চ্চাৰ্জ হৈ থকাৰ সময়ত স্ক্ৰীন কেতিয়াও সুপ্ত অৱস্থালৈ নাযায়"</string>
<string name="bt_hci_snoop_log" msgid="7291287955649081448">"ব্লুটুথ HCI স্নুপ ল’গ সক্ষম কৰক"</string>
- <string name="bt_hci_snoop_log_summary" msgid="6808538971394092284">"ব্লুটুথ পেকেট সংগ্ৰহ কৰক। (এই ছেটিংটো সলনি কৰাৰ পিছত ব্লুটুথ ট’গল কৰক)"</string>
+ <string name="bt_hci_snoop_log_summary" msgid="6808538971394092284">"ব্লুটুথ পেকেট সংগ্ৰহ কৰক। (এই ছেটিংটো সলনি কৰাৰ পাছত ব্লুটুথ ট’গল কৰক)"</string>
<string name="oem_unlock_enable" msgid="5334869171871566731">"ঔইএম আনলক"</string>
<string name="oem_unlock_enable_summary" msgid="5857388174390953829">"বুটল\'ডাৰটো আনলক কৰিবলৈ অনুমতি দিয়ক"</string>
<string name="confirm_enable_oem_unlock_title" msgid="8249318129774367535">"ঔইএম আনলক কৰাৰ অনুমতি দিবনে?"</string>
@@ -515,8 +515,8 @@
<string name="active_input_method_subtypes" msgid="4232680535471633046">"সক্ৰিয়হৈ থকা ইনপুট পদ্ধতিসমূহ"</string>
<string name="use_system_language_to_select_input_method_subtypes" msgid="4865195835541387040">"ছিষ্টেমৰ ভাষা ব্যৱহাৰ কৰক"</string>
<string name="failed_to_open_app_settings_toast" msgid="764897252657692092">"<xliff:g id="SPELL_APPLICATION_NAME">%1$s</xliff:g>ৰ ছেটিং খুলিব পৰা নগ\'ল"</string>
- <string name="ime_security_warning" msgid="6547562217880551450">"এই ইনপুট পদ্ধতিটোৱে আপুনি টাইপ কৰা আপোনাৰ ব্যক্তিগত ডেটা যেনে পাছৱৰ্ডসমূহ আৰু ক্ৰেডিট কাৰ্ডৰ নম্বৰসমূহকে ধৰি আটাইবোৰ পাঠ সংগ্ৰহ কৰিবলৈ সক্ষম হ\'ব পাৰে। <xliff:g id="IME_APPLICATION_NAME">%1$s</xliff:g> এপটোৰ লগত ই সংলগ্ন। এই ইনপুট পদ্ধতিটো ব্যৱহাৰ কৰেনে?"</string>
- <string name="direct_boot_unaware_dialog_message" msgid="7845398276735021548">"টোকা: ৰিবুট কৰাৰ পিছত আপুনি ফ\'নটো আনলক নকৰালৈকে এই এপটো ষ্টাৰ্ট নহ’ব"</string>
+ <string name="ime_security_warning" msgid="6547562217880551450">"এই ইনপুট পদ্ধতিটোৱে আপুনি টাইপ কৰা আপোনাৰ ব্যক্তিগত ডেটা যেনে পাছৱৰ্ডসমূহ আৰু ক্ৰেডিট কাৰ্ডৰ নম্বৰসমূহকে ধৰি আটাইবোৰ পাঠ সংগ্ৰহ কৰিবলৈ সক্ষম হ\'ব পাৰে। <xliff:g id="IME_APPLICATION_NAME">%1$s</xliff:g> এপ্টোৰ লগত ই সংলগ্ন। এই ইনপুট পদ্ধতিটো ব্যৱহাৰ কৰেনে?"</string>
+ <string name="direct_boot_unaware_dialog_message" msgid="7845398276735021548">"টোকা: ৰিবুট কৰাৰ পাছত আপুনি ফ\'নটো আনলক নকৰালৈকে এই এপ্টো ষ্টাৰ্ট নহ’ব"</string>
<string name="ims_reg_title" msgid="8197592958123671062">"আইএমএছ পঞ্জীয়ন স্থিতি"</string>
<string name="ims_reg_status_registered" msgid="884916398194885457">"পঞ্জীকৃত"</string>
<string name="ims_reg_status_not_registered" msgid="2989287366045704694">"পঞ্জীকৃত নহয়"</string>
diff --git a/packages/SettingsLib/res/values-fr/strings.xml b/packages/SettingsLib/res/values-fr/strings.xml
index 98cc18e..b6eb82e 100644
--- a/packages/SettingsLib/res/values-fr/strings.xml
+++ b/packages/SettingsLib/res/values-fr/strings.xml
@@ -247,7 +247,7 @@
<string name="adb_paired_devices_title" msgid="5268997341526217362">"Appareils associés"</string>
<string name="adb_wireless_device_connected_summary" msgid="3039660790249148713">"Actuellement connecté"</string>
<string name="adb_wireless_device_details_title" msgid="7129369670526565786">"Infos sur l\'appareil"</string>
- <string name="adb_device_forget" msgid="193072400783068417">"Supprimer"</string>
+ <string name="adb_device_forget" msgid="193072400783068417">"Retirer"</string>
<string name="adb_device_fingerprint_title_format" msgid="291504822917843701">"Empreinte de l\'appareil : <xliff:g id="FINGERPRINT_PARAM">%1$s</xliff:g>"</string>
<string name="adb_wireless_connection_failed_title" msgid="664211177427438438">"Échec de la connexion"</string>
<string name="adb_wireless_connection_failed_message" msgid="9213896700171602073">"Vérifiez que l\'appareil <xliff:g id="DEVICE_NAME">%1$s</xliff:g> est connecté au bon réseau"</string>
diff --git a/packages/SettingsLib/res/values-te/strings.xml b/packages/SettingsLib/res/values-te/strings.xml
index 5d5ebe7..c17a1d5 100644
--- a/packages/SettingsLib/res/values-te/strings.xml
+++ b/packages/SettingsLib/res/values-te/strings.xml
@@ -509,7 +509,7 @@
<string name="screen_zoom_summary_extremely_large" msgid="1438045624562358554">"అతి పెద్దగా"</string>
<string name="screen_zoom_summary_custom" msgid="3468154096832912210">"అనుకూలం (<xliff:g id="DENSITYDPI">%d</xliff:g>)"</string>
<string name="content_description_menu_button" msgid="6254844309171779931">"మెనూ"</string>
- <string name="retail_demo_reset_message" msgid="5392824901108195463">"డెమో మోడ్లో ఫ్యాక్టరీ రీసెట్ను నిర్వహించడానికి పాస్వర్డ్ను నమోదు చేయండి"</string>
+ <string name="retail_demo_reset_message" msgid="5392824901108195463">"డెమో మోడ్లో ఫ్యాక్టరీ రీసెట్ను మేనేజ్ చేయడానికి పాస్వర్డ్ను నమోదు చేయండి"</string>
<string name="retail_demo_reset_next" msgid="3688129033843885362">"తర్వాత"</string>
<string name="retail_demo_reset_title" msgid="1866911701095959800">"పాస్వర్డ్ అవసరం"</string>
<string name="active_input_method_subtypes" msgid="4232680535471633046">"సక్రియ ఇన్పుట్ పద్ధతులు"</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 5662ce6..6bc1160 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -356,7 +356,7 @@
* @return {@code true}, if the device should pair automatically; Otherwise, return
* {@code false}.
*/
- public synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) {
+ private synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) {
boolean isOngoingSetMemberPair = mOngoingSetMemberPair != null;
int bondState = device.getBondState();
if (isOngoingSetMemberPair || bondState != BluetoothDevice.BOND_NONE
@@ -365,13 +365,47 @@
+ " , device.getBondState: " + bondState);
return false;
}
-
- Log.d(TAG, "Bond " + device.getName() + " by CSIP");
- mOngoingSetMemberPair = device;
return true;
}
/**
+ * Called when we found a set member of a group. The function will check the {@code groupId} if
+ * it exists and the bond state of the device is BOND_NONE, and if there isn't any ongoing pair
+ * , and then pair the device automatically.
+ *
+ * @param device The found device
+ * @param groupId The group id of the found device
+ */
+ public synchronized void pairDeviceByCsip(BluetoothDevice device, int groupId) {
+ if (!shouldPairByCsip(device, groupId)) {
+ return;
+ }
+ Log.d(TAG, "Bond " + device.getAnonymizedAddress() + " by CSIP");
+ mOngoingSetMemberPair = device;
+ syncConfigFromMainDevice(device, groupId);
+ device.createBond(BluetoothDevice.TRANSPORT_LE);
+ }
+
+ private void syncConfigFromMainDevice(BluetoothDevice device, int groupId) {
+ if (!isOngoingPairByCsip(device)) {
+ return;
+ }
+ CachedBluetoothDevice memberDevice = findDevice(device);
+ CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(memberDevice);
+ if (mainDevice == null) {
+ mainDevice = mCsipDeviceManager.getCachedDevice(groupId);
+ }
+
+ if (mainDevice == null || mainDevice.equals(memberDevice)) {
+ Log.d(TAG, "no mainDevice");
+ return;
+ }
+
+ // The memberDevice set PhonebookAccessPermission
+ device.setPhonebookAccessPermission(mainDevice.getDevice().getPhonebookAccessPermission());
+ }
+
+ /**
* Called when the bond state change. If the bond state change is related with the
* ongoing set member pair, the cachedBluetoothDevice will be created but the UI
* would not be updated. For the other case, return {@code false} to go through the normal
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index d5de3f0..20a6cd8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -101,7 +101,14 @@
return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
- private CachedBluetoothDevice getCachedDevice(int groupId) {
+ /**
+ * To find the device with {@code groupId}.
+ *
+ * @param groupId The group id
+ * @return if we could find a device with this {@code groupId} return this device. Otherwise,
+ * return null.
+ */
+ public CachedBluetoothDevice getCachedDevice(int groupId) {
log("getCachedDevice: groupId: " + groupId);
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java
index 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/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java
index 62552f91..61802a8 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java
@@ -582,4 +582,24 @@
assertThat(mCachedDeviceManager.isSubDevice(mDevice2)).isTrue();
assertThat(mCachedDeviceManager.isSubDevice(mDevice3)).isFalse();
}
+
+ @Test
+ public void pairDeviceByCsip_device2AndCapGroup1_device2StartsPairing() {
+ doReturn(CAP_GROUP1).when(mCsipSetCoordinatorProfile).getGroupUuidMapByDevice(mDevice1);
+ when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+ when(mDevice1.getPhonebookAccessPermission()).thenReturn(BluetoothDevice.ACCESS_ALLOWED);
+ CachedBluetoothDevice cachedDevice1 = mCachedDeviceManager.addDevice(mDevice1);
+ assertThat(cachedDevice1).isNotNull();
+ when(mDevice2.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
+ CachedBluetoothDevice cachedDevice2 = mCachedDeviceManager.addDevice(mDevice2);
+ assertThat(cachedDevice2).isNotNull();
+
+ int groupId = CAP_GROUP1.keySet().stream().findFirst().orElse(
+ BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+ assertThat(groupId).isNotEqualTo(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+ mCachedDeviceManager.pairDeviceByCsip(mDevice2, groupId);
+
+ verify(mDevice2).setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
+ verify(mDevice2).createBond(BluetoothDevice.TRANSPORT_LE);
+ }
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 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/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index fd7554f..528af2e 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -376,9 +376,11 @@
Setting newSetting = new Setting(name, oldSetting.getValue(), null,
oldSetting.getPackageName(), oldSetting.getTag(), false,
oldSetting.getId());
- mSettings.put(name, newSetting);
- updateMemoryUsagePerPackageLocked(newSetting.getPackageName(), oldValue,
+ int newSize = getNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), oldValue,
newSetting.getValue(), oldDefaultValue, newSetting.getDefaultValue());
+ checkNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), newSize);
+ mSettings.put(name, newSetting);
+ updateMemoryUsagePerPackageLocked(newSetting.getPackageName(), newSize);
scheduleWriteIfNeededLocked();
}
}
@@ -410,6 +412,12 @@
Setting oldState = mSettings.get(name);
String oldValue = (oldState != null) ? oldState.value : null;
String oldDefaultValue = (oldState != null) ? oldState.defaultValue : null;
+ String newDefaultValue = makeDefault ? value : oldDefaultValue;
+
+ int newSize = getNewMemoryUsagePerPackageLocked(packageName, oldValue, value,
+ oldDefaultValue, newDefaultValue);
+ checkNewMemoryUsagePerPackageLocked(packageName, newSize);
+
Setting newState;
if (oldState != null) {
@@ -430,8 +438,7 @@
addHistoricalOperationLocked(HISTORICAL_OPERATION_UPDATE, newState);
- updateMemoryUsagePerPackageLocked(packageName, oldValue, value,
- oldDefaultValue, newState.getDefaultValue());
+ updateMemoryUsagePerPackageLocked(packageName, newSize);
scheduleWriteIfNeededLocked();
@@ -552,13 +559,14 @@
}
Setting oldState = mSettings.remove(name);
+ int newSize = getNewMemoryUsagePerPackageLocked(oldState.packageName, oldState.value,
+ null, oldState.defaultValue, null);
FrameworkStatsLog.write(FrameworkStatsLog.SETTING_CHANGED, name, /* value= */ "",
/* newValue= */ "", oldState.value, /* tag */ "", false, getUserIdFromKey(mKey),
FrameworkStatsLog.SETTING_CHANGED__REASON__DELETED);
- updateMemoryUsagePerPackageLocked(oldState.packageName, oldState.value,
- null, oldState.defaultValue, null);
+ updateMemoryUsagePerPackageLocked(oldState.packageName, newSize);
addHistoricalOperationLocked(HISTORICAL_OPERATION_DELETE, oldState);
@@ -579,16 +587,18 @@
Setting oldSetting = new Setting(setting);
String oldValue = setting.getValue();
String oldDefaultValue = setting.getDefaultValue();
+ String newValue = oldDefaultValue;
+ String newDefaultValue = oldDefaultValue;
+
+ int newSize = getNewMemoryUsagePerPackageLocked(setting.packageName, oldValue,
+ newValue, oldDefaultValue, newDefaultValue);
+ checkNewMemoryUsagePerPackageLocked(setting.packageName, newSize);
if (!setting.reset()) {
return false;
}
- String newValue = setting.getValue();
- String newDefaultValue = setting.getDefaultValue();
-
- updateMemoryUsagePerPackageLocked(setting.packageName, oldValue,
- newValue, oldDefaultValue, newDefaultValue);
+ updateMemoryUsagePerPackageLocked(setting.packageName, newSize);
addHistoricalOperationLocked(HISTORICAL_OPERATION_RESET, oldSetting);
@@ -696,38 +706,49 @@
}
@GuardedBy("mLock")
- private void updateMemoryUsagePerPackageLocked(String packageName, String oldValue,
+ private boolean isExemptFromMemoryUsageCap(String packageName) {
+ return mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED
+ || SYSTEM_PACKAGE_NAME.equals(packageName);
+ }
+
+ @GuardedBy("mLock")
+ private void checkNewMemoryUsagePerPackageLocked(String packageName, int newSize)
+ throws IllegalStateException {
+ if (isExemptFromMemoryUsageCap(packageName)) {
+ return;
+ }
+ if (newSize > mMaxBytesPerAppPackage) {
+ throw new IllegalStateException("You are adding too many system settings. "
+ + "You should stop using system settings for app specific data"
+ + " package: " + packageName);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private int getNewMemoryUsagePerPackageLocked(String packageName, String oldValue,
String newValue, String oldDefaultValue, String newDefaultValue) {
- if (mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED) {
- return;
+ if (isExemptFromMemoryUsageCap(packageName)) {
+ return 0;
}
-
- if (SYSTEM_PACKAGE_NAME.equals(packageName)) {
- return;
- }
-
+ final Integer currentSize = mPackageToMemoryUsage.get(packageName);
final int oldValueSize = (oldValue != null) ? oldValue.length() : 0;
final int newValueSize = (newValue != null) ? newValue.length() : 0;
final int oldDefaultValueSize = (oldDefaultValue != null) ? oldDefaultValue.length() : 0;
final int newDefaultValueSize = (newDefaultValue != null) ? newDefaultValue.length() : 0;
final int deltaSize = newValueSize + newDefaultValueSize
- oldValueSize - oldDefaultValueSize;
+ return Math.max((currentSize != null) ? currentSize + deltaSize : deltaSize, 0);
+ }
- Integer currentSize = mPackageToMemoryUsage.get(packageName);
- final int newSize = Math.max((currentSize != null)
- ? currentSize + deltaSize : deltaSize, 0);
-
- if (newSize > mMaxBytesPerAppPackage) {
- throw new IllegalStateException("You are adding too many system settings. "
- + "You should stop using system settings for app specific data"
- + " package: " + packageName);
+ @GuardedBy("mLock")
+ private void updateMemoryUsagePerPackageLocked(String packageName, int newSize) {
+ if (isExemptFromMemoryUsageCap(packageName)) {
+ return;
}
-
if (DEBUG) {
Slog.i(LOG_TAG, "Settings for package: " + packageName
+ " size: " + newSize + " bytes.");
}
-
mPackageToMemoryUsage.put(packageName, newSize);
}
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index 69eb713..66b809a 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -20,6 +20,8 @@
import android.util.TypedXmlSerializer;
import android.util.Xml;
+import com.google.common.base.Strings;
+
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
@@ -276,4 +278,40 @@
settingsState.setVersionLocked(SettingsState.SETTINGS_VERSION_NEW_ENCODING);
return settingsState;
}
+
+ public void testInsertSetting_memoryUsage() {
+ SettingsState settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1,
+ SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
+ // No exception should be thrown when there is no cap
+ settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001),
+ null, false, "p1");
+ settingsState.deleteSettingLocked(SETTING_NAME);
+
+ settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1,
+ SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper());
+ // System package doesn't have memory usage limit
+ settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001),
+ null, false, SYSTEM_PACKAGE);
+ settingsState.deleteSettingLocked(SETTING_NAME);
+
+ // Should not throw if usage is under the cap
+ settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 19999),
+ null, false, "p1");
+ settingsState.deleteSettingLocked(SETTING_NAME);
+ try {
+ settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001),
+ null, false, "p1");
+ fail("Should throw because it exceeded per package memory usage");
+ } catch (IllegalStateException ex) {
+ assertTrue(ex.getMessage().contains("p1"));
+ }
+ try {
+ settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001),
+ null, false, "p1");
+ fail("Should throw because it exceeded per package memory usage");
+ } catch (IllegalStateException ex) {
+ assertTrue(ex.getMessage().contains("p1"));
+ }
+ assertTrue(settingsState.getSettingLocked(SETTING_NAME).isNull());
+ }
}
diff --git a/packages/SoundPicker/res/values-am/strings.xml b/packages/SoundPicker/res/values-am/strings.xml
index 07aee8a..85206c0 100644
--- a/packages/SoundPicker/res/values-am/strings.xml
+++ b/packages/SoundPicker/res/values-am/strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ringtone_default" msgid="798836092118824500">"ነባሪ የስልክ ላይ ጥሪ"</string>
- <string name="notification_sound_default" msgid="8133121186242636840">"ነባሪ የማሳወቂያ ድምጽ"</string>
+ <string name="notification_sound_default" msgid="8133121186242636840">"ነባሪ የማሳወቂያ ድምፅ"</string>
<string name="alarm_sound_default" msgid="4787646764557462649">"ነባሪ የማንቂያ ድምፅ"</string>
<string name="add_ringtone_text" msgid="6642389991738337529">"የጥሪ ቅላጼ አክል"</string>
<string name="add_alarm_text" msgid="3545497316166999225">"የማንቂያ ደውል አክል"</string>
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 2737ecf..b5145f9 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -402,6 +402,9 @@
android:permission="com.android.systemui.permission.SELF"
android:exported="false" />
+ <service android:name=".screenshot.ScreenshotCrossProfileService"
+ android:permission="com.android.systemui.permission.SELF"
+ android:exported="false" />
<service android:name=".screenrecord.RecordingService" />
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index a65f9be..6d61fd8 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -5,23 +5,32 @@
dsandler@android.com
aaliomer@google.com
+aaronjli@google.com
+acul@google.com
adamcohen@google.com
+aioana@google.com
alexflo@google.com
+andonian@google.com
+aroederer@google.com
arteiro@google.com
asc@google.com
awickham@google.com
+ayepin@google.com
+bbade@google.com
beverlyt@google.com
-brockman@google.com
-brzezinski@google.com
+bhinegardner@google.com
+bhnm@google.com
brycelee@google.com
+brzezinski@google.com
caitlinshk@google.com
+chandruis@google.com
chrisgollner@google.com
cinek@google.com
-cwren@google.com
dupin@google.com
ethibodeau@google.com
evanlaird@google.com
florenceyang@google.com
+gallmann@google.com
gwasserman@google.com
hwwang@google.com
hyunyoungs@google.com
@@ -37,35 +46,42 @@
joshtrask@google.com
juliacr@google.com
juliatuttle@google.com
-kchyn@google.com
+justinkoh@google.com
+justinweir@google.com
kozynski@google.com
kprevas@google.com
+lusilva@google.com
lynhan@google.com
madym@google.com
mankoff@google.com
-mett@google.com
+mateuszc@google.com
+michaelmikhil@google.com
+michschn@google.com
mkephart@google.com
mpietal@google.com
mrcasey@google.com
mrenouf@google.com
-nesciosquid@google.com
nickchameyev@google.com
nicomazz@google.com
+nijamkin@google.com
ogunwale@google.com
+omarmt@google.com
+patmanning@google.com
peanutbutter@google.com
peskal@google.com
pinyaoting@google.com
pixel@google.com
pomini@google.com
rahulbanerjee@google.com
+rasheedlewis@google.com
roosa@google.com
+saff@google.com
santie@google.com
shanh@google.com
snoeberger@google.com
-sreyasr@google.com
steell@google.com
-sfufa@google.com
stwu@google.com
+syeonlee@google.com
sunnygoyal@google.com
susikp@google.com
thiruram@google.com
@@ -75,13 +91,14 @@
vadimt@google.com
victortulias@google.com
winsonc@google.com
+wleshner@google.com
+xilei@google.com
xuqiu@google.com
+yeinj@google.com
yuandizhou@google.com
yurilin@google.com
zakcohen@google.com
-
-#Android Auto
-hseog@google.com
+zoepage@google.com
#Android TV
rgl@google.com
diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/res/values/ids.xml
index f7150ab..2d82307a 100644
--- a/packages/SystemUI/animation/res/values/ids.xml
+++ b/packages/SystemUI/animation/res/values/ids.xml
@@ -16,7 +16,6 @@
-->
<resources>
<!-- DialogLaunchAnimator -->
- <item type="id" name="tag_launch_animation_running"/>
<item type="id" name="tag_dialog_background"/>
<!-- ViewBoundsAnimator -->
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 9656b8a..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,16 +25,15 @@
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
+import android.view.ViewRootImpl
import android.view.WindowInsets
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
import android.widget.FrameLayout
import com.android.internal.jank.InteractionJankMonitor
-import com.android.internal.jank.InteractionJankMonitor.Configuration
import com.android.internal.jank.InteractionJankMonitor.CujType
import kotlin.math.roundToInt
@@ -46,6 +45,7 @@
*
* This animator also allows to easily animate a dialog into an activity.
*
+ * @see show
* @see showFromView
* @see showFromDialog
* @see createActivityLaunchController
@@ -67,8 +67,108 @@
ActivityLaunchAnimator.INTERPOLATORS.copy(
positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator
)
+ }
- private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.tag_launch_animation_running
+ /**
+ * A controller that takes care of applying the dialog launch and exit animations to the source
+ * that triggered the animation.
+ */
+ interface Controller {
+ /** The [ViewRootImpl] of this controller. */
+ val viewRoot: ViewRootImpl
+
+ /**
+ * The identity object of the source animated by this controller. This animator will ensure
+ * that 2 animations with the same source identity are not going to run at the same time, to
+ * avoid flickers when a dialog is shown from the same source more or less at the same time
+ * (for instance if the user clicks an expandable button twice).
+ */
+ val sourceIdentity: Any
+
+ /** The CUJ associated to this controller. */
+ val cuj: DialogCuj?
+
+ /**
+ * Move the drawing of the source in the overlay of [viewGroup].
+ *
+ * Once this method is called, and until [stopDrawingInOverlay] is called, the source
+ * controlled by this Controller should be drawn in the overlay of [viewGroup] so that it is
+ * drawn above all other elements in the same [viewRoot].
+ */
+ fun startDrawingInOverlayOf(viewGroup: ViewGroup)
+
+ /**
+ * Move the drawing of the source back in its original location.
+ *
+ * @see startDrawingInOverlayOf
+ */
+ fun stopDrawingInOverlay()
+
+ /**
+ * Create the [LaunchAnimator.Controller] that will be called to animate the source
+ * controlled by this [Controller] during the dialog launch animation.
+ *
+ * At the end of this animation, the source should *not* be visible anymore (until the
+ * dialog is closed and is animated back into the source).
+ */
+ fun createLaunchController(): LaunchAnimator.Controller
+
+ /**
+ * Create the [LaunchAnimator.Controller] that will be called to animate the source
+ * controlled by this [Controller] during the dialog exit animation.
+ *
+ * At the end of this animation, the source should be visible again.
+ */
+ fun createExitController(): LaunchAnimator.Controller
+
+ /**
+ * Whether we should animate the dialog back into the source when it is dismissed. If this
+ * methods returns `false`, then the dialog will simply fade out and
+ * [onExitAnimationCancelled] will be called.
+ *
+ * Note that even when this returns `true`, the exit animation might still be cancelled (in
+ * which case [onExitAnimationCancelled] will also be called).
+ */
+ fun shouldAnimateExit(): Boolean
+
+ /**
+ * Called if we decided to *not* animate the dialog into the source for some reason. This
+ * means that [createExitController] will *not* be called and this implementation should
+ * make sure that the source is back in its original state, before it was animated into the
+ * dialog. In particular, the source should be visible again.
+ */
+ fun onExitAnimationCancelled()
+
+ /**
+ * Return the [InteractionJankMonitor.Configuration.Builder] to be used for animations
+ * controlled by this controller.
+ */
+ // TODO(b/252723237): Make this non-nullable
+ 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)
+ }
+ }
}
/**
@@ -96,7 +196,33 @@
dialog: Dialog,
view: View,
cuj: DialogCuj? = null,
- animateBackgroundBoundsChange: Boolean = false,
+ animateBackgroundBoundsChange: Boolean = false
+ ) {
+ val controller = Controller.fromView(view, cuj)
+ if (controller == null) {
+ dialog.show()
+ } else {
+ show(dialog, controller, animateBackgroundBoundsChange)
+ }
+ }
+
+ /**
+ * Show [dialog] by expanding it from a source controlled by [controller].
+ *
+ * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be
+ * animated when the dialog bounds change.
+ *
+ * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
+ * animated.
+ *
+ * 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,
+ animateBackgroundBoundsChange: Boolean = false
) {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw IllegalStateException(
@@ -109,9 +235,13 @@
// intent is to launch a dialog from another dialog.
val animatedParent =
openedDialogs.firstOrNull {
- it.dialog.window.decorView.viewRootImpl == view.viewRootImpl
+ it.dialog.window.decorView.viewRootImpl == controller.viewRoot
}
- val animateFrom = animatedParent?.dialogContentWithBackground ?: view
+ val animateFrom =
+ 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
@@ -126,15 +256,17 @@
)
}
- // Make sure we don't run the launch animation from the same view twice at the same time.
- if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) {
- Log.e(TAG, "Not running dialog launch animation as there is already one running")
+ // Make sure we don't run the launch animation from the same source twice at the same time.
+ if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) {
+ Log.e(
+ TAG,
+ "Not running dialog launch animation from source as it is already expanded into a" +
+ " dialog"
+ )
dialog.show()
return
}
- animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
-
val animatedDialog =
AnimatedDialog(
launchAnimator,
@@ -146,7 +278,6 @@
animateBackgroundBoundsChange,
animatedParent,
isForTesting,
- cuj
)
openedDialogs.add(animatedDialog)
@@ -154,8 +285,8 @@
}
/**
- * Launch [dialog] from [another dialog][animateFrom] that was shown using [showFromView]. This
- * will allow for dismissing the whole stack.
+ * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will
+ * allow for dismissing the whole stack.
*
* @see dismissStack
*/
@@ -181,32 +312,55 @@
/**
* Create an [ActivityLaunchAnimator.Controller] that can be used to launch an activity from the
- * dialog that contains [View]. Note that the dialog must have been show using [showFromView]
- * and be currently showing, otherwise this will return null.
+ * dialog that contains [View]. Note that the dialog must have been shown using this animator,
+ * otherwise this method will return null.
*
* The returned controller will take care of dismissing the dialog at the right time after the
* activity started, when the dialog to app animation is done (or when it is cancelled). If this
* method returns null, then the dialog won't be dismissed.
*
- * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
- * animated.
- *
* @param view any view inside the dialog to animate.
*/
@JvmOverloads
fun createActivityLaunchController(
view: View,
- cujType: Int? = null
+ cujType: Int? = null,
): ActivityLaunchAnimator.Controller? {
val animatedDialog =
openedDialogs.firstOrNull {
it.dialog.window.decorView.viewRootImpl == view.viewRootImpl
}
?: return null
+ return createActivityLaunchController(animatedDialog, cujType)
+ }
+ /**
+ * Create an [ActivityLaunchAnimator.Controller] that can be used to launch an activity from
+ * [dialog]. Note that the dialog must have been shown using this animator, otherwise this
+ * method will return null.
+ *
+ * The returned controller will take care of dismissing the dialog at the right time after the
+ * activity started, when the dialog to app animation is done (or when it is cancelled). If this
+ * method returns null, then the dialog won't be dismissed.
+ *
+ * @param dialog the dialog to animate.
+ */
+ @JvmOverloads
+ fun createActivityLaunchController(
+ dialog: Dialog,
+ cujType: Int? = null,
+ ): ActivityLaunchAnimator.Controller? {
+ val animatedDialog = openedDialogs.firstOrNull { it.dialog == dialog } ?: return null
+ return createActivityLaunchController(animatedDialog, cujType)
+ }
+
+ private fun createActivityLaunchController(
+ animatedDialog: AnimatedDialog,
+ cujType: Int? = null
+ ): ActivityLaunchAnimator.Controller? {
// At this point, we know that the intent of the caller is to dismiss the dialog to show
- // an app, so we disable the exit animation into the touch surface because we will never
- // want to run it anyways.
+ // an app, so we disable the exit animation into the source because we will never want to
+ // run it anyways.
animatedDialog.exitAnimationDisabled = true
val dialog = animatedDialog.dialog
@@ -252,7 +406,7 @@
// If this dialog was shown from a cascade of other dialogs, make sure those ones
// are dismissed too.
- animatedDialog.touchSurface = animatedDialog.prepareForStackDismiss()
+ animatedDialog.prepareForStackDismiss()
// Remove the dim.
dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
@@ -283,12 +437,11 @@
}
/**
- * Ensure that all dialogs currently shown won't animate into their touch surface when
- * dismissed.
+ * Ensure that all dialogs currently shown won't animate into their source when dismissed.
*
* This is a temporary API meant to be called right before we both dismiss a dialog and start an
- * activity, which currently does not look good if we animate the dialog into the touch surface
- * at the same time as the activity starts.
+ * activity, which currently does not look good if we animate the dialog into their source at
+ * the same time as the activity starts.
*
* TODO(b/193634619): Remove this function and animate dialog into opening activity instead.
*/
@@ -297,13 +450,11 @@
}
/**
- * Dismiss [dialog]. If it was launched from another dialog using [showFromView], also dismiss
- * the stack of dialogs, animating back to the original touchSurface.
+ * Dismiss [dialog]. If it was launched from another dialog using this animator, also dismiss
+ * the stack of dialogs and simply fade out [dialog].
*/
fun dismissStack(dialog: Dialog) {
- openedDialogs
- .firstOrNull { it.dialog == dialog }
- ?.let { it.touchSurface = it.prepareForStackDismiss() }
+ openedDialogs.firstOrNull { it.dialog == dialog }?.prepareForStackDismiss()
dialog.dismiss()
}
@@ -337,8 +488,11 @@
private val callback: DialogLaunchAnimator.Callback,
private val interactionJankMonitor: InteractionJankMonitor,
- /** The view that triggered the dialog after being tapped. */
- var touchSurface: View,
+ /**
+ * The controller of the source that triggered the dialog and that will animate into/from the
+ * dialog.
+ */
+ val controller: DialogLaunchAnimator.Controller,
/**
* A callback that will be called with this [AnimatedDialog] after the dialog was dismissed and
@@ -359,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.
@@ -383,17 +534,18 @@
private var originalDialogBackgroundColor = Color.BLACK
/**
- * Whether we are currently launching/showing the dialog by animating it from [touchSurface].
+ * Whether we are currently launching/showing the dialog by animating it from its source
+ * controlled by [controller].
*/
private var isLaunching = true
- /** Whether we are currently dismissing/hiding the dialog by animating into [touchSurface]. */
+ /** Whether we are currently dismissing/hiding the dialog by animating into its source. */
private var isDismissing = false
private var dismissRequested = false
var exitAnimationDisabled = false
- private var isTouchSurfaceGhostDrawn = false
+ private var isSourceDrawnInDialog = false
private var isOriginalDialogViewLaidOut = false
/** A layout listener to animate the dialog height change. */
@@ -410,13 +562,20 @@
*/
private var decorViewLayoutListener: View.OnLayoutChangeListener? = null
+ private var hasInstrumentedJank = false
+
fun start() {
+ val cuj = controller.cuj
if (cuj != null) {
- val config = Configuration.Builder.withView(cuj.cujType, touchSurface)
- if (cuj.tag != null) {
- config.setTag(cuj.tag)
+ val config = controller.jankConfigurationBuilder()
+ if (config != null) {
+ if (cuj.tag != null) {
+ config.setTag(cuj.tag)
+ }
+
+ interactionJankMonitor.begin(config)
+ hasInstrumentedJank = true
}
- interactionJankMonitor.begin(config)
}
// Create the dialog so that its onCreate() method is called, which usually sets the dialog
@@ -618,47 +777,45 @@
// Show the dialog.
dialog.show()
- addTouchSurfaceGhost()
+ moveSourceDrawingToDialog()
}
- private fun addTouchSurfaceGhost() {
+ private fun moveSourceDrawingToDialog() {
if (decorView.viewRootImpl == null) {
- // Make sure that we have access to the dialog view root to synchronize the creation of
- // the ghost.
- decorView.post(::addTouchSurfaceGhost)
+ // Make sure that we have access to the dialog view root to move the drawing to the
+ // dialog overlay.
+ decorView.post(::moveSourceDrawingToDialog)
return
}
- // Create a ghost of the touch surface (which will make the touch surface invisible) and add
- // it to the host dialog. We trigger a one off synchronization to make sure that this is
- // done in sync between the two different windows.
+ // Move the drawing of the source in the overlay of this dialog, then animate. We trigger a
+ // one-off synchronization to make sure that this is done in sync between the two different
+ // windows.
synchronizeNextDraw(
then = {
- isTouchSurfaceGhostDrawn = true
+ isSourceDrawnInDialog = true
maybeStartLaunchAnimation()
}
)
- GhostView.addGhost(touchSurface, decorView)
-
- // The ghost of the touch surface was just created, so the touch surface is currently
- // invisible. We need to make sure that it stays invisible as long as the dialog is shown or
- // animating.
- (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
+ controller.startDrawingInOverlayOf(decorView)
}
/**
- * Synchronize the next draw of the touch surface and dialog view roots so that they are
- * performed at the same time, in the same transaction. This is necessary to make sure that the
- * ghost of the touch surface is drawn at the same time as the touch surface is made invisible
- * (or inversely, removed from the UI when the touch surface is made visible).
+ * Synchronize the next draw of the source and dialog view roots so that they are performed at
+ * the same time, in the same transaction. This is necessary to make sure that the source is
+ * drawn in the overlay at the same time as it is removed from its original position (or
+ * inversely, removed from the overlay when the source is moved back to its original position).
*/
private fun synchronizeNextDraw(then: () -> Unit) {
if (forceDisableSynchronization) {
+ // Don't synchronize when inside an automated test.
then()
return
}
- ViewRootSync.synchronizeNextDraw(touchSurface, decorView, then)
+ ViewRootSync.synchronizeNextDraw(controller.viewRoot.view, decorView, then)
+ decorView.invalidate()
+ controller.viewRoot.view.invalidate()
}
private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
@@ -681,7 +838,7 @@
}
private fun maybeStartLaunchAnimation() {
- if (!isTouchSurfaceGhostDrawn || !isOriginalDialogViewLaidOut) {
+ if (!isSourceDrawnInDialog || !isOriginalDialogViewLaidOut) {
return
}
@@ -690,19 +847,7 @@
startAnimation(
isLaunching = true,
- onLaunchAnimationStart = {
- // Remove the temporary ghost. Another ghost (that ghosts only the touch surface
- // content, and not its background) will be added right after this and will be
- // animated.
- GhostView.removeGhost(touchSurface)
- },
onLaunchAnimationEnd = {
- touchSurface.setTag(R.id.tag_launch_animation_running, null)
-
- // We hide the touch surface when the dialog is showing. We will make this view
- // visible again when dismissing the dialog.
- touchSurface.visibility = View.INVISIBLE
-
isLaunching = false
// dismiss was called during the animation, dismiss again now to actually dismiss.
@@ -718,7 +863,10 @@
backgroundLayoutListener
)
}
- cuj?.run { interactionJankMonitor.end(cujType) }
+
+ if (hasInstrumentedJank) {
+ interactionJankMonitor.end(controller.cuj!!.cujType)
+ }
}
)
}
@@ -753,8 +901,8 @@
}
/**
- * Hide the dialog into the touch surface and call [onAnimationFinished] when the animation is
- * done (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually
+ * Hide the dialog into the source and call [onAnimationFinished] when the animation is done
+ * (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually
* dismiss the dialog.
*/
private fun hideDialogIntoView(onAnimationFinished: (Boolean) -> Unit) {
@@ -763,17 +911,9 @@
decorView.removeOnLayoutChangeListener(decorViewLayoutListener)
}
- if (!shouldAnimateDialogIntoView()) {
- Log.i(TAG, "Skipping animation of dialog into the touch surface")
-
- // Make sure we allow the touch surface to change its visibility again.
- (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
-
- // If the view is invisible it's probably because of us, so we make it visible again.
- if (touchSurface.visibility == View.INVISIBLE) {
- touchSurface.visibility = View.VISIBLE
- }
-
+ if (!shouldAnimateDialogIntoSource()) {
+ Log.i(TAG, "Skipping animation of dialog into the source")
+ controller.onExitAnimationCancelled()
onAnimationFinished(false /* instantDismiss */)
onDialogDismissed(this@AnimatedDialog)
return
@@ -786,10 +926,6 @@
dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
},
onLaunchAnimationEnd = {
- // Make sure we allow the touch surface to change its visibility again.
- (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
-
- touchSurface.visibility = View.VISIBLE
val dialogContentWithBackground = this.dialogContentWithBackground!!
dialogContentWithBackground.visibility = View.INVISIBLE
@@ -799,14 +935,11 @@
)
}
- // Make sure that the removal of the ghost and making the touch surface visible is
- // done at the same time.
- synchronizeNextDraw(
- then = {
- onAnimationFinished(true /* instantDismiss */)
- onDialogDismissed(this@AnimatedDialog)
- }
- )
+ controller.stopDrawingInOverlay()
+ synchronizeNextDraw {
+ onAnimationFinished(true /* instantDismiss */)
+ onDialogDismissed(this@AnimatedDialog)
+ }
}
)
}
@@ -816,27 +949,34 @@
onLaunchAnimationStart: () -> Unit = {},
onLaunchAnimationEnd: () -> Unit = {}
) {
- // Create 2 ghost controllers to animate both the dialog and the touch surface in the
- // dialog.
- val startView = if (isLaunching) touchSurface else dialogContentWithBackground!!
- val endView = if (isLaunching) dialogContentWithBackground!! else touchSurface
- val startViewController = GhostedViewLaunchAnimatorController(startView)
- val endViewController = GhostedViewLaunchAnimatorController(endView)
- startViewController.launchContainer = decorView
- endViewController.launchContainer = decorView
+ // Create 2 controllers to animate both the dialog and the source.
+ val startController =
+ if (isLaunching) {
+ controller.createLaunchController()
+ } else {
+ GhostedViewLaunchAnimatorController(dialogContentWithBackground!!)
+ }
+ val endController =
+ if (isLaunching) {
+ GhostedViewLaunchAnimatorController(dialogContentWithBackground!!)
+ } else {
+ controller.createExitController()
+ }
+ startController.launchContainer = decorView
+ endController.launchContainer = decorView
- val endState = endViewController.createAnimatorState()
+ val endState = endController.createAnimatorState()
val controller =
object : LaunchAnimator.Controller {
override var launchContainer: ViewGroup
- get() = startViewController.launchContainer
+ get() = startController.launchContainer
set(value) {
- startViewController.launchContainer = value
- endViewController.launchContainer = value
+ startController.launchContainer = value
+ endController.launchContainer = value
}
override fun createAnimatorState(): LaunchAnimator.State {
- return startViewController.createAnimatorState()
+ return startController.createAnimatorState()
}
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
@@ -845,15 +985,29 @@
// onLaunchAnimationStart on the controller (which will create its own ghost).
onLaunchAnimationStart()
- startViewController.onLaunchAnimationStart(isExpandingFullyAbove)
- endViewController.onLaunchAnimationStart(isExpandingFullyAbove)
+ startController.onLaunchAnimationStart(isExpandingFullyAbove)
+ endController.onLaunchAnimationStart(isExpandingFullyAbove)
}
override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- startViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
- endViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
+ // onLaunchAnimationEnd is called by an Animator at the end of the animation,
+ // on a Choreographer animation tick. The following calls will move the animated
+ // content from the dialog overlay back to its original position, and this
+ // change must be reflected in the next frame given that we then sync the next
+ // frame of both the content and dialog ViewRoots. However, in case that content
+ // is rendered by Compose, whose compositions are also scheduled on a
+ // Choreographer frame, any state change made *right now* won't be reflected in
+ // the next frame given that a Choreographer frame can't schedule another and
+ // have it happen in the same frame. So we post the forwarded calls to
+ // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring
+ // that the move of the content back to its original window will be reflected in
+ // the next frame right after [onLaunchAnimationEnd] is called.
+ dialog.context.mainExecutor.execute {
+ startController.onLaunchAnimationEnd(isExpandingFullyAbove)
+ endController.onLaunchAnimationEnd(isExpandingFullyAbove)
- onLaunchAnimationEnd()
+ onLaunchAnimationEnd()
+ }
}
override fun onLaunchAnimationProgress(
@@ -861,11 +1015,11 @@
progress: Float,
linearProgress: Float
) {
- startViewController.onLaunchAnimationProgress(state, progress, linearProgress)
+ startController.onLaunchAnimationProgress(state, progress, linearProgress)
// The end view is visible only iff the starting view is not visible.
state.visible = !state.visible
- endViewController.onLaunchAnimationProgress(state, progress, linearProgress)
+ endController.onLaunchAnimationProgress(state, progress, linearProgress)
// If the dialog content is complex, its dimension might change during the
// launch animation. The animation end position might also change during the
@@ -873,14 +1027,16 @@
// Therefore we update the end state to the new position/size. Usually the
// dialog dimension or position will change in the early frames, so changing the
// end state shouldn't really be noticeable.
- endViewController.fillGhostedViewState(endState)
+ if (endController is GhostedViewLaunchAnimatorController) {
+ endController.fillGhostedViewState(endState)
+ }
}
}
launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
}
- private fun shouldAnimateDialogIntoView(): Boolean {
+ private fun shouldAnimateDialogIntoSource(): Boolean {
// Don't animate if the dialog was previously hidden using hide() or if we disabled the exit
// animation.
if (exitAnimationDisabled || !dialog.isShowing) {
@@ -888,24 +1044,12 @@
}
// If we are dreaming, the dialog was probably closed because of that so we don't animate
- // into the touchSurface.
+ // into the source.
if (callback.isDreaming()) {
return false
}
- // The touch surface 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 (touchSurface.visibility != View.INVISIBLE) {
- return false
- }
-
- // If the touch surface is not attached or one of its ancestors is not visible, then we
- // don't run the animation either.
- if (!touchSurface.isAttachedToWindow) {
- return false
- }
-
- return (touchSurface.parent as? View)?.isShown ?: true
+ return controller.shouldAnimateExit()
}
/** A layout listener to animate the change of bounds of the dialog background. */
@@ -988,17 +1132,13 @@
}
}
- fun prepareForStackDismiss(): View {
+ fun prepareForStackDismiss() {
if (parentAnimatedDialog == null) {
- return touchSurface
+ return
}
parentAnimatedDialog.exitAnimationDisabled = true
parentAnimatedDialog.dialog.hide()
- val view = parentAnimatedDialog.prepareForStackDismiss()
+ parentAnimatedDialog.prepareForStackDismiss()
parentAnimatedDialog.dialog.dismiss()
- // Make the touch surface invisible, so we end up animating to it when we actually
- // dismiss the stack
- view.visibility = View.INVISIBLE
- return view
}
}
diff --git a/packages/SystemUI/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/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
index 1b7e26b..58ffef2 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
@@ -360,7 +360,9 @@
* [interpolator] and [duration].
*
* The end state of the animation is controlled by [destination]. This value can be any of
- * the four corners, any of the four edges, or the center of the view.
+ * the four corners, any of the four edges, or the center of the view. If any margins are
+ * added on the side(s) of the [destination], the translation of those margins can be
+ * included by specifying [includeMargins].
*
* @param onAnimationEnd an optional runnable that will be run once the animation finishes
* successfully. Will not be run if the animation is cancelled.
@@ -371,6 +373,7 @@
destination: Hotspot = Hotspot.CENTER,
interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR,
duration: Long = DEFAULT_DURATION,
+ includeMargins: Boolean = false,
onAnimationEnd: Runnable? = null,
): Boolean {
if (
@@ -428,10 +431,12 @@
val endValues =
processEndValuesForRemoval(
destination,
+ rootView,
rootView.left,
rootView.top,
rootView.right,
- rootView.bottom
+ rootView.bottom,
+ includeMargins,
)
val boundsToAnimate = mutableSetOf<Bound>()
@@ -718,70 +723,111 @@
* | | -> | | -> | | -> x---x -> x
* | | x-------x x-----x
* x---------x
+ * 4) destination=TOP, includeMargins=true (and view has large top margin)
+ * x---------x
+ * x---------x
+ * x---------x x---------x
+ * x---------x | |
+ * x---------x | | x---------x
+ * | | | |
+ * | | -> x---------x -> -> ->
+ * | |
+ * x---------x
* ```
*/
private fun processEndValuesForRemoval(
destination: Hotspot,
+ rootView: View,
left: Int,
top: Int,
right: Int,
- bottom: Int
+ bottom: Int,
+ includeMargins: Boolean = false,
): Map<Bound, Int> {
- val endLeft =
- when (destination) {
- Hotspot.CENTER -> (left + right) / 2
- Hotspot.BOTTOM,
- Hotspot.BOTTOM_LEFT,
- Hotspot.LEFT,
- Hotspot.TOP_LEFT,
- Hotspot.TOP -> left
- Hotspot.TOP_RIGHT,
- Hotspot.RIGHT,
- Hotspot.BOTTOM_RIGHT -> right
- }
- val endTop =
- when (destination) {
- Hotspot.CENTER -> (top + bottom) / 2
- Hotspot.LEFT,
- Hotspot.TOP_LEFT,
- Hotspot.TOP,
- Hotspot.TOP_RIGHT,
- Hotspot.RIGHT -> top
- Hotspot.BOTTOM_RIGHT,
- Hotspot.BOTTOM,
- Hotspot.BOTTOM_LEFT -> bottom
- }
- val endRight =
- when (destination) {
- Hotspot.CENTER -> (left + right) / 2
- Hotspot.TOP,
- Hotspot.TOP_RIGHT,
- Hotspot.RIGHT,
- Hotspot.BOTTOM_RIGHT,
- Hotspot.BOTTOM -> right
- Hotspot.BOTTOM_LEFT,
- Hotspot.LEFT,
- Hotspot.TOP_LEFT -> left
- }
- val endBottom =
- when (destination) {
- Hotspot.CENTER -> (top + bottom) / 2
- Hotspot.RIGHT,
- Hotspot.BOTTOM_RIGHT,
- Hotspot.BOTTOM,
- Hotspot.BOTTOM_LEFT,
- Hotspot.LEFT -> bottom
- Hotspot.TOP_LEFT,
- Hotspot.TOP,
- Hotspot.TOP_RIGHT -> top
- }
+ val marginAdjustment =
+ if (includeMargins &&
+ (rootView.layoutParams is ViewGroup.MarginLayoutParams)) {
+ val marginLp = rootView.layoutParams as ViewGroup.MarginLayoutParams
+ DimenHolder(
+ left = marginLp.leftMargin,
+ top = marginLp.topMargin,
+ right = marginLp.rightMargin,
+ bottom = marginLp.bottomMargin
+ )
+ } else {
+ DimenHolder(0, 0, 0, 0)
+ }
- return mapOf(
- Bound.LEFT to endLeft,
- Bound.TOP to endTop,
- Bound.RIGHT to endRight,
- Bound.BOTTOM to endBottom
- )
+ // These are the end values to use *if* this bound is part of the destination.
+ val endLeft = left - marginAdjustment.left
+ val endTop = top - marginAdjustment.top
+ val endRight = right + marginAdjustment.right
+ val endBottom = bottom + marginAdjustment.bottom
+
+ // For the below calculations: We need to ensure that the destination bound and the
+ // bound *opposite* to the destination bound end at the same value, to ensure that the
+ // view has size 0 for that dimension.
+ // For example,
+ // - If destination=TOP, then endTop == endBottom. Left and right stay the same.
+ // - If destination=RIGHT, then endRight == endLeft. Top and bottom stay the same.
+ // - If destination=BOTTOM_LEFT, then endBottom == endTop AND endLeft == endRight.
+
+ return when (destination) {
+ Hotspot.TOP -> mapOf(
+ Bound.TOP to endTop,
+ Bound.BOTTOM to endTop,
+ Bound.LEFT to left,
+ Bound.RIGHT to right,
+ )
+ Hotspot.TOP_RIGHT -> mapOf(
+ Bound.TOP to endTop,
+ Bound.BOTTOM to endTop,
+ Bound.RIGHT to endRight,
+ Bound.LEFT to endRight,
+ )
+ Hotspot.RIGHT -> mapOf(
+ Bound.RIGHT to endRight,
+ Bound.LEFT to endRight,
+ Bound.TOP to top,
+ Bound.BOTTOM to bottom,
+ )
+ Hotspot.BOTTOM_RIGHT -> mapOf(
+ Bound.BOTTOM to endBottom,
+ Bound.TOP to endBottom,
+ Bound.RIGHT to endRight,
+ Bound.LEFT to endRight,
+ )
+ Hotspot.BOTTOM -> mapOf(
+ Bound.BOTTOM to endBottom,
+ Bound.TOP to endBottom,
+ Bound.LEFT to left,
+ Bound.RIGHT to right,
+ )
+ Hotspot.BOTTOM_LEFT -> mapOf(
+ Bound.BOTTOM to endBottom,
+ Bound.TOP to endBottom,
+ Bound.LEFT to endLeft,
+ Bound.RIGHT to endLeft,
+ )
+ Hotspot.LEFT -> mapOf(
+ Bound.LEFT to endLeft,
+ Bound.RIGHT to endLeft,
+ Bound.TOP to top,
+ Bound.BOTTOM to bottom,
+ )
+ Hotspot.TOP_LEFT -> mapOf(
+ Bound.TOP to endTop,
+ Bound.BOTTOM to endTop,
+ Bound.LEFT to endLeft,
+ Bound.RIGHT to endLeft,
+ )
+ Hotspot.CENTER -> mapOf(
+ Bound.LEFT to (endLeft + endRight) / 2,
+ Bound.RIGHT to (endLeft + endRight) / 2,
+ Bound.TOP to (endTop + endBottom) / 2,
+ Bound.BOTTOM to (endTop + endBottom) / 2,
+ )
+ }
}
/**
@@ -1061,4 +1107,12 @@
abstract fun setValue(view: View, value: Int)
abstract fun getValue(view: View): Int
}
+
+ /** Simple data class to hold a set of dimens for left, top, right, bottom. */
+ private data class DimenHolder(
+ val left: Int,
+ val top: Int,
+ val right: Int,
+ val bottom: Int,
+ )
}
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/BindServiceOnMainThreadDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt
new file mode 100644
index 0000000..1d808ba
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.SdkConstants.CLASS_CONTEXT
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiModifierListOwner
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.getParentOfType
+
+/**
+ * Warns if {@code Context.bindService}, {@code Context.bindServiceAsUser}, or {@code
+ * Context.unbindService} is not called on a {@code WorkerThread}
+ */
+@Suppress("UnstableApiUsage")
+class BindServiceOnMainThreadDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableMethodNames(): List<String> {
+ return listOf("bindService", "bindServiceAsUser", "unbindService")
+ }
+
+ private fun hasWorkerThreadAnnotation(
+ context: JavaContext,
+ annotated: PsiModifierListOwner?
+ ): Boolean {
+ return context.evaluator.getAnnotations(annotated, inHierarchy = true).any { uAnnotation ->
+ uAnnotation.qualifiedName == "androidx.annotation.WorkerThread"
+ }
+ }
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
+ if (
+ !hasWorkerThreadAnnotation(context, node.getParentOfType(UMethod::class.java)) &&
+ !hasWorkerThreadAnnotation(context, node.getParentOfType(UClass::class.java))
+ ) {
+ context.report(
+ ISSUE,
+ method,
+ context.getLocation(node),
+ "This method should be annotated with `@WorkerThread` because " +
+ "it calls ${method.name}",
+ )
+ }
+ }
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE: Issue =
+ Issue.create(
+ id = "BindServiceOnMainThread",
+ briefDescription = "Service bound or unbound on main thread",
+ explanation =
+ """
+ Binding and unbinding services are synchronous calls to `ActivityManager`. \
+ They usually take multiple milliseconds to complete. If called on the main \
+ thread, it will likely cause missed frames. To fix it, use a `@Background \
+ Executor` and annotate the calling method with `@WorkerThread`.
+ """,
+ category = Category.PERFORMANCE,
+ priority = 8,
+ severity = Severity.WARNING,
+ implementation =
+ Implementation(
+ BindServiceOnMainThreadDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+ }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt
deleted file mode 100644
index 925fae0e..0000000
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt
+++ /dev/null
@@ -1,67 +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.internal.systemui.lint
-
-import com.android.tools.lint.detector.api.Category
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Implementation
-import com.android.tools.lint.detector.api.Issue
-import com.android.tools.lint.detector.api.JavaContext
-import com.android.tools.lint.detector.api.Scope
-import com.android.tools.lint.detector.api.Severity
-import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.intellij.psi.PsiMethod
-import org.jetbrains.uast.UCallExpression
-
-@Suppress("UnstableApiUsage")
-class BindServiceViaContextDetector : Detector(), SourceCodeScanner {
-
- override fun getApplicableMethodNames(): List<String> {
- return listOf("bindService", "bindServiceAsUser", "unbindService")
- }
-
- override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
- if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) {
- context.report(
- ISSUE,
- method,
- context.getNameLocation(node),
- "Binding or unbinding services are synchronous calls, please make " +
- "sure you're on a @Background Executor."
- )
- }
- }
-
- companion object {
- @JvmField
- val ISSUE: Issue =
- Issue.create(
- id = "BindServiceViaContextDetector",
- briefDescription = "Service bound/unbound via Context, please make sure " +
- "you're on a background thread.",
- explanation =
- "Binding or unbinding services are synchronous calls to ActivityManager, " +
- "they usually take multiple milliseconds to complete and will make" +
- "the caller drop frames. Make sure you're on a @Background Executor.",
- category = Category.PERFORMANCE,
- priority = 8,
- severity = Severity.WARNING,
- implementation =
- Implementation(BindServiceViaContextDetector::class.java, Scope.JAVA_FILE_SCOPE)
- )
- }
-}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
index 8d48f09..1129929 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
@@ -16,6 +16,7 @@
package com.android.internal.systemui.lint
+import com.android.SdkConstants.CLASS_CONTEXT
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
@@ -48,14 +49,14 @@
return
}
- val evaulator = context.evaluator
- if (evaulator.isMemberInSubClassOf(method, "android.content.Context")) {
+ val evaluator = context.evaluator
+ if (evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
context.report(
ISSUE,
method,
context.getNameLocation(node),
- "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
- "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+ "`Context.${method.name}()` should be replaced with " +
+ "`BroadcastSender.${method.name}()`"
)
}
}
@@ -65,14 +66,14 @@
val ISSUE: Issue =
Issue.create(
id = "BroadcastSentViaContext",
- briefDescription = "Broadcast sent via Context instead of BroadcastSender.",
- explanation =
- "Broadcast was sent via " +
- "Context.sendBroadcast/Context.sendBroadcastAsUser. Please use " +
- "BroadcastSender.sendBroadcast/BroadcastSender.sendBroadcastAsUser " +
- "which will schedule dispatch of broadcasts on background thread. " +
- "Sending broadcasts on main thread causes jank due to synchronous " +
- "Binder calls.",
+ briefDescription = "Broadcast sent via `Context` instead of `BroadcastSender`",
+ // lint trims indents and converts \ to line continuations
+ explanation = """
+ Broadcasts sent via `Context.sendBroadcast()` or \
+ `Context.sendBroadcastAsUser()` will block the main thread and may cause \
+ missed frames. Instead, use `BroadcastSender.sendBroadcast()` or \
+ `BroadcastSender.sendBroadcastAsUser()` which will schedule and dispatch \
+ broadcasts on a background worker thread.""",
category = Category.PERFORMANCE,
priority = 8,
severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt
deleted file mode 100644
index a629eee..0000000
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt
+++ /dev/null
@@ -1,66 +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.internal.systemui.lint
-
-import com.android.tools.lint.detector.api.Category
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Implementation
-import com.android.tools.lint.detector.api.Issue
-import com.android.tools.lint.detector.api.JavaContext
-import com.android.tools.lint.detector.api.Scope
-import com.android.tools.lint.detector.api.Severity
-import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.intellij.psi.PsiMethod
-import org.jetbrains.uast.UCallExpression
-
-@Suppress("UnstableApiUsage")
-class GetMainLooperViaContextDetector : Detector(), SourceCodeScanner {
-
- override fun getApplicableMethodNames(): List<String> {
- return listOf("getMainThreadHandler", "getMainLooper", "getMainExecutor")
- }
-
- override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
- if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) {
- context.report(
- ISSUE,
- method,
- context.getNameLocation(node),
- "Please inject a @Main Executor instead."
- )
- }
- }
-
- companion object {
- @JvmField
- val ISSUE: Issue =
- Issue.create(
- id = "GetMainLooperViaContextDetector",
- briefDescription = "Please use idiomatic SystemUI executors, injecting " +
- "them via Dagger.",
- explanation = "Injecting the @Main Executor is preferred in order to make" +
- "dependencies explicit and increase testability. It's much " +
- "easier to pass a FakeExecutor on your test ctor than to " +
- "deal with loopers in unit tests.",
- category = Category.LINT,
- priority = 8,
- severity = Severity.WARNING,
- implementation = Implementation(GetMainLooperViaContextDetector::class.java,
- Scope.JAVA_FILE_SCOPE)
- )
- }
-}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt
new file mode 100644
index 0000000..bab76ab
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.SdkConstants.CLASS_CONTEXT
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+@Suppress("UnstableApiUsage")
+class NonInjectedMainThreadDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableMethodNames(): List<String> {
+ return listOf("getMainThreadHandler", "getMainLooper", "getMainExecutor")
+ }
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
+ context.report(
+ ISSUE,
+ method,
+ context.getNameLocation(node),
+ "Replace with injected `@Main Executor`."
+ )
+ }
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE: Issue =
+ Issue.create(
+ id = "NonInjectedMainThread",
+ briefDescription = "Main thread usage without dependency injection",
+ explanation =
+ """
+ Main thread should be injected using the `@Main Executor` instead \
+ of using the accessors in `Context`. This is to make the \
+ dependencies explicit and increase testability. It's much easier \
+ to pass a `FakeExecutor` on test constructors than it is to deal \
+ with loopers in unit tests.""",
+ category = Category.LINT,
+ priority = 8,
+ severity = Severity.WARNING,
+ implementation =
+ Implementation(NonInjectedMainThreadDetector::class.java, Scope.JAVA_FILE_SCOPE)
+ )
+ }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
index 4eb7c7d..b622900 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
@@ -16,6 +16,7 @@
package com.android.internal.systemui.lint
+import com.android.SdkConstants.CLASS_CONTEXT
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
@@ -32,7 +33,7 @@
class NonInjectedServiceDetector : Detector(), SourceCodeScanner {
override fun getApplicableMethodNames(): List<String> {
- return listOf("getSystemService")
+ return listOf("getSystemService", "get")
}
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
@@ -40,14 +41,25 @@
if (
!evaluator.isStatic(method) &&
method.name == "getSystemService" &&
- method.containingClass?.qualifiedName == "android.content.Context"
+ method.containingClass?.qualifiedName == CLASS_CONTEXT
) {
context.report(
ISSUE,
method,
context.getNameLocation(node),
- "Use @Inject to get the handle to a system-level services instead of using " +
- "Context.getSystemService()"
+ "Use `@Inject` to get system-level service handles instead of " +
+ "`Context.getSystemService()`"
+ )
+ } else if (
+ evaluator.isStatic(method) &&
+ method.name == "get" &&
+ method.containingClass?.qualifiedName == "android.accounts.AccountManager"
+ ) {
+ context.report(
+ ISSUE,
+ method,
+ context.getNameLocation(node),
+ "Replace `AccountManager.get()` with an injected instance of `AccountManager`"
)
}
}
@@ -57,14 +69,14 @@
val ISSUE: Issue =
Issue.create(
id = "NonInjectedService",
- briefDescription =
- "System-level services should be retrieved using " +
- "@Inject instead of Context.getSystemService().",
+ briefDescription = "System service not injected",
explanation =
- "Context.getSystemService() should be avoided because it makes testing " +
- "difficult. Instead, use an injected service. For example, " +
- "instead of calling Context.getSystemService(UserManager.class), " +
- "use @Inject and add UserManager to the constructor",
+ """
+ `Context.getSystemService()` should be avoided because it makes testing \
+ difficult. Instead, use an injected service. For example, instead of calling \
+ `Context.getSystemService(UserManager.class)` in a class, annotate the class' \
+ constructor with `@Inject` and add `UserManager` to the parameters.
+ """,
category = Category.CORRECTNESS,
priority = 8,
severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
index eb71d32..4ba3afc 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
@@ -16,6 +16,7 @@
package com.android.internal.systemui.lint
+import com.android.SdkConstants.CLASS_CONTEXT
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
@@ -35,12 +36,12 @@
}
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
- if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) {
+ if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
context.report(
ISSUE,
method,
context.getNameLocation(node),
- "BroadcastReceivers should be registered via BroadcastDispatcher."
+ "Register `BroadcastReceiver` using `BroadcastDispatcher` instead of `Context`"
)
}
}
@@ -49,14 +50,16 @@
@JvmField
val ISSUE: Issue =
Issue.create(
- id = "RegisterReceiverViaContextDetector",
- briefDescription = "Broadcast registrations via Context are blocking " +
- "calls. Please use BroadcastDispatcher.",
- explanation =
- "Context#registerReceiver is a blocking call to the system server, " +
- "making it very likely that you'll drop a frame. Please use " +
- "BroadcastDispatcher instead (or move this call to a " +
- "@Background Executor.)",
+ id = "RegisterReceiverViaContext",
+ briefDescription = "Blocking broadcast registration",
+ // lint trims indents and converts \ to line continuations
+ explanation = """
+ `Context.registerReceiver()` is a blocking call to the system server, \
+ making it very likely that you'll drop a frame. Please use \
+ `BroadcastDispatcher` instead, which registers the receiver on a \
+ background thread. `BroadcastDispatcher` also improves our visibility \
+ into ANRs.""",
+ moreInfo = "go/identifying-broadcast-threads",
category = Category.PERFORMANCE,
priority = 8,
severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt
index b006615..7be21a5 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt
@@ -49,8 +49,7 @@
ISSUE_SLOW_USER_ID_QUERY,
method,
context.getNameLocation(node),
- "ActivityManager.getCurrentUser() is slow. " +
- "Use UserTracker.getUserId() instead."
+ "Use `UserTracker.getUserId()` instead of `ActivityManager.getCurrentUser()`"
)
}
if (
@@ -62,7 +61,7 @@
ISSUE_SLOW_USER_INFO_QUERY,
method,
context.getNameLocation(node),
- "UserManager.getUserInfo() is slow. " + "Use UserTracker.getUserInfo() instead."
+ "Use `UserTracker.getUserInfo()` instead of `UserManager.getUserInfo()`"
)
}
}
@@ -72,11 +71,13 @@
val ISSUE_SLOW_USER_ID_QUERY: Issue =
Issue.create(
id = "SlowUserIdQuery",
- briefDescription = "User ID queried using ActivityManager instead of UserTracker.",
+ briefDescription = "User ID queried using ActivityManager",
explanation =
- "ActivityManager.getCurrentUser() makes a binder call and is slow. " +
- "Instead, inject a UserTracker and call UserTracker.getUserId(). For " +
- "more info, see: http://go/multi-user-in-systemui-slides",
+ """
+ `ActivityManager.getCurrentUser()` uses a blocking binder call and is slow. \
+ Instead, inject a `UserTracker` and call `UserTracker.getUserId()`.
+ """,
+ moreInfo = "http://go/multi-user-in-systemui-slides",
category = Category.PERFORMANCE,
priority = 8,
severity = Severity.WARNING,
@@ -88,11 +89,13 @@
val ISSUE_SLOW_USER_INFO_QUERY: Issue =
Issue.create(
id = "SlowUserInfoQuery",
- briefDescription = "User info queried using UserManager instead of UserTracker.",
+ briefDescription = "User info queried using UserManager",
explanation =
- "UserManager.getUserInfo() makes a binder call and is slow. " +
- "Instead, inject a UserTracker and call UserTracker.getUserInfo(). For " +
- "more info, see: http://go/multi-user-in-systemui-slides",
+ """
+ `UserManager.getUserInfo()` uses a blocking binder call and is slow. \
+ Instead, inject a `UserTracker` and call `UserTracker.getUserInfo()`.
+ """,
+ moreInfo = "http://go/multi-user-in-systemui-slides",
category = Category.PERFORMANCE,
priority = 8,
severity = Severity.WARNING,
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 a584894..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,14 +41,13 @@
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),
- "Usage of Config.HARDWARE is highly encouraged."
+ context.getNameLocation(reference),
+ "Replace software bitmap with `Config.HARDWARE`"
)
}
}
@@ -56,12 +56,12 @@
@JvmField
val ISSUE: Issue =
Issue.create(
- id = "SoftwareBitmapDetector",
- briefDescription = "Software bitmap detected. Please use Config.HARDWARE instead.",
- explanation =
- "Software bitmaps occupy twice as much memory, when compared to Config.HARDWARE. " +
- "In case you need to manipulate the pixels, please consider to either use" +
- "a shader (encouraged), or a short lived software bitmap.",
+ id = "SoftwareBitmap",
+ briefDescription = "Software bitmap",
+ explanation = """
+ Software bitmaps occupy twice as much memory as `Config.HARDWARE` bitmaps \
+ do. However, hardware bitmaps are read-only. If you need to manipulate the \
+ pixels, use a shader (preferably) or a short lived software bitmap.""",
category = Category.PERFORMANCE,
priority = 8,
severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt
new file mode 100644
index 0000000..1db0725
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+private const val CLASS_SETTINGS = "android.provider.Settings"
+
+/**
+ * Detects usage of static methods in android.provider.Settings and suggests to use an injected
+ * settings provider instance instead.
+ */
+@Suppress("UnstableApiUsage")
+class StaticSettingsProviderDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableMethodNames(): List<String> {
+ return listOf(
+ "getFloat",
+ "getInt",
+ "getLong",
+ "getString",
+ "getUriFor",
+ "putFloat",
+ "putInt",
+ "putLong",
+ "putString"
+ )
+ }
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ val evaluator = context.evaluator
+ val className = method.containingClass?.qualifiedName
+ if (
+ className != "$CLASS_SETTINGS.Global" &&
+ className != "$CLASS_SETTINGS.Secure" &&
+ className != "$CLASS_SETTINGS.System"
+ ) {
+ return
+ }
+ if (!evaluator.isStatic(method)) {
+ return
+ }
+
+ val subclassName = className.substring(CLASS_SETTINGS.length + 1)
+
+ context.report(
+ ISSUE,
+ method,
+ context.getNameLocation(node),
+ "`@Inject` a ${subclassName}Settings instead"
+ )
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE: Issue =
+ Issue.create(
+ id = "StaticSettingsProvider",
+ briefDescription = "Static settings provider usage",
+ explanation =
+ """
+ Static settings provider methods, such as `Settings.Global.putInt()`, should \
+ not be used because they make testing difficult. Instead, use an injected \
+ settings provider. For example, instead of calling `Settings.Secure.getInt()`, \
+ annotate the class constructor with `@Inject` and add `SecureSettings` to the \
+ parameters.
+ """,
+ category = Category.CORRECTNESS,
+ priority = 8,
+ severity = Severity.WARNING,
+ implementation =
+ Implementation(
+ StaticSettingsProviderDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+ }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index 312810b..3f334c1c 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -28,14 +28,15 @@
override val issues: List<Issue>
get() = listOf(
- BindServiceViaContextDetector.ISSUE,
+ BindServiceOnMainThreadDetector.ISSUE,
BroadcastSentViaContextDetector.ISSUE,
SlowUserQueryDetector.ISSUE_SLOW_USER_ID_QUERY,
SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY,
- GetMainLooperViaContextDetector.ISSUE,
+ NonInjectedMainThreadDetector.ISSUE,
RegisterReceiverViaContextDetector.ISSUE,
SoftwareBitmapDetector.ISSUE,
NonInjectedServiceDetector.ISSUE,
+ StaticSettingsProviderDetector.ISSUE
)
override val api: Int
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
index 26bd8d0..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
@@ -16,133 +16,25 @@
package com.android.internal.systemui.lint
+import com.android.annotations.NonNull
import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+import com.android.tools.lint.checks.infrastructure.TestFiles.LibraryReferenceTestFile
+import java.io.File
+import org.intellij.lang.annotations.Language
+
+@Suppress("UnstableApiUsage")
+@NonNull
+private fun indentedJava(@NonNull @Language("JAVA") source: String) = java(source).indented()
/*
* 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.
*/
-@Suppress("UnstableApiUsage")
internal val androidStubs =
arrayOf(
- java(
- """
-package android.app;
-
-public class ActivityManager {
- public static int getCurrentUser() {}
-}
-"""
- ),
- java(
- """
-package android.os;
-import android.content.pm.UserInfo;
-import android.annotation.UserIdInt;
-
-public class UserManager {
- public UserInfo getUserInfo(@UserIdInt int userId) {}
-}
-"""
- ),
- java("""
-package android.annotation;
-
-public @interface UserIdInt {}
-"""),
- java("""
-package android.content.pm;
-
-public class UserInfo {}
-"""),
- java("""
-package android.os;
-
-public class Looper {}
-"""),
- java("""
-package android.os;
-
-public class Handler {}
-"""),
- java("""
-package android.content;
-
-public class ServiceConnection {}
-"""),
- java("""
-package android.os;
-
-public enum UserHandle {
- ALL
-}
-"""),
- java(
- """
-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);
-}
-"""
- ),
- java(
- """
-package android.app;
-import android.content.Context;
-
-public class Activity extends Context {}
-"""
- ),
- java(
- """
-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;
- }
-}
-"""
- ),
- java("""
-package android.content;
-
-public class BroadcastReceiver {}
-"""),
- java("""
-package android.content;
-
-public class IntentFilter {}
-"""),
- java(
+ LibraryReferenceTestFile(File("framework.jar").canonicalFile),
+ LibraryReferenceTestFile(File("androidx.annotation_annotation.jar").canonicalFile),
+ indentedJava(
"""
package com.android.systemui.settings;
import android.content.pm.UserInfo;
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
new file mode 100644
index 0000000..c35ac61
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+class BindServiceOnMainThreadDetectorTest : SystemUILintDetectorTest() {
+
+ override fun getDetector(): Detector = BindServiceOnMainThreadDetector()
+
+ override fun getIssues(): List<Issue> = listOf(BindServiceOnMainThreadDetector.ISSUE)
+
+ @Test
+ fun testBindService() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class TestClass {
+ public void bind(Context context) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ context.bindService(intent, null, 0);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceOnMainThreadDetector.ISSUE)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: This method should be annotated with @WorkerThread because it calls bindService [BindServiceOnMainThread]
+ context.bindService(intent, null, 0);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ }
+
+ @Test
+ fun testBindServiceAsUser() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+ import android.os.UserHandle;
+
+ public class TestClass {
+ public void bind(Context context) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ context.bindServiceAsUser(intent, null, 0, UserHandle.ALL);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceOnMainThreadDetector.ISSUE)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:8: Warning: This method should be annotated with @WorkerThread because it calls bindServiceAsUser [BindServiceOnMainThread]
+ context.bindServiceAsUser(intent, null, 0, UserHandle.ALL);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ }
+
+ @Test
+ fun testUnbindService() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+ import android.content.ServiceConnection;
+
+ public class TestClass {
+ public void unbind(Context context, ServiceConnection connection) {
+ context.unbindService(connection);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceOnMainThreadDetector.ISSUE)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: This method should be annotated with @WorkerThread because it calls unbindService [BindServiceOnMainThread]
+ context.unbindService(connection);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ }
+
+ @Test
+ fun testWorkerMethod() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+ import android.content.ServiceConnection;
+ import androidx.annotation.WorkerThread;
+
+ public class TestClass {
+ @WorkerThread
+ public void unbind(Context context, ServiceConnection connection) {
+ context.unbindService(connection);
+ }
+ }
+
+ public class ChildTestClass extends TestClass {
+ @Override
+ public void unbind(Context context, ServiceConnection connection) {
+ context.unbindService(connection);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceOnMainThreadDetector.ISSUE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testWorkerClass() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+ import android.content.ServiceConnection;
+ import androidx.annotation.WorkerThread;
+
+ @WorkerThread
+ public class TestClass {
+ public void unbind(Context context, ServiceConnection connection) {
+ context.unbindService(connection);
+ }
+ }
+
+ public class ChildTestClass extends TestClass {
+ @Override
+ public void unbind(Context context, ServiceConnection connection) {
+ context.unbindService(connection);
+ }
+
+ public void bind(Context context, ServiceConnection connection) {
+ context.bind(connection);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceOnMainThreadDetector.ISSUE)
+ .run()
+ .expectClean()
+ }
+
+ private val stubs = androidStubs
+}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt
deleted file mode 100644
index 564afcb..0000000
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt
+++ /dev/null
@@ -1,116 +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.internal.systemui.lint
-
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Issue
-import org.junit.Test
-
-@Suppress("UnstableApiUsage")
-class BindServiceViaContextDetectorTest : LintDetectorTest() {
-
- override fun getDetector(): Detector = BindServiceViaContextDetector()
- override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
-
- override fun getIssues(): List<Issue> = listOf(BindServiceViaContextDetector.ISSUE)
-
- private val explanation = "Binding or unbinding services are synchronous calls"
-
- @Test
- fun testBindService() {
- lint()
- .files(
- TestFiles.java(
- """
- package test.pkg;
- import android.content.Context;
-
- public class TestClass1 {
- public void bind(Context context) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- context.bindService(intent, null, 0);
- }
- }
- """
- )
- .indented(),
- *stubs
- )
- .issues(BindServiceViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
- }
-
- @Test
- fun testBindServiceAsUser() {
- lint()
- .files(
- TestFiles.java(
- """
- package test.pkg;
- import android.content.Context;
- import android.os.UserHandle;
-
- public class TestClass1 {
- public void bind(Context context) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- context.bindServiceAsUser(intent, null, 0, UserHandle.ALL);
- }
- }
- """
- )
- .indented(),
- *stubs
- )
- .issues(BindServiceViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
- }
-
- @Test
- fun testUnbindService() {
- lint()
- .files(
- TestFiles.java(
- """
- package test.pkg;
- import android.content.Context;
- import android.content.ServiceConnection;
-
- public class TestClass1 {
- public void unbind(Context context, ServiceConnection connection) {
- context.unbindService(connection);
- }
- }
- """
- )
- .indented(),
- *stubs
- )
- .issues(BindServiceViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
- }
-
- private val stubs = androidStubs
-}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
index 06aee8e..376acb5 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
@@ -16,18 +16,15 @@
package com.android.internal.systemui.lint
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
@Suppress("UnstableApiUsage")
-class BroadcastSentViaContextDetectorTest : LintDetectorTest() {
+class BroadcastSentViaContextDetectorTest : SystemUILintDetectorTest() {
override fun getDetector(): Detector = BroadcastSentViaContextDetector()
- override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
override fun getIssues(): List<Issue> = listOf(BroadcastSentViaContextDetector.ISSUE)
@@ -41,7 +38,7 @@
package test.pkg;
import android.content.Context;
- public class TestClass1 {
+ public class TestClass {
public void send(Context context) {
Intent intent = new Intent(Intent.ACTION_VIEW);
context.sendBroadcast(intent);
@@ -54,10 +51,13 @@
)
.issues(BroadcastSentViaContextDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(
- "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
- "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Context.sendBroadcast() should be replaced with BroadcastSender.sendBroadcast() [BroadcastSentViaContext]
+ context.sendBroadcast(intent);
+ ~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
)
}
@@ -71,7 +71,7 @@
import android.content.Context;
import android.os.UserHandle;
- public class TestClass1 {
+ public class TestClass {
public void send(Context context) {
Intent intent = new Intent(Intent.ACTION_VIEW);
context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
@@ -84,10 +84,13 @@
)
.issues(BroadcastSentViaContextDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(
- "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
- "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+ .expect(
+ """
+ src/test/pkg/TestClass.java:8: Warning: Context.sendBroadcastAsUser() should be replaced with BroadcastSender.sendBroadcastAsUser() [BroadcastSentViaContext]
+ context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
+ ~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
)
}
@@ -101,7 +104,7 @@
import android.app.Activity;
import android.os.UserHandle;
- public class TestClass1 {
+ public class TestClass {
public void send(Activity activity) {
Intent intent = new Intent(Intent.ACTION_VIEW);
activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
@@ -115,14 +118,44 @@
)
.issues(BroadcastSentViaContextDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(
- "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
- "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+ .expect(
+ """
+ src/test/pkg/TestClass.java:8: Warning: Context.sendBroadcastAsUser() should be replaced with BroadcastSender.sendBroadcastAsUser() [BroadcastSentViaContext]
+ activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
+ ~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
)
}
@Test
+ fun testSendBroadcastInBroadcastSender() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package com.android.systemui.broadcast;
+ import android.app.Activity;
+ import android.os.UserHandle;
+
+ public class BroadcastSender {
+ public void send(Activity activity) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
+ }
+
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BroadcastSentViaContextDetector.ISSUE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
fun testNoopIfNoCall() {
lint()
.files(
@@ -131,7 +164,7 @@
package test.pkg;
import android.content.Context;
- public class TestClass1 {
+ public class TestClass {
public void sendBroadcast() {
Intent intent = new Intent(Intent.ACTION_VIEW);
context.startActivity(intent);
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
similarity index 62%
rename from packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
index c55f399..301c338 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
@@ -16,22 +16,17 @@
package com.android.internal.systemui.lint
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
@Suppress("UnstableApiUsage")
-class GetMainLooperViaContextDetectorTest : LintDetectorTest() {
+class NonInjectedMainThreadDetectorTest : SystemUILintDetectorTest() {
- override fun getDetector(): Detector = GetMainLooperViaContextDetector()
- override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+ override fun getDetector(): Detector = NonInjectedMainThreadDetector()
- override fun getIssues(): List<Issue> = listOf(GetMainLooperViaContextDetector.ISSUE)
-
- private val explanation = "Please inject a @Main Executor instead."
+ override fun getIssues(): List<Issue> = listOf(NonInjectedMainThreadDetector.ISSUE)
@Test
fun testGetMainThreadHandler() {
@@ -43,7 +38,7 @@
import android.content.Context;
import android.os.Handler;
- public class TestClass1 {
+ public class TestClass {
public void test(Context context) {
Handler mainThreadHandler = context.getMainThreadHandler();
}
@@ -53,10 +48,16 @@
.indented(),
*stubs
)
- .issues(GetMainLooperViaContextDetector.ISSUE)
+ .issues(NonInjectedMainThreadDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread]
+ Handler mainThreadHandler = context.getMainThreadHandler();
+ ~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
@Test
@@ -69,7 +70,7 @@
import android.content.Context;
import android.os.Looper;
- public class TestClass1 {
+ public class TestClass {
public void test(Context context) {
Looper mainLooper = context.getMainLooper();
}
@@ -79,10 +80,16 @@
.indented(),
*stubs
)
- .issues(GetMainLooperViaContextDetector.ISSUE)
+ .issues(NonInjectedMainThreadDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread]
+ Looper mainLooper = context.getMainLooper();
+ ~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
@Test
@@ -95,7 +102,7 @@
import android.content.Context;
import java.util.concurrent.Executor;
- public class TestClass1 {
+ public class TestClass {
public void test(Context context) {
Executor mainExecutor = context.getMainExecutor();
}
@@ -105,10 +112,16 @@
.indented(),
*stubs
)
- .issues(GetMainLooperViaContextDetector.ISSUE)
+ .issues(NonInjectedMainThreadDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread]
+ Executor mainExecutor = context.getMainExecutor();
+ ~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
private val stubs = androidStubs
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
index 6b9f88f..0a74bfc 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
@@ -16,18 +16,15 @@
package com.android.internal.systemui.lint
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
@Suppress("UnstableApiUsage")
-class NonInjectedServiceDetectorTest : LintDetectorTest() {
+class NonInjectedServiceDetectorTest : SystemUILintDetectorTest() {
override fun getDetector(): Detector = NonInjectedServiceDetector()
- override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
override fun getIssues(): List<Issue> = listOf(NonInjectedServiceDetector.ISSUE)
@Test
@@ -39,7 +36,7 @@
package test.pkg;
import android.content.Context;
- public class TestClass1 {
+ public class TestClass {
public void getSystemServiceWithoutDagger(Context context) {
context.getSystemService("user");
}
@@ -51,8 +48,14 @@
)
.issues(NonInjectedServiceDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains("Use @Inject to get the handle")
+ .expect(
+ """
+ src/test/pkg/TestClass.java:6: Warning: Use @Inject to get system-level service handles instead of Context.getSystemService() [NonInjectedService]
+ context.getSystemService("user");
+ ~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
@Test
@@ -65,7 +68,7 @@
import android.content.Context;
import android.os.UserManager;
- public class TestClass2 {
+ public class TestClass {
public void getSystemServiceWithoutDagger(Context context) {
context.getSystemService(UserManager.class);
}
@@ -77,8 +80,46 @@
)
.issues(NonInjectedServiceDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains("Use @Inject to get the handle")
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Use @Inject to get system-level service handles instead of Context.getSystemService() [NonInjectedService]
+ context.getSystemService(UserManager.class);
+ ~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ }
+
+ @Test
+ fun testGetAccountManager() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+ import android.accounts.AccountManager;
+
+ public class TestClass {
+ public void getSystemServiceWithoutDagger(Context context) {
+ AccountManager.get(context);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(NonInjectedServiceDetector.ISSUE)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Replace AccountManager.get() with an injected instance of AccountManager [NonInjectedService]
+ AccountManager.get(context);
+ ~~~
+ 0 errors, 1 warnings
+ """
+ )
}
private val stubs = androidStubs
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
index 802ceba..9ed7aa0 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
@@ -16,23 +16,18 @@
package com.android.internal.systemui.lint
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
@Suppress("UnstableApiUsage")
-class RegisterReceiverViaContextDetectorTest : LintDetectorTest() {
+class RegisterReceiverViaContextDetectorTest : SystemUILintDetectorTest() {
override fun getDetector(): Detector = RegisterReceiverViaContextDetector()
- override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE)
- private val explanation = "BroadcastReceivers should be registered via BroadcastDispatcher."
-
@Test
fun testRegisterReceiver() {
lint()
@@ -44,7 +39,7 @@
import android.content.Context;
import android.content.IntentFilter;
- public class TestClass1 {
+ public class TestClass {
public void bind(Context context, BroadcastReceiver receiver,
IntentFilter filter) {
context.registerReceiver(receiver, filter, 0);
@@ -57,8 +52,14 @@
)
.issues(RegisterReceiverViaContextDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ .expect(
+ """
+ src/test/pkg/TestClass.java:9: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext]
+ context.registerReceiver(receiver, filter, 0);
+ ~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
@Test
@@ -74,7 +75,7 @@
import android.os.Handler;
import android.os.UserHandle;
- public class TestClass1 {
+ public class TestClass {
public void bind(Context context, BroadcastReceiver receiver,
IntentFilter filter, Handler handler) {
context.registerReceiverAsUser(receiver, UserHandle.ALL, filter,
@@ -88,8 +89,14 @@
)
.issues(RegisterReceiverViaContextDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ .expect(
+ """
+ src/test/pkg/TestClass.java:11: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext]
+ context.registerReceiverAsUser(receiver, UserHandle.ALL, filter,
+ ~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
@Test
@@ -105,7 +112,7 @@
import android.os.Handler;
import android.os.UserHandle;
- public class TestClass1 {
+ public class TestClass {
public void bind(Context context, BroadcastReceiver receiver,
IntentFilter filter, Handler handler) {
context.registerReceiverForAllUsers(receiver, filter, "permission",
@@ -119,8 +126,14 @@
)
.issues(RegisterReceiverViaContextDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ .expect(
+ """
+ src/test/pkg/TestClass.java:11: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext]
+ context.registerReceiverForAllUsers(receiver, filter, "permission",
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
private val stubs = androidStubs
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
index e265837..54cac7b 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
@@ -16,18 +16,15 @@
package com.android.internal.systemui.lint
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
@Suppress("UnstableApiUsage")
-class SlowUserQueryDetectorTest : LintDetectorTest() {
+class SlowUserQueryDetectorTest : SystemUILintDetectorTest() {
override fun getDetector(): Detector = SlowUserQueryDetector()
- override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
override fun getIssues(): List<Issue> =
listOf(
@@ -44,7 +41,7 @@
package test.pkg;
import android.app.ActivityManager;
- public class TestClass1 {
+ public class TestClass {
public void slewlyGetCurrentUser() {
ActivityManager.getCurrentUser();
}
@@ -59,10 +56,13 @@
SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY
)
.run()
- .expectWarningCount(1)
- .expectContains(
- "ActivityManager.getCurrentUser() is slow. " +
- "Use UserTracker.getUserId() instead."
+ .expect(
+ """
+ src/test/pkg/TestClass.java:6: Warning: Use UserTracker.getUserId() instead of ActivityManager.getCurrentUser() [SlowUserIdQuery]
+ ActivityManager.getCurrentUser();
+ ~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
)
}
@@ -75,7 +75,7 @@
package test.pkg;
import android.os.UserManager;
- public class TestClass2 {
+ public class TestClass {
public void slewlyGetUserInfo(UserManager userManager) {
userManager.getUserInfo();
}
@@ -90,9 +90,13 @@
SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY
)
.run()
- .expectWarningCount(1)
- .expectContains(
- "UserManager.getUserInfo() is slow. " + "Use UserTracker.getUserInfo() instead."
+ .expect(
+ """
+ src/test/pkg/TestClass.java:6: Warning: Use UserTracker.getUserInfo() instead of UserManager.getUserInfo() [SlowUserInfoQuery]
+ userManager.getUserInfo();
+ ~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
)
}
@@ -105,7 +109,7 @@
package test.pkg;
import com.android.systemui.settings.UserTracker;
- public class TestClass3 {
+ public class TestClass {
public void quicklyGetUserId(UserTracker userTracker) {
userTracker.getUserId();
}
@@ -132,7 +136,7 @@
package test.pkg;
import com.android.systemui.settings.UserTracker;
- public class TestClass4 {
+ public class TestClass {
public void quicklyGetUserId(UserTracker userTracker) {
userTracker.getUserInfo();
}
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 fd6ab09..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
@@ -16,23 +16,18 @@
package com.android.internal.systemui.lint
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
@Suppress("UnstableApiUsage")
-class SoftwareBitmapDetectorTest : LintDetectorTest() {
+class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() {
override fun getDetector(): Detector = SoftwareBitmapDetector()
- override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
override fun getIssues(): List<Issue> = listOf(SoftwareBitmapDetector.ISSUE)
- private val explanation = "Usage of Config.HARDWARE is highly encouraged."
-
@Test
fun testSoftwareBitmap() {
lint()
@@ -41,7 +36,7 @@
"""
import android.graphics.Bitmap;
- public class TestClass1 {
+ public class TestClass {
public void test() {
Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565);
Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
@@ -54,8 +49,17 @@
)
.issues(SoftwareBitmapDetector.ISSUE)
.run()
- .expectWarningCount(2)
- .expectContains(explanation)
+ .expect(
+ """
+ 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
+ """
+ )
}
@Test
@@ -63,22 +67,21 @@
lint()
.files(
TestFiles.java(
- """
+ """
import android.graphics.Bitmap;
- public class TestClass1 {
+ public class TestClass {
public void test() {
Bitmap.createBitmap(300, 300, Bitmap.Config.HARDWARE);
}
}
"""
- )
- .indented(),
+ ),
*stubs
)
.issues(SoftwareBitmapDetector.ISSUE)
.run()
- .expectWarningCount(0)
+ .expectClean()
}
private val stubs = androidStubs
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt
new file mode 100644
index 0000000..b83ed70
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+class StaticSettingsProviderDetectorTest : SystemUILintDetectorTest() {
+
+ override fun getDetector(): Detector = StaticSettingsProviderDetector()
+ override fun getIssues(): List<Issue> = listOf(StaticSettingsProviderDetector.ISSUE)
+
+ @Test
+ fun testGetServiceWithString() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+
+ import android.provider.Settings;
+ import android.provider.Settings.Global;
+ import android.provider.Settings.Secure;
+
+ public class TestClass {
+ public void getSystemServiceWithoutDagger(Context context) {
+ final ContentResolver cr = mContext.getContentResolver();
+ Global.getFloat(cr, Settings.Global.UNLOCK_SOUND);
+ Global.getInt(cr, Settings.Global.UNLOCK_SOUND);
+ Global.getLong(cr, Settings.Global.UNLOCK_SOUND);
+ Global.getString(cr, Settings.Global.UNLOCK_SOUND);
+ Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+ Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+ Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+ Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1");
+ Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+ Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+ Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+ Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+
+ Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+ Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+ Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+ Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+ Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+ Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+ Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+ Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+ Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+ Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+ Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+ Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+
+ Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+ Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+ Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+ Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+ Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+ Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+ Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+ Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1");
+ Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+ Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+ Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+ Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(StaticSettingsProviderDetector.ISSUE)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:10: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+ Global.getFloat(cr, Settings.Global.UNLOCK_SOUND);
+ ~~~~~~~~
+ src/test/pkg/TestClass.java:11: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+ Global.getInt(cr, Settings.Global.UNLOCK_SOUND);
+ ~~~~~~
+ src/test/pkg/TestClass.java:12: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+ Global.getLong(cr, Settings.Global.UNLOCK_SOUND);
+ ~~~~~~~
+ src/test/pkg/TestClass.java:13: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+ Global.getString(cr, Settings.Global.UNLOCK_SOUND);
+ ~~~~~~~~~
+ src/test/pkg/TestClass.java:14: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+ Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+ ~~~~~~~~
+ src/test/pkg/TestClass.java:15: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+ Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+ ~~~~~~
+ src/test/pkg/TestClass.java:16: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+ Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+ ~~~~~~~
+ src/test/pkg/TestClass.java:17: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+ Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1");
+ ~~~~~~~~~
+ src/test/pkg/TestClass.java:18: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+ Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+ ~~~~~~~~
+ src/test/pkg/TestClass.java:19: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+ Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+ ~~~~~~
+ src/test/pkg/TestClass.java:20: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+ Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+ ~~~~~~~
+ src/test/pkg/TestClass.java:21: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+ Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+ ~~~~~~~~~
+ src/test/pkg/TestClass.java:23: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+ Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+ ~~~~~~~~
+ src/test/pkg/TestClass.java:24: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+ Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+ ~~~~~~
+ src/test/pkg/TestClass.java:25: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+ Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+ ~~~~~~~
+ src/test/pkg/TestClass.java:26: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+ Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+ ~~~~~~~~~
+ src/test/pkg/TestClass.java:27: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+ Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+ ~~~~~~~~
+ src/test/pkg/TestClass.java:28: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+ Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+ ~~~~~~
+ src/test/pkg/TestClass.java:29: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+ Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+ ~~~~~~~
+ src/test/pkg/TestClass.java:30: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+ Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+ ~~~~~~~~~
+ src/test/pkg/TestClass.java:31: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+ Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+ ~~~~~~~~
+ src/test/pkg/TestClass.java:32: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+ Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+ ~~~~~~
+ src/test/pkg/TestClass.java:33: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+ Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+ ~~~~~~~
+ src/test/pkg/TestClass.java:34: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+ Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+ ~~~~~~~~~
+ src/test/pkg/TestClass.java:36: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+ Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+ ~~~~~~~~
+ src/test/pkg/TestClass.java:37: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+ Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+ ~~~~~~
+ src/test/pkg/TestClass.java:38: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+ Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+ ~~~~~~~
+ src/test/pkg/TestClass.java:39: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+ Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+ ~~~~~~~~~
+ src/test/pkg/TestClass.java:40: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+ Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+ ~~~~~~~~
+ src/test/pkg/TestClass.java:41: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+ Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+ ~~~~~~
+ src/test/pkg/TestClass.java:42: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+ Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+ ~~~~~~~
+ src/test/pkg/TestClass.java:43: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+ Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1");
+ ~~~~~~~~~
+ src/test/pkg/TestClass.java:44: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+ Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+ ~~~~~~~~
+ src/test/pkg/TestClass.java:45: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+ Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+ ~~~~~~
+ src/test/pkg/TestClass.java:46: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+ Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+ ~~~~~~~
+ src/test/pkg/TestClass.java:47: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+ Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+ ~~~~~~~~~
+ 0 errors, 36 warnings
+ """
+ )
+ }
+
+ private val stubs = androidStubs
+}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt
new file mode 100644
index 0000000..3f93f07
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt
@@ -0,0 +1,48 @@
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import java.io.File
+import org.junit.ClassRule
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.model.Statement
+
+@Suppress("UnstableApiUsage")
+@RunWith(JUnit4::class)
+abstract class SystemUILintDetectorTest : LintDetectorTest() {
+
+ companion object {
+ @ClassRule
+ @JvmField
+ val libraryChecker: LibraryExists =
+ LibraryExists("framework.jar", "androidx.annotation_annotation.jar")
+ }
+
+ class LibraryExists(vararg val libraryNames: String) : TestRule {
+ override fun apply(base: Statement, description: Description): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ for (libName in libraryNames) {
+ val libFile = File(libName)
+ if (!libFile.canonicalFile.exists()) {
+ throw Exception(
+ "Could not find $libName in the test's working directory. " +
+ "File ${libFile.absolutePath} does not exist."
+ )
+ }
+ }
+ base.evaluate()
+ }
+ }
+ }
+ }
+ /**
+ * Customize the lint task to disable SDK usage completely. This ensures that running the tests
+ * in Android Studio has the same result as running the tests in atest
+ */
+ override fun lint(): TestLintTask =
+ super.lint().allowMissingSdk(true).sdkHome(File("/dev/null"))
+}
diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp
index 4cfe392..fbdb526 100644
--- a/packages/SystemUI/compose/core/Android.bp
+++ b/packages/SystemUI/compose/core/Android.bp
@@ -30,8 +30,11 @@
],
static_libs: [
+ "SystemUIAnimationLib",
+
"androidx.compose.runtime_runtime",
"androidx.compose.material3_material3",
+ "androidx.savedstate_savedstate",
],
kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt
new file mode 100644
index 0000000..8f9a4da
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt
@@ -0,0 +1,363 @@
+/*
+ * 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.compose.animation
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroupOverlay
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawOutline
+import androidx.compose.ui.graphics.drawscope.scale
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.Density
+import androidx.lifecycle.ViewTreeLifecycleOwner
+import androidx.lifecycle.ViewTreeViewModelStoreOwner
+import androidx.savedstate.ViewTreeSavedStateRegistryOwner
+import com.android.systemui.animation.LaunchAnimator
+import kotlin.math.min
+
+/**
+ * Create an expandable shape that can launch into an Activity or a Dialog.
+ *
+ * Example:
+ * ```
+ * Expandable(
+ * color = MaterialTheme.colorScheme.primary,
+ * shape = RoundedCornerShape(16.dp),
+ * ) { controller ->
+ * Row(
+ * Modifier
+ * // For activities:
+ * .clickable { activityStarter.startActivity(intent, controller.forActivity()) }
+ *
+ * // For dialogs:
+ * .clickable { dialogLaunchAnimator.show(dialog, controller.forDialog()) }
+ * ) { ... }
+ * }
+ * ```
+ *
+ * @sample com.android.systemui.compose.gallery.ActivityLaunchScreen
+ * @sample com.android.systemui.compose.gallery.DialogLaunchScreen
+ */
+@Composable
+fun Expandable(
+ color: Color,
+ shape: Shape,
+ modifier: Modifier = Modifier,
+ contentColor: Color = contentColorFor(color),
+ content: @Composable (ExpandableController) -> Unit,
+) {
+ Expandable(
+ rememberExpandableController(color, shape, contentColor),
+ modifier,
+ content,
+ )
+}
+
+/**
+ * Create an expandable shape that can launch into an Activity or a Dialog.
+ *
+ * This overload can be used in cases where you need to create the [ExpandableController] before
+ * composing this [Expandable], for instance if something outside of this Expandable can trigger a
+ * launch animation
+ *
+ * Example:
+ * ```
+ * // The controller that you can use to trigger the animations from anywhere.
+ * val controller =
+ * rememberExpandableController(
+ * color = MaterialTheme.colorScheme.primary,
+ * shape = RoundedCornerShape(16.dp),
+ * )
+ *
+ * Expandable(controller) {
+ * ...
+ * }
+ * ```
+ *
+ * @sample com.android.systemui.compose.gallery.ActivityLaunchScreen
+ * @sample com.android.systemui.compose.gallery.DialogLaunchScreen
+ */
+@Composable
+fun Expandable(
+ controller: ExpandableController,
+ modifier: Modifier = Modifier,
+ content: @Composable (ExpandableController) -> Unit,
+) {
+ val controller = controller as ExpandableControllerImpl
+ val color = controller.color
+ val contentColor = controller.contentColor
+ val shape = controller.shape
+
+ // TODO(b/230830644): Use movableContentOf to preserve the content state instead once the
+ // Compose libraries have been updated and include aosp/2163631.
+ val wrappedContent =
+ @Composable { controller: ExpandableController ->
+ CompositionLocalProvider(
+ LocalContentColor provides contentColor,
+ ) {
+ content(controller)
+ }
+ }
+
+ val thisExpandableSize by remember {
+ derivedStateOf { controller.boundsInComposeViewRoot.value.size }
+ }
+
+ // Make sure we don't read animatorState directly here to avoid recomposition every time the
+ // state changes (i.e. every frame of the animation).
+ val isAnimating by remember {
+ derivedStateOf {
+ controller.animatorState.value != null && controller.overlay.value != null
+ }
+ }
+
+ when {
+ isAnimating -> {
+ // Don't compose the movable content during the animation, as it should be composed only
+ // once at all times. We make this spacer exactly the same size as this Expandable when
+ // it is visible.
+ Spacer(
+ modifier
+ .clip(shape)
+ .requiredSize(with(controller.density) { thisExpandableSize.toDpSize() })
+ )
+
+ // The content and its animated background in the overlay. We draw it only when we are
+ // animating.
+ AnimatedContentInOverlay(
+ color,
+ thisExpandableSize,
+ controller.animatorState,
+ controller.overlay.value
+ ?: error("AnimatedContentInOverlay shouldn't be composed with null overlay."),
+ controller,
+ wrappedContent,
+ controller.composeViewRoot,
+ { controller.currentComposeViewInOverlay.value = it },
+ controller.density,
+ )
+ }
+ controller.isDialogShowing.value -> {
+ Box(
+ modifier
+ .drawWithContent { /* Don't draw anything when the dialog is shown. */}
+ .onGloballyPositioned {
+ controller.boundsInComposeViewRoot.value = it.boundsInRoot()
+ }
+ ) { wrappedContent(controller) }
+ }
+ else -> {
+ Box(
+ modifier.clip(shape).background(color, shape).onGloballyPositioned {
+ controller.boundsInComposeViewRoot.value = it.boundsInRoot()
+ }
+ ) { wrappedContent(controller) }
+ }
+ }
+}
+
+/** Draw [content] in [overlay] while respecting its screen position given by [animatorState]. */
+@Composable
+private fun AnimatedContentInOverlay(
+ color: Color,
+ sizeInOriginalLayout: Size,
+ animatorState: State<LaunchAnimator.State?>,
+ overlay: ViewGroupOverlay,
+ controller: ExpandableController,
+ content: @Composable (ExpandableController) -> Unit,
+ composeViewRoot: View,
+ onOverlayComposeViewChanged: (View?) -> Unit,
+ density: Density,
+) {
+ val compositionContext = rememberCompositionContext()
+ val context = LocalContext.current
+
+ // Create the ComposeView and force its content composition so that the movableContent is
+ // composed exactly once when we start animating.
+ val composeViewInOverlay =
+ remember(context, density) {
+ val startWidth = sizeInOriginalLayout.width
+ val startHeight = sizeInOriginalLayout.height
+ val contentModifier =
+ Modifier
+ // Draw the content with the same size as it was at the start of the animation
+ // so that its content is laid out exactly the same way.
+ .requiredSize(with(density) { sizeInOriginalLayout.toDpSize() })
+ .drawWithContent {
+ val animatorState = animatorState.value ?: return@drawWithContent
+
+ // Scale the content with the background while keeping its aspect ratio.
+ val widthRatio =
+ if (startWidth != 0f) {
+ animatorState.width.toFloat() / startWidth
+ } else {
+ 1f
+ }
+ val heightRatio =
+ if (startHeight != 0f) {
+ animatorState.height.toFloat() / startHeight
+ } else {
+ 1f
+ }
+ val scale = min(widthRatio, heightRatio)
+ scale(scale) { this@drawWithContent.drawContent() }
+ }
+
+ val composeView =
+ ComposeView(context).apply {
+ setContent {
+ Box(
+ Modifier.fillMaxSize().drawWithContent {
+ val animatorState = animatorState.value ?: return@drawWithContent
+ if (!animatorState.visible) {
+ return@drawWithContent
+ }
+
+ val topRadius = animatorState.topCornerRadius
+ val bottomRadius = animatorState.bottomCornerRadius
+ if (topRadius == bottomRadius) {
+ // Shortcut to avoid Outline calculation and allocation.
+ val cornerRadius = CornerRadius(topRadius)
+ drawRoundRect(color, cornerRadius = cornerRadius)
+ } else {
+ val shape =
+ RoundedCornerShape(
+ topStart = topRadius,
+ topEnd = topRadius,
+ bottomStart = bottomRadius,
+ bottomEnd = bottomRadius,
+ )
+ val outline = shape.createOutline(size, layoutDirection, this)
+ drawOutline(outline, color = color)
+ }
+
+ drawContent()
+ },
+ // We center the content in the expanding container.
+ contentAlignment = Alignment.Center,
+ ) {
+ Box(contentModifier) { content(controller) }
+ }
+ }
+ }
+
+ // Set the owners.
+ val overlayViewGroup =
+ getOverlayViewGroup(
+ context,
+ overlay,
+ )
+ ViewTreeLifecycleOwner.set(
+ overlayViewGroup,
+ ViewTreeLifecycleOwner.get(composeViewRoot),
+ )
+ ViewTreeViewModelStoreOwner.set(
+ overlayViewGroup,
+ ViewTreeViewModelStoreOwner.get(composeViewRoot),
+ )
+ ViewTreeSavedStateRegistryOwner.set(
+ overlayViewGroup,
+ ViewTreeSavedStateRegistryOwner.get(composeViewRoot),
+ )
+
+ composeView.setParentCompositionContext(compositionContext)
+
+ composeView
+ }
+
+ DisposableEffect(overlay, composeViewInOverlay) {
+ // Add the ComposeView to the overlay.
+ overlay.add(composeViewInOverlay)
+
+ val startState =
+ animatorState.value
+ ?: throw IllegalStateException(
+ "AnimatedContentInOverlay shouldn't be composed with null animatorState."
+ )
+ measureAndLayoutComposeViewInOverlay(composeViewInOverlay, startState)
+ onOverlayComposeViewChanged(composeViewInOverlay)
+
+ onDispose {
+ composeViewInOverlay.disposeComposition()
+ overlay.remove(composeViewInOverlay)
+ onOverlayComposeViewChanged(null)
+ }
+ }
+}
+
+internal fun measureAndLayoutComposeViewInOverlay(
+ view: View,
+ state: LaunchAnimator.State,
+) {
+ val exactWidth = state.width
+ val exactHeight = state.height
+ view.measure(
+ View.MeasureSpec.makeSafeMeasureSpec(exactWidth, View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeSafeMeasureSpec(exactHeight, View.MeasureSpec.EXACTLY),
+ )
+
+ val parent = view.parent as ViewGroup
+ val parentLocation = parent.locationOnScreen
+ val offsetX = parentLocation[0]
+ val offsetY = parentLocation[1]
+ view.layout(
+ state.left - offsetX,
+ state.top - offsetY,
+ state.right - offsetX,
+ state.bottom - offsetY,
+ )
+}
+
+// TODO(b/230830644): Add hidden API to ViewGroupOverlay to access this ViewGroup directly?
+private fun getOverlayViewGroup(context: Context, overlay: ViewGroupOverlay): ViewGroup {
+ val view = View(context)
+ overlay.add(view)
+ var current = view.parent
+ while (current.parent != null) {
+ current = current.parent
+ }
+ overlay.remove(view)
+ return current as ViewGroup
+}
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
new file mode 100644
index 0000000..50c3d7e
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt
@@ -0,0 +1,317 @@
+/*
+ * 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.compose.animation
+
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroupOverlay
+import android.view.ViewRootImpl
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
+import com.android.systemui.animation.LaunchAnimator
+import kotlin.math.roundToInt
+
+/** A controller that can control animated launches from an [Expandable]. */
+interface ExpandableController {
+ /** The [Expandable] controlled by this controller. */
+ val expandable: Expandable
+}
+
+/**
+ * Create an [ExpandableController] to control an [Expandable]. This is useful if you need to create
+ * the controller before the [Expandable], for instance to handle clicks outside of the Expandable
+ * that would still trigger a dialog/activity launch animation.
+ */
+@Composable
+fun rememberExpandableController(
+ color: Color,
+ shape: Shape,
+ contentColor: Color = contentColorFor(color),
+): ExpandableController {
+ val composeViewRoot = LocalView.current
+ val density = LocalDensity.current
+ val layoutDirection = LocalLayoutDirection.current
+
+ // The current animation state, if we are currently animating a dialog or activity.
+ val animatorState = remember { mutableStateOf<LaunchAnimator.State?>(null) }
+
+ // Whether a dialog controlled by this ExpandableController is currently showing.
+ val isDialogShowing = remember { mutableStateOf(false) }
+
+ // The overlay in which we should animate the launch.
+ val overlay = remember { mutableStateOf<ViewGroupOverlay?>(null) }
+
+ // The current [ComposeView] being animated in the [overlay], if any.
+ val currentComposeViewInOverlay = remember { mutableStateOf<View?>(null) }
+
+ // The bounds in [composeViewRoot] of the expandable controlled by this controller.
+ val boundsInComposeViewRoot = remember { mutableStateOf(Rect.Zero) }
+
+ // Whether this composable is still composed. We only do the dialog exit animation if this is
+ // true.
+ val isComposed = remember { mutableStateOf(true) }
+ DisposableEffect(Unit) { onDispose { isComposed.value = false } }
+
+ return remember(color, contentColor, shape, composeViewRoot, density, layoutDirection) {
+ ExpandableControllerImpl(
+ color,
+ contentColor,
+ shape,
+ composeViewRoot,
+ density,
+ animatorState,
+ isDialogShowing,
+ overlay,
+ currentComposeViewInOverlay,
+ boundsInComposeViewRoot,
+ layoutDirection,
+ isComposed,
+ )
+ }
+}
+
+internal class ExpandableControllerImpl(
+ internal val color: Color,
+ internal val contentColor: Color,
+ internal val shape: Shape,
+ internal val composeViewRoot: View,
+ internal val density: Density,
+ internal val animatorState: MutableState<LaunchAnimator.State?>,
+ internal val isDialogShowing: MutableState<Boolean>,
+ internal val overlay: MutableState<ViewGroupOverlay?>,
+ internal val currentComposeViewInOverlay: MutableState<View?>,
+ internal val boundsInComposeViewRoot: MutableState<Rect>,
+ private val layoutDirection: LayoutDirection,
+ private val isComposed: State<Boolean>,
+) : ExpandableController {
+ override val expandable: Expandable =
+ object : Expandable {
+ override fun activityLaunchController(
+ cujType: Int?,
+ ): ActivityLaunchAnimator.Controller? {
+ if (!isComposed.value) {
+ return null
+ }
+
+ 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
+ * animation. This controller will:
+ * 1. Compute the start/end animation state using [boundsInComposeViewRoot] and the location of
+ * composeViewRoot on the screen.
+ * 2. Update [animatorState] with the current animation state if we are animating, or null
+ * otherwise.
+ */
+ private fun launchController(): LaunchAnimator.Controller {
+ return object : LaunchAnimator.Controller {
+ private val rootLocationOnScreen = intArrayOf(0, 0)
+
+ override var launchContainer: ViewGroup = composeViewRoot.rootView as ViewGroup
+
+ override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ animatorState.value = null
+ }
+
+ override fun onLaunchAnimationProgress(
+ state: LaunchAnimator.State,
+ progress: Float,
+ linearProgress: Float
+ ) {
+ // We copy state given that it's always the same object that is mutated by
+ // ActivityLaunchAnimator.
+ animatorState.value =
+ LaunchAnimator.State(
+ state.top,
+ state.bottom,
+ state.left,
+ state.right,
+ state.topCornerRadius,
+ state.bottomCornerRadius,
+ )
+ .apply { visible = state.visible }
+
+ // Force measure and layout the ComposeView in the overlay whenever the animation
+ // state changes.
+ currentComposeViewInOverlay.value?.let {
+ measureAndLayoutComposeViewInOverlay(it, state)
+ }
+ }
+
+ override fun createAnimatorState(): LaunchAnimator.State {
+ val boundsInRoot = boundsInComposeViewRoot.value
+ val outline =
+ shape.createOutline(
+ Size(boundsInRoot.width, boundsInRoot.height),
+ layoutDirection,
+ density,
+ )
+
+ val (topCornerRadius, bottomCornerRadius) =
+ when (outline) {
+ is Outline.Rectangle -> 0f to 0f
+ is Outline.Rounded -> {
+ val roundRect = outline.roundRect
+
+ // TODO(b/230830644): Add better support different corner radii.
+ val topCornerRadius =
+ maxOf(
+ roundRect.topLeftCornerRadius.x,
+ roundRect.topLeftCornerRadius.y,
+ roundRect.topRightCornerRadius.x,
+ roundRect.topRightCornerRadius.y,
+ )
+ val bottomCornerRadius =
+ maxOf(
+ roundRect.bottomLeftCornerRadius.x,
+ roundRect.bottomLeftCornerRadius.y,
+ roundRect.bottomRightCornerRadius.x,
+ roundRect.bottomRightCornerRadius.y,
+ )
+
+ topCornerRadius to bottomCornerRadius
+ }
+ else ->
+ error(
+ "ExpandableState only supports (rounded) rectangles at the " +
+ "moment."
+ )
+ }
+
+ val rootLocation = rootLocationOnScreen()
+ return LaunchAnimator.State(
+ top = rootLocation.y.roundToInt(),
+ bottom = (rootLocation.y + boundsInRoot.height).roundToInt(),
+ left = rootLocation.x.roundToInt(),
+ right = (rootLocation.x + boundsInRoot.width).roundToInt(),
+ topCornerRadius = topCornerRadius,
+ bottomCornerRadius = bottomCornerRadius,
+ )
+ }
+
+ private fun rootLocationOnScreen(): Offset {
+ composeViewRoot.getLocationOnScreen(rootLocationOnScreen)
+ val boundsInRoot = boundsInComposeViewRoot.value
+ val x = rootLocationOnScreen[0] + boundsInRoot.left
+ val y = rootLocationOnScreen[1] + boundsInRoot.top
+ return Offset(x, y)
+ }
+ }
+ }
+
+ /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */
+ private fun activityController(cujType: Int?): ActivityLaunchAnimator.Controller {
+ val delegate = launchController()
+ return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate {
+ override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+ overlay.value = composeViewRoot.rootView.overlay as ViewGroupOverlay
+ }
+
+ override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ overlay.value = null
+ }
+ }
+ }
+
+ 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
+ if (newOverlay != overlay.value) {
+ overlay.value = newOverlay
+ }
+ }
+
+ override fun stopDrawingInOverlay() {
+ if (overlay.value != null) {
+ overlay.value = null
+ }
+ }
+
+ override fun createLaunchController(): LaunchAnimator.Controller {
+ val delegate = launchController()
+ return object : LaunchAnimator.Controller by delegate {
+ override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+
+ // Make sure we don't draw this expandable when the dialog is showing.
+ isDialogShowing.value = true
+ }
+ }
+ }
+
+ override fun createExitController(): LaunchAnimator.Controller {
+ val delegate = launchController()
+ return object : LaunchAnimator.Controller by delegate {
+ override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ isDialogShowing.value = false
+ }
+ }
+ }
+
+ override fun shouldAnimateExit(): Boolean = isComposed.value
+
+ override fun onExitAnimationCancelled() {
+ isDialogShowing.value = false
+ }
+
+ 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/docs/device-entry/doze.md b/packages/SystemUI/docs/device-entry/doze.md
index 6b6dce5..10bd367 100644
--- a/packages/SystemUI/docs/device-entry/doze.md
+++ b/packages/SystemUI/docs/device-entry/doze.md
@@ -1,5 +1,7 @@
# Doze
+`Dozing` is a low-powered state of the device. If Always-on Display (AOD), pulsing, or wake-gestures are enabled, then the device will enter the `dozing` state after a user intent to turn off the screen (ie: power button) or the screen times out.
+
Always-on Display (AOD) provides an alternative 'screen-off' experience. Instead, of completely turning the display off, it provides a distraction-free, glanceable experience for the phone in a low-powered mode. In this low-powered mode, the display will have a lower refresh rate and the UI should frequently shift its displayed contents in order to prevent burn-in. The recommended max on-pixel-ratio (OPR) is 5% to reduce battery consumption.

@@ -58,7 +60,7 @@
Refer to the documentation in [DozeSuppressors][15] for more information.
## AOD burn-in and image retention
-Because AOD will show an image on the screen for an elogated period of time, AOD designs must take into consideration burn-in (leaving a permanent mark on the screen). Temporary burn-in is called image-retention.
+Because AOD will show an image on the screen for an elongated period of time, AOD designs must take into consideration burn-in (leaving a permanent mark on the screen). Temporary burn-in is called image-retention.
To prevent burn-in, it is recommended to often shift UI on the screen. [DozeUi][17] schedules a call to dozeTimeTick every minute to request a shift in UI for all elements on AOD. The amount of shift can be determined by undergoing simulated AOD testing since this may vary depending on the display.
diff --git a/packages/SystemUI/docs/device-entry/glossary.md b/packages/SystemUI/docs/device-entry/glossary.md
index f3d12c2..7f19b16 100644
--- a/packages/SystemUI/docs/device-entry/glossary.md
+++ b/packages/SystemUI/docs/device-entry/glossary.md
@@ -2,38 +2,38 @@
## Keyguard
-| Term | Description |
-| :-----------: | ----------- |
-| Keyguard, [keyguard.md][1] | Coordinates the first experience when turning on the display of a device, as long as the user has not specified a security method of NONE. Consists of the lock screen and bouncer.|
-| Lock screen<br><br>| The first screen available when turning on the display of a device, as long as the user has not specified a security method of NONE. On the lock screen, users can access:<ul><li>Quick Settings - users can swipe down from the top of the screen to interact with quick settings tiles</li><li>[Keyguard Status Bar][9] - This special status bar shows SIM related information and system icons.</li><li>Clock - uses the font specified at [clock.xml][8]. If the clock font supports variable weights, users will experience delightful clock weight animations - in particular, on transitions between the lock screen and AOD.</li><li>Notifications - ability to view and interact with notifications depending on user lock screen notification settings: `Settings > Display > Lock screen > Privacy`</li><li>Message area - contains device information like biometric errors, charging information and device policy information. Also includes user configured information from `Settings > Display > Lock screen > Add text on lock screen`. </li><li>Bouncer - if the user has a primary authentication method, they can swipe up from the bottom of the screen to bring up the bouncer.</li></ul>The lock screen is one state of the notification shade. See [StatusBarState#KEYGUARD][10] and [StatusBarState#SHADE_LOCKED][10].|
-| Bouncer, [bouncer.md][2]<br><br>| The component responsible for displaying the primary security method set by the user (password, PIN, pattern). The bouncer can also show SIM-related security methods, allowing the user to unlock the device or SIM.|
-| Split shade | State of the shade (which keyguard is a part of) in which notifications are on the right side and Quick Settings on the left. For keyguard that means notifications being on the right side and clock with media being on the left.<br><br>Split shade is automatically activated - using resources - for big screens in landscape, see [sw600dp-land/config.xml][3] `config_use_split_notification_shade`.<br><br>In that state we can see the big clock more often - every time when media is not visible on the lock screen. When there is no media and no notifications - or we enter AOD - big clock is always positioned in the center of the screen.<br><br>The magic of positioning views happens by changing constraints of [NotificationsQuickSettingsContainer][4] and positioning elements vertically in [KeyguardClockPositionAlgorithm][5]|
-| Ambient display (AOD), [doze.md][6]<br><br>| UI shown when the device is in a low-powered display state. This is controlled by the doze component. The same lock screen views (ie: clock, notification shade) are used on AOD. The AOSP image on the left shows the usage of a clock that does not support variable weights which is why the clock is thicker in that image than what users see on Pixel devices.|
+| Term | Description |
+|--------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Keyguard, [keyguard.md][1] | Coordinates the first experience when turning on the display of a device, as long as the user has not specified a security method of NONE. Consists of the lock screen and bouncer. |
+| Lock screen<br><br> | The first screen available when turning on the display of a device, as long as the user has not specified a security method of NONE. On the lock screen, users can access:<ul><li>Quick Settings - users can swipe down from the top of the screen to interact with quick settings tiles</li><li>[Keyguard Status Bar][9] - This special status bar shows SIM related information and system icons.</li><li>Clock - uses the font specified at [clock.xml][8]. If the clock font supports variable weights, users will experience delightful clock weight animations - in particular, on transitions between the lock screen and AOD.</li><li>Notifications - ability to view and interact with notifications depending on user lock screen notification settings: `Settings > Display > Lock screen > Privacy`</li><li>Message area - contains device information like biometric errors, charging information and device policy information. Also includes user configured information from `Settings > Display > Lock screen > Add text on lock screen`. </li><li>Bouncer - if the user has a primary authentication method, they can swipe up from the bottom of the screen to bring up the bouncer.</li></ul>The lock screen is one state of the notification shade. See [StatusBarState#KEYGUARD][10] and [StatusBarState#SHADE_LOCKED][10]. |
+| Bouncer, [bouncer.md][2]<br><br> | The component responsible for displaying the primary security method set by the user (password, PIN, pattern). The bouncer can also show SIM-related security methods, allowing the user to unlock the device or SIM. |
+| Split shade | State of the shade (which keyguard is a part of) in which notifications are on the right side and Quick Settings on the left. For keyguard that means notifications being on the right side and clock with media being on the left.<br><br>Split shade is automatically activated - using resources - for big screens in landscape, see [sw600dp-land/config.xml][3] `config_use_split_notification_shade`.<br><br>In that state we can see the big clock more often - every time when media is not visible on the lock screen. When there is no media and no notifications - or we enter AOD - big clock is always positioned in the center of the screen.<br><br>The magic of positioning views happens by changing constraints of [NotificationsQuickSettingsContainer][4] and positioning elements vertically in [KeyguardClockPositionAlgorithm][5] |
+| Ambient display (AOD), [doze.md][6]<br><br> | UI shown when the device is in a low-powered display state. This is controlled by the doze component. The same lock screen views (ie: clock, notification shade) are used on AOD. The AOSP image on the left shows the usage of a clock that does not support variable weights which is why the clock is thicker in that image than what users see on Pixel devices. |
## General Authentication Terms
-| Term | Description |
-| ----------- | ----------- |
-| Primary Authentication | The strongest form of authentication. Includes: Pin, pattern and password input.|
-| Biometric Authentication | Face or fingerprint input. Biometric authentication is categorized into different classes of security. See [Measuring Biometric Security][7].|
+| Term | Description |
+|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
+| Primary Authentication | The strongest form of authentication. Includes: Pin, pattern and password input. |
+| Biometric Authentication | Face or fingerprint input. Biometric authentication is categorized into different classes of security. See [Measuring Biometric Security][7]. |
## Face Authentication Terms
-| Term | Description |
-| ----------- | ----------- |
-| Passive Authentication | When a user hasn't explicitly requested an authentication method; however, it may still put the device in an unlocked state.<br><br>For example, face authentication is triggered immediately when waking the device; however, users may not have the intent of unlocking their device. Instead, they could have wanted to just check the lock screen. Because of this, SystemUI provides the option for a bypass OR non-bypass face authentication experience which have different user flows.<br><br>In contrast, fingerprint authentication is considered an active authentication method since users need to actively put their finger on the fingerprint sensor to authenticate. Therefore, it's an explicit request for authentication and SystemUI knows the user has the intent for device-entry.|
-| Bypass | Used to refer to the face authentication bypass device entry experience. We have this distinction because face auth is a passive authentication method (see above).|
-| Bypass User Journey <br><br>| Once the user successfully authenticates with face, the keyguard immediately dismisses and the user is brought to the home screen/last app. This CUJ prioritizes speed of device entry. SystemUI hides interactive views (notifications) on the lock screen to avoid putting users in a state where the lock screen could immediately disappear while they're interacting with affordances on the lock screen.|
-| Non-bypass User Journey | Once the user successfully authenticates with face, the device remains on keyguard until the user performs an action to indicate they'd like to enter the device (ie: swipe up on the lock screen or long press on the unlocked icon). This CUJ prioritizes notification visibility.|
+| Term | Description |
+|-----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Passive Authentication | When a user hasn't explicitly requested an authentication method; however, it may still put the device in an unlocked state.<br><br>For example, face authentication is triggered immediately when waking the device; however, users may not have the intent of unlocking their device. Instead, they could have wanted to just check the lock screen. Because of this, SystemUI provides the option for a bypass OR non-bypass face authentication experience which have different user flows.<br><br>In contrast, fingerprint authentication is considered an active authentication method since users need to actively put their finger on the fingerprint sensor to authenticate. Therefore, it's an explicit request for authentication and SystemUI knows the user has the intent for device-entry. |
+| Bypass | Used to refer to the face authentication bypass device entry experience. We have this distinction because face auth is a passive authentication method (see above). |
+| Bypass User Journey <br><br> | Once the user successfully authenticates with face, the keyguard immediately dismisses and the user is brought to the home screen/last app. This CUJ prioritizes speed of device entry. SystemUI hides interactive views (notifications) on the lock screen to avoid putting users in a state where the lock screen could immediately disappear while they're interacting with affordances on the lock screen. |
+| Non-bypass User Journey | Once the user successfully authenticates with face, the device remains on keyguard until the user performs an action to indicate they'd like to enter the device (ie: swipe up on the lock screen or long press on the unlocked icon). This CUJ prioritizes notification visibility. |
## Fingerprint Authentication Terms
-| Term | Description |
-| ----------- | ----------- |
-| Under-display fingerprint sensor (UDFPS) | References the HW affordance for a fingerprint sensor that is under the display, which requires a software visual affordance. System UI supports showing the UDFPS affordance on the lock screen and on AOD. Users cannot authenticate from the screen-off state.<br><br>Supported SystemUI CUJs include:<ul><li> sliding finger on the screen to the UDFPS area to being authentication (as opposed to directly placing finger in the UDFPS area) </li><li> when a11y services are enabled, there is a haptic played when a touch is detected on UDFPS</li><li>after two hard-fingerprint-failures, the primary authentication bouncer is shown</li><li> when tapping on an affordance that requests to dismiss the lock screen, the user may see the UDFPS icon highlighted - see UDFPS bouncer</li></ul>|
-| UDFPS Bouncer | UI that highlights the UDFPS sensor. Users can get into this state after tapping on a notification from the lock screen or locked expanded shade.|
+| Term | Description |
+|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Under-display fingerprint sensor (UDFPS) | References the HW affordance for a fingerprint sensor that is under the display, which requires a software visual affordance. System UI supports showing the UDFPS affordance on the lock screen and on AOD. Users cannot authenticate from the screen-off state.<br><br>Supported SystemUI CUJs include:<ul><li> sliding finger on the screen to the UDFPS area to being authentication (as opposed to directly placing finger in the UDFPS area) </li><li> when a11y services are enabled, there is a haptic played when a touch is detected on UDFPS</li><li>after multiple consecutive hard-fingerprint-failures, the primary authentication bouncer is shown. The exact number of attempts is defined in: [BiometricUnlockController#UDFPS_ATTEMPTS_BEFORE_SHOW_BOUNCER][4]</li><li> when tapping on an affordance that requests to dismiss the lock screen, the user may see the UDFPS icon highlighted - see UDFPS bouncer</li></ul> |
+| UDFPS Bouncer | UI that highlights the UDFPS sensor. Users can get into this state after tapping on a notification from the lock screen or locked expanded shade. |
## Other Authentication Terms
-| Term | Description |
-| ---------- | ----------- |
-| Trust Agents | Provides signals to the keyguard to allow it to lock less frequently.|
+| Term | Description |
+|--------------|-----------------------------------------------------------------------|
+| Trust Agents | Provides signals to the keyguard to allow it to lock less frequently. |
[1]: /frameworks/base/packages/SystemUI/docs/device-entry/keyguard.md
@@ -46,3 +46,4 @@
[8]: /frameworks/base/packages/SystemUI/res-keyguard/font/clock.xml
[9]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
[10]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarState.java
+[11]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index 9f275af..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
@@ -246,8 +209,6 @@
-packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
-packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
-packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
--packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
--packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt
-packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogger.kt
-packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLogger.kt
-packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
@@ -493,7 +454,7 @@
-packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallFlags.kt
-packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLogger.kt
-packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
--packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
+-packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/ShadeStateListener.kt
-packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt
-packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
-packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
@@ -528,6 +489,8 @@
-packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
-packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
-packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+-packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+-packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarRootView.kt
-packages/SystemUI/src/com/android/systemui/toast/ToastDefaultAnimation.kt
-packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
-packages/SystemUI/src/com/android/systemui/tv/TVSystemUICoreStartableModule.kt
@@ -653,32 +616,11 @@
-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
-packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLoggerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
-packages/SystemUI/tests/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitorTest.kt
@@ -812,7 +754,7 @@
-packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLoggerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
+-packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/ShadeExpansionStateManagerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
-packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.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/Android.bp b/packages/SystemUI/plugin/Android.bp
index cafaaf8..7709f21 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -33,6 +33,7 @@
static_libs: [
"androidx.annotation_annotation",
+ "error_prone_annotations",
"PluginCoreLib",
"SystemUIAnimationLib",
],
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index 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/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
index 6124e10..6436dcb 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
@@ -14,12 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.log
+package com.android.systemui.plugins.log
import android.os.Trace
import android.util.Log
-import com.android.systemui.log.dagger.LogModule
-import com.android.systemui.util.collection.RingBuffer
+import com.android.systemui.plugins.util.RingBuffer
import com.google.errorprone.annotations.CompileTimeConstant
import java.io.PrintWriter
import java.util.concurrent.ArrayBlockingQueue
@@ -61,15 +60,18 @@
* In either case, `level` can be any of `verbose`, `debug`, `info`, `warn`, `error`, `assert`, or
* the first letter of any of the previous.
*
- * Buffers are provided by [LogModule]. Instances should be created using a [LogBufferFactory].
+ * In SystemUI, buffers are provided by LogModule. Instances should be created using a SysUI
+ * LogBufferFactory.
*
* @param name The name of this buffer, printed when the buffer is dumped and in some other
* situations.
* @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start
- * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches
- * the maximum, it behaves like a ring buffer.
+ * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches the
+ * maximum, it behaves like a ring buffer.
*/
-class LogBuffer @JvmOverloads constructor(
+class LogBuffer
+@JvmOverloads
+constructor(
private val name: String,
private val maxSize: Int,
private val logcatEchoTracker: LogcatEchoTracker,
@@ -78,7 +80,7 @@
private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() }
private val echoMessageQueue: BlockingQueue<LogMessage>? =
- if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null
+ if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null
init {
if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) {
@@ -133,11 +135,11 @@
*/
@JvmOverloads
inline fun log(
- tag: String,
- level: LogLevel,
- messageInitializer: MessageInitializer,
- noinline messagePrinter: MessagePrinter,
- exception: Throwable? = null,
+ tag: String,
+ level: LogLevel,
+ messageInitializer: MessageInitializer,
+ noinline messagePrinter: MessagePrinter,
+ exception: Throwable? = null,
) {
val message = obtain(tag, level, messagePrinter, exception)
messageInitializer(message)
@@ -152,14 +154,13 @@
* log message is built during runtime, use the [LogBuffer.log] overloaded method that takes in
* an initializer and a message printer.
*
- * Log buffers are limited by the number of entries, so logging more frequently
- * will limit the time window that the LogBuffer covers in a bug report. Richer logs, on the
- * other hand, make a bug report more actionable, so using the [log] with a messagePrinter to
- * add more detail to every log may do more to improve overall logging than adding more logs
- * with this method.
+ * Log buffers are limited by the number of entries, so logging more frequently will limit the
+ * time window that the LogBuffer covers in a bug report. Richer logs, on the other hand, make a
+ * bug report more actionable, so using the [log] with a messagePrinter to add more detail to
+ * every log may do more to improve overall logging than adding more logs with this method.
*/
fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) =
- log(tag, level, {str1 = message}, { str1!! })
+ log(tag, level, { str1 = message }, { str1!! })
/**
* You should call [log] instead of this method.
@@ -172,10 +173,10 @@
*/
@Synchronized
fun obtain(
- tag: String,
- level: LogLevel,
- messagePrinter: MessagePrinter,
- exception: Throwable? = null,
+ tag: String,
+ level: LogLevel,
+ messagePrinter: MessagePrinter,
+ exception: Throwable? = null,
): LogMessage {
if (!mutable) {
return FROZEN_MESSAGE
@@ -189,8 +190,7 @@
* You should call [log] instead of this method.
*
* After acquiring a message via [obtain], call this method to signal to the buffer that you
- * have finished filling in its data fields. The message will be echoed to logcat if
- * necessary.
+ * have finished filling in its data fields. The message will be echoed to logcat if necessary.
*/
@Synchronized
fun commit(message: LogMessage) {
@@ -213,7 +213,8 @@
/** Sends message to echo after determining whether to use Logcat and/or systrace. */
private fun echoToDesiredEndpoints(message: LogMessage) {
- val includeInLogcat = logcatEchoTracker.isBufferLoggable(name, message.level) ||
+ val includeInLogcat =
+ logcatEchoTracker.isBufferLoggable(name, message.level) ||
logcatEchoTracker.isTagLoggable(message.tag, message.level)
echo(message, toLogcat = includeInLogcat, toSystrace = systrace)
}
@@ -221,7 +222,12 @@
/** Converts the entire buffer to a newline-delimited string */
@Synchronized
fun dump(pw: PrintWriter, tailLength: Int) {
- val iterationStart = if (tailLength <= 0) { 0 } else { max(0, buffer.size - tailLength) }
+ val iterationStart =
+ if (tailLength <= 0) {
+ 0
+ } else {
+ max(0, buffer.size - tailLength)
+ }
for (i in iterationStart until buffer.size) {
buffer[i].dump(pw)
@@ -229,9 +235,9 @@
}
/**
- * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called.
- * Calls to [log], [obtain], and [commit] will not affect the buffer and will return dummy
- * values if necessary.
+ * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. Calls
+ * to [log], [obtain], and [commit] will not affect the buffer and will return dummy values if
+ * necessary.
*/
@Synchronized
fun freeze() {
@@ -241,9 +247,7 @@
}
}
- /**
- * Undoes the effects of calling [freeze].
- */
+ /** Undoes the effects of calling [freeze]. */
@Synchronized
fun unfreeze() {
if (frozen) {
@@ -265,8 +269,11 @@
}
private fun echoToSystrace(message: LogMessage, strMessage: String) {
- Trace.instantForTrack(Trace.TRACE_TAG_APP, "UI Events",
- "$name - ${message.level.shortString} ${message.tag}: $strMessage")
+ Trace.instantForTrack(
+ Trace.TRACE_TAG_APP,
+ "UI Events",
+ "$name - ${message.level.shortString} ${message.tag}: $strMessage"
+ )
}
private fun echoToLogcat(message: LogMessage, strMessage: String) {
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
similarity index 83%
rename from packages/SystemUI/src/com/android/systemui/log/LogLevel.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
index 53f231c..b036cf0 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
@@ -14,17 +14,12 @@
* limitations under the License.
*/
-package com.android.systemui.log
+package com.android.systemui.plugins.log
import android.util.Log
-/**
- * Enum version of @Log.Level
- */
-enum class LogLevel(
- @Log.Level val nativeLevel: Int,
- val shortString: String
-) {
+/** Enum version of @Log.Level */
+enum class LogLevel(@Log.Level val nativeLevel: Int, val shortString: String) {
VERBOSE(Log.VERBOSE, "V"),
DEBUG(Log.DEBUG, "D"),
INFO(Log.INFO, "I"),
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/log/LogMessage.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
index dae2592..9468681 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.log
+package com.android.systemui.plugins.log
import java.io.PrintWriter
import java.text.SimpleDateFormat
@@ -29,9 +29,10 @@
*
* When a message is logged, the code doing the logging stores data in one or more of the generic
* fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the
- * [messagePrinter] function reads the data stored in the generic fields and converts that to a human-
- * readable string. Thus, for every log type there must be a specialized initializer function that
- * stores data specific to that log type and a specialized printer function that prints that data.
+ * [messagePrinter] function reads the data stored in the generic fields and converts that to a
+ * human- readable string. Thus, for every log type there must be a specialized initializer function
+ * that stores data specific to that log type and a specialized printer function that prints that
+ * data.
*
* See [LogBuffer.log] for more information.
*/
@@ -55,9 +56,7 @@
var bool3: Boolean
var bool4: Boolean
- /**
- * Function that dumps the [LogMessage] to the provided [writer].
- */
+ /** Function that dumps the [LogMessage] to the provided [writer]. */
fun dump(writer: PrintWriter) {
val formattedTimestamp = DATE_FORMAT.format(timestamp)
val shortLevel = level.shortString
@@ -68,12 +67,12 @@
}
/**
- * A function that will be called if and when the message needs to be dumped to
- * logcat or a bug report. It should read the data stored by the initializer and convert it to
- * a human-readable string. The value of `this` will be the LogMessage to be printed.
- * **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and NEVER any
- * variables in its enclosing scope. Otherwise, the runtime will need to allocate a new instance
- * of the printer for each call, thwarting our attempts at avoiding any sort of allocation.
+ * A function that will be called if and when the message needs to be dumped to logcat or a bug
+ * report. It should read the data stored by the initializer and convert it to a human-readable
+ * string. The value of `this` will be the LogMessage to be printed. **IMPORTANT:** The printer
+ * should ONLY ever reference fields on the LogMessage and NEVER any variables in its enclosing
+ * scope. Otherwise, the runtime will need to allocate a new instance of the printer for each call,
+ * thwarting our attempts at avoiding any sort of allocation.
*/
typealias MessagePrinter = LogMessage.() -> String
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
index 4dd6f65..f2a6a91 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
@@ -14,11 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.log
+package com.android.systemui.plugins.log
-/**
- * Recyclable implementation of [LogMessage].
- */
+/** Recyclable implementation of [LogMessage]. */
data class LogMessageImpl(
override var level: LogLevel,
override var tag: String,
@@ -68,23 +66,24 @@
companion object Factory {
fun create(): LogMessageImpl {
return LogMessageImpl(
- LogLevel.DEBUG,
- DEFAULT_TAG,
- 0,
- DEFAULT_PRINTER,
- null,
- null,
- null,
- null,
- 0,
- 0,
- 0,
- 0,
- 0.0,
- false,
- false,
- false,
- false)
+ LogLevel.DEBUG,
+ DEFAULT_TAG,
+ 0,
+ DEFAULT_PRINTER,
+ null,
+ null,
+ null,
+ null,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0.0,
+ false,
+ false,
+ false,
+ false
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
similarity index 68%
rename from packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
index 8cda423..cfe894f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
@@ -14,24 +14,16 @@
* limitations under the License.
*/
-package com.android.systemui.log
+package com.android.systemui.plugins.log
-/**
- * Keeps track of which [LogBuffer] messages should also appear in logcat.
- */
+/** Keeps track of which [LogBuffer] messages should also appear in logcat. */
interface LogcatEchoTracker {
- /**
- * Whether [bufferName] should echo messages of [level] or higher to logcat.
- */
+ /** Whether [bufferName] should echo messages of [level] or higher to logcat. */
fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean
- /**
- * Whether [tagName] should echo messages of [level] or higher to logcat.
- */
+ /** Whether [tagName] should echo messages of [level] or higher to logcat. */
fun isTagLoggable(tagName: String, level: LogLevel): Boolean
- /**
- * Whether to log messages in a background thread.
- */
+ /** Whether to log messages in a background thread. */
val logInBackgroundThread: Boolean
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
similarity index 73%
rename from packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
index 40b0cdc..d3fabac 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.log
+package com.android.systemui.plugins.log
import android.content.ContentResolver
import android.database.ContentObserver
@@ -36,19 +36,15 @@
* $ adb shell settings put global systemui/tag/<tag> <level>
* ```
*/
-class LogcatEchoTrackerDebug private constructor(
- private val contentResolver: ContentResolver
-) : LogcatEchoTracker {
+class LogcatEchoTrackerDebug private constructor(private val contentResolver: ContentResolver) :
+ LogcatEchoTracker {
private val cachedBufferLevels: MutableMap<String, LogLevel> = mutableMapOf()
private val cachedTagLevels: MutableMap<String, LogLevel> = mutableMapOf()
override val logInBackgroundThread = true
companion object Factory {
@JvmStatic
- fun create(
- contentResolver: ContentResolver,
- mainLooper: Looper
- ): LogcatEchoTrackerDebug {
+ fun create(contentResolver: ContentResolver, mainLooper: Looper): LogcatEchoTrackerDebug {
val tracker = LogcatEchoTrackerDebug(contentResolver)
tracker.attach(mainLooper)
return tracker
@@ -57,37 +53,35 @@
private fun attach(mainLooper: Looper) {
contentResolver.registerContentObserver(
- Settings.Global.getUriFor(BUFFER_PATH),
- true,
- object : ContentObserver(Handler(mainLooper)) {
- override fun onChange(selfChange: Boolean, uri: Uri?) {
- super.onChange(selfChange, uri)
- cachedBufferLevels.clear()
- }
- })
+ Settings.Global.getUriFor(BUFFER_PATH),
+ true,
+ object : ContentObserver(Handler(mainLooper)) {
+ override fun onChange(selfChange: Boolean, uri: Uri?) {
+ super.onChange(selfChange, uri)
+ cachedBufferLevels.clear()
+ }
+ }
+ )
contentResolver.registerContentObserver(
- Settings.Global.getUriFor(TAG_PATH),
- true,
- object : ContentObserver(Handler(mainLooper)) {
- override fun onChange(selfChange: Boolean, uri: Uri?) {
- super.onChange(selfChange, uri)
- cachedTagLevels.clear()
- }
- })
+ Settings.Global.getUriFor(TAG_PATH),
+ true,
+ object : ContentObserver(Handler(mainLooper)) {
+ override fun onChange(selfChange: Boolean, uri: Uri?) {
+ super.onChange(selfChange, uri)
+ cachedTagLevels.clear()
+ }
+ }
+ )
}
- /**
- * Whether [bufferName] should echo messages of [level] or higher to logcat.
- */
+ /** Whether [bufferName] should echo messages of [level] or higher to logcat. */
@Synchronized
override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean {
return level.ordinal >= getLogLevel(bufferName, BUFFER_PATH, cachedBufferLevels).ordinal
}
- /**
- * Whether [tagName] should echo messages of [level] or higher to logcat.
- */
+ /** Whether [tagName] should echo messages of [level] or higher to logcat. */
@Synchronized
override fun isTagLoggable(tagName: String, level: LogLevel): Boolean {
return level >= getLogLevel(tagName, TAG_PATH, cachedTagLevels)
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
similarity index 89%
rename from packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
index 1a4ad19..3c8bda4 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
@@ -14,11 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.log
+package com.android.systemui.plugins.log
-/**
- * Production version of [LogcatEchoTracker] that isn't configurable.
- */
+/** Production version of [LogcatEchoTracker] that isn't configurable. */
class LogcatEchoTrackerProd : LogcatEchoTracker {
override val logInBackgroundThread = false
diff --git a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
similarity index 82%
rename from packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
index 97dc842..68d7890 100644
--- a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.util.collection
+package com.android.systemui.plugins.util
import kotlin.math.max
@@ -32,19 +32,16 @@
* @param factory A function that creates a fresh instance of T. Used by the buffer while it's
* growing to [maxSize].
*/
-class RingBuffer<T>(
- private val maxSize: Int,
- private val factory: () -> T
-) : Iterable<T> {
+class RingBuffer<T>(private val maxSize: Int, private val factory: () -> T) : Iterable<T> {
private val buffer = MutableList<T?>(maxSize) { null }
/**
* An abstract representation that points to the "end" of the buffer. Increments every time
- * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into
- * the backing array. Always points to the "next" available slot in the buffer. Before the
- * buffer has completely filled, the value pointed to will be null. Afterward, it will be the
- * value at the "beginning" of the buffer.
+ * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into the
+ * backing array. Always points to the "next" available slot in the buffer. Before the buffer
+ * has completely filled, the value pointed to will be null. Afterward, it will be the value at
+ * the "beginning" of the buffer.
*
* This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms,
* omega will overflow after a little under three million years of continuous operation.
@@ -60,24 +57,23 @@
/**
* Advances the buffer's position by one and returns the value that is now present at the "end"
- * of the buffer. If the buffer is not yet full, uses [factory] to create a new item.
- * Otherwise, reuses the value that was previously at the "beginning" of the buffer.
+ * of the buffer. If the buffer is not yet full, uses [factory] to create a new item. Otherwise,
+ * reuses the value that was previously at the "beginning" of the buffer.
*
- * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that
- * was previously stored on it.
+ * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that was
+ * previously stored on it.
*/
fun advance(): T {
val index = indexOf(omega)
omega += 1
- val entry = buffer[index] ?: factory().also {
- buffer[index] = it
- }
+ val entry = buffer[index] ?: factory().also { buffer[index] = it }
return entry
}
/**
* Returns the value stored at [index], which can range from 0 (the "start", or oldest element
- * of the buffer) to [size] - 1 (the "end", or newest element of the buffer).
+ * of the buffer) to [size]
+ * - 1 (the "end", or newest element of the buffer).
*/
operator fun get(index: Int): T {
if (index < 0 || index >= size) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt
similarity index 74%
rename from packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
rename to packages/SystemUI/plugin/tests/log/LogBufferTest.kt
index 56aff3c..a39b856 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
+++ b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt
@@ -2,6 +2,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.log.LogBuffer
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
@@ -18,8 +19,7 @@
private lateinit var outputWriter: StringWriter
- @Mock
- private lateinit var logcatEchoTracker: LogcatEchoTracker
+ @Mock private lateinit var logcatEchoTracker: LogcatEchoTracker
@Before
fun setup() {
@@ -67,15 +67,17 @@
@Test
fun dump_writesCauseAndStacktrace() {
buffer = createBuffer()
- val exception = createTestException("Exception message",
+ val exception =
+ createTestException(
+ "Exception message",
"TestClass",
- cause = createTestException("The real cause!", "TestClass"))
+ cause = createTestException("The real cause!", "TestClass")
+ )
buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
val dumpedString = dumpBuffer()
- assertThat(dumpedString)
- .contains("Caused by: java.lang.RuntimeException: The real cause!")
+ assertThat(dumpedString).contains("Caused by: java.lang.RuntimeException: The real cause!")
assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:1)")
assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:2)")
}
@@ -85,49 +87,47 @@
buffer = createBuffer()
val exception = RuntimeException("Root exception message")
exception.addSuppressed(
- createTestException(
- "First suppressed exception",
- "FirstClass",
- createTestException("Cause of suppressed exp", "ThirdClass")
- ))
- exception.addSuppressed(
- createTestException("Second suppressed exception", "SecondClass"))
+ createTestException(
+ "First suppressed exception",
+ "FirstClass",
+ createTestException("Cause of suppressed exp", "ThirdClass")
+ )
+ )
+ exception.addSuppressed(createTestException("Second suppressed exception", "SecondClass"))
buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
val dumpedStr = dumpBuffer()
// first suppressed exception
assertThat(dumpedStr)
- .contains("Suppressed: " +
- "java.lang.RuntimeException: First suppressed exception")
+ .contains("Suppressed: " + "java.lang.RuntimeException: First suppressed exception")
assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:1)")
assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:2)")
assertThat(dumpedStr)
- .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp")
+ .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp")
assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:1)")
assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:2)")
// second suppressed exception
assertThat(dumpedStr)
- .contains("Suppressed: " +
- "java.lang.RuntimeException: Second suppressed exception")
+ .contains("Suppressed: " + "java.lang.RuntimeException: Second suppressed exception")
assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:1)")
assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:2)")
}
private fun createTestException(
- message: String,
- errorClass: String,
- cause: Throwable? = null,
+ message: String,
+ errorClass: String,
+ cause: Throwable? = null,
): Exception {
val exception = RuntimeException(message, cause)
- exception.stackTrace = (1..5).map { lineNumber ->
- StackTraceElement(errorClass,
- "TestMethod",
- "$errorClass.java",
- lineNumber)
- }.toTypedArray()
+ exception.stackTrace =
+ (1..5)
+ .map { lineNumber ->
+ StackTraceElement(errorClass, "TestMethod", "$errorClass.java", lineNumber)
+ }
+ .toTypedArray()
return exception
}
diff --git a/packages/SystemUI/res-keyguard/drawable/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 b22655a..443f666 100644
--- a/packages/SystemUI/res-keyguard/values-as/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-as/strings.xml
@@ -64,9 +64,9 @@
<string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"ছিম কার্ড আনলক কৰি থকা হৈছে…"</string>
<string name="kg_invalid_sim_pin_hint" msgid="2762202646949552978">"৪টাৰ পৰা ৮টা সংখ্যাযুক্ত এটা পিন লিখক।"</string>
<string name="kg_invalid_sim_puk_hint" msgid="5319756880543857694">"PUK ক\'ডটো ৮টা বা তাতকৈ অধিক সংখ্যা থকা হ\'ব লাগিব।"</string>
- <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"আপুনি আপোনাৰ পিন <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ ভুলকৈ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>ছেকেণ্ডৰ পিছত আকৌ চেষ্টা কৰক।"</string>
+ <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"আপুনি আপোনাৰ পিন <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ ভুলকৈ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>ছেকেণ্ডৰ পাছত আকৌ চেষ্টা কৰক।"</string>
<string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"আপুনি আপোনাৰ পাছৱৰ্ড <xliff:g id="NUMBER_0">%1$d</xliff:g>বাৰ ভুলকৈ লিখিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g> ছেকেণ্ডৰ পাছত আকৌ চেষ্টা কৰক।"</string>
- <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"আপুনি আপোনাৰ আনলক আৰ্হি <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ ভুলকৈ আঁকিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>ছেকেণ্ডৰ পিছত আকৌ চেষ্টা কৰক।"</string>
+ <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"আপুনি আপোনাৰ আনলক আৰ্হি <xliff:g id="NUMBER_0">%1$d</xliff:g> বাৰ ভুলকৈ আঁকিছে। \n\n<xliff:g id="NUMBER_1">%2$d</xliff:g>ছেকেণ্ডৰ পাছত আকৌ চেষ্টা কৰক।"</string>
<string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"ছিমৰ ভুল পিন ক\'ড, আপোনাৰ ডিভাইচটো আনলক কৰিবলৈ আপুনি এতিয়া আপোনাৰ বাহকৰ সৈতে যোগাযোগ কৰিবই লাগিব।"</string>
<string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{ছিমৰ পিন ক’ড ভুল হৈছে, আপোনাৰ ডিভাইচ আনলক কৰিবলৈ আপোনাৰ বাহকৰ লগত যোগাযোগ কৰিবই লগা হোৱাৰ পূৰ্বে আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে।}one{ছিমৰ পিন ক’ড ভুল হৈছে, আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে। }other{ছিমৰ পিন ক’ড ভুল হৈছে, আপোনাৰ ওচৰত # টা প্ৰয়াস বাকী আছে। }}"</string>
<string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"ছিম ব্যৱহাৰযোগ্য নহয়। আপোনাৰ বাহকৰ সৈতে যোগাযোগ কৰক।"</string>
@@ -75,12 +75,15 @@
<string name="kg_password_puk_failed" msgid="6778867411556937118">"ছিম PUKৰ জৰিয়তে আনলক কৰিব পৰা নগ\'ল!"</string>
<string name="accessibility_ime_switch_button" msgid="9082358310194861329">"ইনপুট পদ্ধতি সলনি কৰক"</string>
<string name="airplane_mode" msgid="2528005343938497866">"এয়াৰপ্লে’ন ম’ড"</string>
- <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_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>
+ <!-- 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-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml
index d90156d..a129fb6 100644
--- a/packages/SystemUI/res-keyguard/values/strings.xml
+++ b/packages/SystemUI/res-keyguard/values/strings.xml
@@ -201,13 +201,13 @@
<string name="kg_prompt_reason_restart_password">Password required after device restarts</string>
<!-- An explanation text that the pattern needs to be solved since the user hasn't used strong authentication since quite some time. [CHAR LIMIT=80] -->
- <string name="kg_prompt_reason_timeout_pattern">Pattern required for additional security</string>
+ <string name="kg_prompt_reason_timeout_pattern">For additional security, use pattern instead</string>
<!-- An explanation text that the pin needs to be entered since the user hasn't used strong authentication since quite some time. [CHAR LIMIT=80] -->
- <string name="kg_prompt_reason_timeout_pin">PIN required for additional security</string>
+ <string name="kg_prompt_reason_timeout_pin">For additional security, use PIN instead</string>
<!-- An explanation text that the password needs to be entered since the user hasn't used strong authentication since quite some time. [CHAR LIMIT=80] -->
- <string name="kg_prompt_reason_timeout_password">Password required for additional security</string>
+ <string name="kg_prompt_reason_timeout_password">For additional security, use password instead</string>
<!-- An explanation text that the credential needs to be entered because a device admin has
locked the device. [CHAR LIMIT=80] -->
@@ -241,4 +241,6 @@
<string name="clock_title_bubble">Bubble</string>
<!-- Name of the "Analog" clock face [CHAR LIMIT=15]-->
<string name="clock_title_analog">Analog</string>
+ <!-- Title of bouncer when we want to authenticate before continuing with action. [CHAR LIMIT=NONE] -->
+ <string name="keyguard_unlock_to_continue">Unlock your device to continue</string>
</resources>
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/bg_smartspace_media_item.xml b/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
index 6939084..33c68bf1 100644
--- a/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
+++ b/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
@@ -16,6 +16,6 @@
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
- <solid android:color="@android:color/white" />
+ <solid android:color="@android:color/transparent" />
<corners android:radius="@dimen/qs_media_album_radius" />
</shape>
\ No newline at end of file
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-keyguard/drawable/media_squiggly_progress.xml b/packages/SystemUI/res/drawable/overlay_badge_background.xml
similarity index 67%
copy from packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
copy to packages/SystemUI/res/drawable/overlay_badge_background.xml
index 9e61236..857632e 100644
--- a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
+++ b/packages/SystemUI/res/drawable/overlay_badge_background.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2022 The Android Open Source Project
+ ~ 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.
@@ -14,4 +14,8 @@
~ 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
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="oval">
+ <solid android:color="?androidprv:attr/colorSurface"/>
+</shape>
diff --git a/packages/SystemUI/res/drawable/qs_media_background.xml b/packages/SystemUI/res/drawable/qs_media_background.xml
index 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/media_ttt_chip.xml b/packages/SystemUI/res/layout/chipbar.xml
similarity index 90%
rename from packages/SystemUI/res/layout/media_ttt_chip.xml
rename to packages/SystemUI/res/layout/chipbar.xml
index ae8e38e..bc97e51 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip.xml
+++ b/packages/SystemUI/res/layout/chipbar.xml
@@ -16,15 +16,15 @@
<!-- Wrap in a frame layout so that we can update the margins on the inner layout. (Since this view
is the root view of a window, we cannot change the root view's margins.) -->
<!-- Alphas start as 0 because the view will be animated in. -->
-<com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView
+<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"
@@ -97,4 +97,4 @@
/>
</LinearLayout>
-</com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView>
+</com.android.systemui.temporarydisplay.chipbar.ChipbarRootView>
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index 1a1fc75..0e9abee 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -14,7 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<com.android.systemui.screenshot.DraggableConstraintLayout
+<com.android.systemui.clipboardoverlay.ClipboardOverlayView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -157,4 +157,4 @@
android:layout_margin="@dimen/overlay_dismiss_button_margin"
android:src="@drawable/overlay_cancel"/>
</FrameLayout>
-</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file
+</com.android.systemui.clipboardoverlay.ClipboardOverlayView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml b/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml
new file mode 100644
index 0000000..1a1fc75
--- /dev/null
+++ b/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<com.android.systemui.screenshot.DraggableConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/clipboard_ui"
+ android:theme="@style/FloatingOverlay"
+ android:alpha="0"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/clipboard_overlay_window_name">
+ <ImageView
+ android:id="@+id/actions_container_background"
+ android:visibility="gone"
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ android:elevation="4dp"
+ android:background="@drawable/action_chip_container_background"
+ android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+ app:layout_constraintBottom_toBottomOf="@+id/actions_container"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="@+id/actions_container"
+ app:layout_constraintEnd_toEndOf="@+id/actions_container"/>
+ <HorizontalScrollView
+ android:id="@+id/actions_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
+ android:paddingEnd="@dimen/overlay_action_container_padding_right"
+ android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
+ android:elevation="4dp"
+ android:scrollbars="none"
+ android:layout_marginBottom="4dp"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintWidth_percent="1.0"
+ app:layout_constraintWidth_max="wrap"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/preview_border"
+ app:layout_constraintEnd_toEndOf="parent">
+ <LinearLayout
+ android:id="@+id/actions"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true">
+ <include layout="@layout/overlay_action_chip"
+ android:id="@+id/share_chip"/>
+ <include layout="@layout/overlay_action_chip"
+ android:id="@+id/remote_copy_chip"/>
+ <include layout="@layout/overlay_action_chip"
+ android:id="@+id/edit_chip"/>
+ </LinearLayout>
+ </HorizontalScrollView>
+ <View
+ android:id="@+id/preview_border"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="@dimen/overlay_offset_x"
+ android:layout_marginBottom="12dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:elevation="7dp"
+ app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end"
+ app:layout_constraintTop_toTopOf="@id/clipboard_preview_top"
+ android:background="@drawable/overlay_border"/>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/clipboard_preview_end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:barrierMargin="@dimen/overlay_border_width"
+ app:barrierDirection="end"
+ app:constraint_referenced_ids="clipboard_preview"/>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/clipboard_preview_top"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:barrierDirection="top"
+ app:barrierMargin="@dimen/overlay_border_width_neg"
+ app:constraint_referenced_ids="clipboard_preview"/>
+ <FrameLayout
+ android:id="@+id/clipboard_preview"
+ android:elevation="7dp"
+ android:background="@drawable/overlay_preview_background"
+ android:clipChildren="true"
+ android:clipToOutline="true"
+ android:clipToPadding="true"
+ android:layout_width="@dimen/clipboard_preview_size"
+ android:layout_margin="@dimen/overlay_border_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ app:layout_constraintBottom_toBottomOf="@id/preview_border"
+ app:layout_constraintStart_toStartOf="@id/preview_border"
+ app:layout_constraintEnd_toEndOf="@id/preview_border"
+ app:layout_constraintTop_toTopOf="@id/preview_border">
+ <TextView android:id="@+id/text_preview"
+ android:textFontWeight="500"
+ android:padding="8dp"
+ android:gravity="center|start"
+ android:ellipsize="end"
+ android:autoSizeTextType="uniform"
+ android:autoSizeMinTextSize="@dimen/clipboard_overlay_min_font"
+ android:autoSizeMaxTextSize="@dimen/clipboard_overlay_max_font"
+ android:textColor="?attr/overlayButtonTextColor"
+ android:textColorLink="?attr/overlayButtonTextColor"
+ android:background="?androidprv:attr/colorAccentSecondary"
+ android:layout_width="@dimen/clipboard_preview_size"
+ android:layout_height="@dimen/clipboard_preview_size"/>
+ <ImageView
+ android:id="@+id/image_preview"
+ android:scaleType="fitCenter"
+ android:adjustViewBounds="true"
+ android:contentDescription="@string/clipboard_image_preview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ <TextView
+ android:id="@+id/hidden_preview"
+ android:visibility="gone"
+ android:textFontWeight="500"
+ android:padding="8dp"
+ android:gravity="center"
+ android:textSize="14sp"
+ android:textColor="?attr/overlayButtonTextColor"
+ android:background="?androidprv:attr/colorAccentSecondary"
+ android:layout_width="@dimen/clipboard_preview_size"
+ android:layout_height="@dimen/clipboard_preview_size"/>
+ </FrameLayout>
+ <FrameLayout
+ android:id="@+id/dismiss_button"
+ android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
+ android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
+ android:elevation="10dp"
+ android:visibility="gone"
+ android:alpha="0"
+ app:layout_constraintStart_toEndOf="@id/clipboard_preview"
+ app:layout_constraintEnd_toEndOf="@id/clipboard_preview"
+ app:layout_constraintTop_toTopOf="@id/clipboard_preview"
+ app:layout_constraintBottom_toTopOf="@id/clipboard_preview"
+ android:contentDescription="@string/clipboard_dismiss_description">
+ <ImageView
+ android:id="@+id/dismiss_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="@dimen/overlay_dismiss_button_margin"
+ android:src="@drawable/overlay_cancel"/>
+ </FrameLayout>
+</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml
index 5dc34b9..a565988 100644
--- a/packages/SystemUI/res/layout/combined_qs_header.xml
+++ b/packages/SystemUI/res/layout/combined_qs_header.xml
@@ -73,8 +73,8 @@
android:singleLine="true"
android:textDirection="locale"
android:textAppearance="@style/TextAppearance.QS.Status"
- android:transformPivotX="0sp"
- android:transformPivotY="20sp"
+ android:transformPivotX="0dp"
+ android:transformPivotY="24dp"
android:scaleX="1"
android:scaleY="1"
/>
diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml
index 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/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
index 2fb6d6c..9fc3f40 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
@@ -23,6 +23,7 @@
android:layout_height="wrap_content"
android:layout_gravity="@integer/notification_panel_layout_gravity"
android:background="@android:color/transparent"
+ android:importantForAccessibility="no"
android:baselineAligned="false"
android:clickable="false"
android:clipChildren="false"
@@ -56,7 +57,7 @@
android:clipToPadding="false"
android:focusable="true"
android:paddingBottom="@dimen/qqs_layout_padding_bottom"
- android:importantForAccessibility="yes">
+ android:importantForAccessibility="no">
</com.android.systemui.qs.QuickQSPanel>
</RelativeLayout>
diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
index 60bc373..8b5d953 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
@@ -25,6 +25,7 @@
android:gravity="center"
android:layout_gravity="top"
android:orientation="horizontal"
+ android:importantForAccessibility="no"
android:clickable="true"
android:minHeight="48dp">
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index 9c02749..1ac78d4 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -103,8 +103,18 @@
app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
app:layout_constraintStart_toStartOf="@id/screenshot_preview_border"
app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"
- app:layout_constraintTop_toTopOf="@id/screenshot_preview_border">
- </ImageView>
+ app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"/>
+ <ImageView
+ android:id="@+id/screenshot_badge"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:padding="4dp"
+ android:visibility="gone"
+ android:background="@drawable/overlay_badge_background"
+ android:elevation="8dp"
+ android:src="@drawable/overlay_cancel"
+ app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
+ app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/>
<FrameLayout
android:id="@+id/screenshot_dismiss_button"
android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index f0e49d5..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"
@@ -75,12 +42,6 @@
android:clipToPadding="false"
android:clipChildren="false">
- <ViewStub
- android:id="@+id/qs_header_stub"
- android:layout_height="wrap_content"
- android:layout_width="match_parent"
- />
-
<include
layout="@layout/keyguard_status_view"
android:visibility="gone"/>
@@ -102,6 +63,15 @@
systemui:layout_constraintBottom_toBottomOf="parent"
/>
+ <!-- This view should be after qs_frame so touches are dispatched first to it. That gives
+ it a chance to capture clicks before the NonInterceptingScrollView disallows all
+ intercepts -->
+ <ViewStub
+ android:id="@+id/qs_header_stub"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ />
+
<androidx.constraintlayout.widget.Guideline
android:id="@+id/qs_edge_guideline"
android:layout_width="wrap_content"
@@ -145,6 +115,39 @@
/>
</com.android.systemui.shade.NotificationsQuickSettingsContainer>
+ <include
+ layout="@layout/keyguard_bottom_area"
+ android:visibility="gone" />
+
+ <ViewStub
+ android:id="@+id/keyguard_user_switcher_stub"
+ android:layout="@layout/keyguard_user_switcher"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent" />
+
+ <include layout="@layout/dock_info_bottom_area_overlay" />
+
+ <com.android.keyguard.LockIconView
+ android:id="@+id/lock_icon_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <!-- Background protection -->
+ <ImageView
+ android:id="@+id/lock_icon_bg"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/fingerprint_bg"
+ android:visibility="invisible"/>
+
+ <ImageView
+ android:id="@+id/lock_icon"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:scaleType="centerCrop"/>
+
+ </com.android.keyguard.LockIconView>
+
<FrameLayout
android:id="@+id/preview_container"
android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/values-am/strings.xml b/packages/SystemUI/res/values-am/strings.xml
index f07f10d..eccce2f 100644
--- a/packages/SystemUI/res/values-am/strings.xml
+++ b/packages/SystemUI/res/values-am/strings.xml
@@ -94,19 +94,14 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"ለአንድ የማያ ገጽ ቀረጻ ክፍለ-ጊዜ በመካሄድ ያለ ማሳወቂያ"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"መቅረጽ ይጀመር?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"እየቀረጹ ሳለ የAndroid ስርዓት በማያ ገጽዎ ላይ የሚታይ ወይም በመሣሪያዎ ላይ የሚጫወት ማንኛውም ሚስጥራዊነት ያለው መረጃን መያዝ ይችላል። ይህ የይለፍ ቃላትን፣ የክፍያ መረጃን፣ ፎቶዎችን፣ መልዕክቶችን እና ኦዲዮን ያካትታል።"</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"መላው ማያ ገጹን ቅረጽ"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"አንድ ነጠላ መተግበሪያን ቅረጽ"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"እየቀረጹ እያለ Android በማያ ገጽዎ ላይ ለሚታይ ወይም በመሣሪያዎ ላይ ለሚጫወት ማንኛውም ነገር መዳረሻ አለው። ስለዚህ በይለፍ ቃላት፣ በክፍያ ዝርዝሮች፣ በመልዕክቶች ወይም በሌሎች ልዩ ጥንቃቄ የሚያስፈልጋቸው መረጃዎች ላይ ጥንቃቄ ያድርጉ።"</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"አንድን መተግበሪያ እየቀረጹ ሳለ Android በዚያ መተግበሪያ ላይ ለሚታይ ወይም ለሚጫወት ማንኛውም ነገር መዳረሻ አለው። ስለዚህ በይለፍ ቃላት፣ በክፍያ ዝርዝሮች፣ በመልዕክቶች ወይም በሌሎች ልዩ ጥንቃቄ የሚያስፈልጋቸው መረጃዎች ላይ ጥንቃቄ ያድርጉ።"</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"መቅረጽ ጀምር"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"ኦዲዮን ቅረጽ"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"የመሣሪያ ኦዲዮ"</string>
- <string name="screenrecord_device_audio_description" msgid="4922694220572186193">"እንደ ሙዚቃ፣ ጥሪዎች እና የጥሪ ቅላጼዎች ያሉ የመሣሪያዎ ድምጽ"</string>
+ <string name="screenrecord_device_audio_description" msgid="4922694220572186193">"እንደ ሙዚቃ፣ ጥሪዎች እና የጥሪ ቅላጼዎች ያሉ የመሣሪያዎ ድምፅ"</string>
<string name="screenrecord_mic_label" msgid="2111264835791332350">"ማይክሮፎን"</string>
<string name="screenrecord_device_audio_and_mic_label" msgid="1831323771978646841">"የመሣሪያ ኦዲዮ እና ማይክሮፎን"</string>
<string name="screenrecord_start" msgid="330991441575775004">"ጀምር"</string>
@@ -274,14 +269,14 @@
<string name="quick_settings_cellular_detail_data_warning" msgid="7957253810481086455">"የ<xliff:g id="DATA_LIMIT">%s</xliff:g> ማስጠንቀቂያ"</string>
<string name="quick_settings_work_mode_label" msgid="6440531507319809121">"የሥራ መተግበሪያዎች"</string>
<string name="quick_settings_night_display_label" msgid="8180030659141778180">"የምሽት ብርሃን"</string>
- <string name="quick_settings_night_secondary_label_on_at_sunset" msgid="3358706312129866626">"ጸሐይ ስትጠልቅ ይበራል"</string>
- <string name="quick_settings_night_secondary_label_until_sunrise" msgid="4063448287758262485">"ጸሐይ እስክትወጣ ድረስ"</string>
+ <string name="quick_settings_night_secondary_label_on_at_sunset" msgid="3358706312129866626">"ፀሐይ ስትጠልቅ ይበራል"</string>
+ <string name="quick_settings_night_secondary_label_until_sunrise" msgid="4063448287758262485">"ፀሐይ እስክትወጣ ድረስ"</string>
<string name="quick_settings_night_secondary_label_on_at" msgid="3584738542293528235">"<xliff:g id="TIME">%s</xliff:g> ላይ ይበራል"</string>
<string name="quick_settings_secondary_label_until" msgid="1883981263191927372">"እስከ <xliff:g id="TIME">%s</xliff:g> ድረስ"</string>
<string name="quick_settings_ui_mode_night_label" msgid="1398928270610780470">"ጨለማ ገጽታ"</string>
<string name="quick_settings_dark_mode_secondary_label_battery_saver" msgid="4990712734503013251">"ባትሪ ቆጣቢ"</string>
- <string name="quick_settings_dark_mode_secondary_label_on_at_sunset" msgid="6017379738102015710">"ጸሐይ ስትጠልቅ ይበራል"</string>
- <string name="quick_settings_dark_mode_secondary_label_until_sunrise" msgid="4404885070316716472">"ጸሐይ እስክትወጣ ድረስ"</string>
+ <string name="quick_settings_dark_mode_secondary_label_on_at_sunset" msgid="6017379738102015710">"ፀሐይ ስትጠልቅ ይበራል"</string>
+ <string name="quick_settings_dark_mode_secondary_label_until_sunrise" msgid="4404885070316716472">"ፀሐይ እስክትወጣ ድረስ"</string>
<string name="quick_settings_dark_mode_secondary_label_on_at" msgid="5128758823486361279">"<xliff:g id="TIME">%s</xliff:g> ላይ ይበራል"</string>
<string name="quick_settings_dark_mode_secondary_label_until" msgid="2289774641256492437">"እስከ <xliff:g id="TIME">%s</xliff:g> ድረስ"</string>
<string name="quick_settings_dark_mode_secondary_label_on_at_bedtime" msgid="2274300599408864897">"በመኝታ ሰዓት ላይ"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"ይህን ተግባር የሚያቀርበው አገልግሎት በእርስዎ ማያ ገጽ ላይ ያለን ወይም በእርስዎ መሣሪያ ላይ በመጫወት ላይ ያለን ሁሉንም መረጃ በቀረጻ ወይም casting ላይ እያለ መዳረሻ ይኖረዋል። ይህ እንደ የይለፍ ቃላት፣ የክፍያ ዝርዝሮች፣ ፎቶዎች፣ መልዕክቶች እና እርስዎ የሚጫውቱት ኦዲዮን የመሳሰለ መረጃን ያካትታል።"</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"ቀረጻ ወይም cast ማድረግ ይጀምር?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"ከ<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ጋር ቀረጻ ወይም casting ይጀምር?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> እንዲያጋራ ወይም እንዲቀርጽ ይፈቀድለት?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"መላው ማያ ገጽ"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"አንድ ነጠላ መተግበሪያ"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"ሲያጋሩ፣ ሲቀርጹ ወይም cast ሲያደርጉ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> በማያ ገጽዎ ላይ ለሚታይ ወይም በመሣሪያዎ ላይ ለሚጫወት ማንኛውም ነገር መዳረሻ አለው። ስለዚህ በይለፍ ቃላት፣ በክፍያ ዝርዝሮች፣ በመልዕክቶች ወይም በሌሎች ልዩ ጥንቃቄ የሚያስፈልጋቸው መረጃዎች ላይ ጥንቃቄ ያድርጉ።"</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"አንድን መተግበሪያ ሲያጋሩ፣ ሲቀርጹ ወይም cast ሲያደርጉ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> በዚያ መተግበሪያ ላይ ለሚታይ ወይም ለሚጫወት ማንኛውም ነገር መዳረሻ አለው። ስለዚህ በይለፍ ቃላት፣ በክፍያ ዝርዝሮች፣ በመልዕክቶች ወይም በሌሎች ልዩ ጥንቃቄ የሚያስፈልጋቸው መረጃዎች ላይ ጥንቃቄ ያድርጉ።"</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ቀጥል"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"መተግበሪያ ያጋሩ ወይም ይቅረጹ"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"ሁሉንም አጽዳ"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"ያቀናብሩ"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"ታሪክ"</string>
@@ -445,7 +433,7 @@
<string name="volume_odi_captions_content_description" msgid="4172765742046013630">"የሥዕል መግለጫ ጽሑፎች ንብርብር"</string>
<string name="volume_odi_captions_hint_enable" msgid="2073091194012843195">"አንቃ"</string>
<string name="volume_odi_captions_hint_disable" msgid="2518846326748183407">"አሰናክል"</string>
- <string name="sound_settings" msgid="8874581353127418308">"ድምጽ እና ንዝረት"</string>
+ <string name="sound_settings" msgid="8874581353127418308">"ድምፅ እና ንዝረት"</string>
<string name="volume_panel_dialog_settings_button" msgid="2513228491513390310">"ቅንብሮች"</string>
<string name="screen_pinning_title" msgid="9058007390337841305">"መተግበሪያ ተሰክቷል"</string>
<string name="screen_pinning_description" msgid="8699395373875667743">"ይሄ እስኪነቅሉት ድረስ በእይታ ውስጥ ያስቀምጠዋል። ለመንቀል ተመለስ እና አጠቃላይ ዕይታ የሚለውን ይጫኑ እና ይያዙ።"</string>
@@ -527,11 +515,11 @@
<string name="notification_silence_title" msgid="8608090968400832335">"ፀጥ ያለ"</string>
<string name="notification_alert_title" msgid="3656229781017543655">"ነባሪ"</string>
<string name="notification_automatic_title" msgid="3745465364578762652">"ራስ-ሰር"</string>
- <string name="notification_channel_summary_low" msgid="4860617986908931158">"ምንም ድምጽ ወይም ንዝረት የለም"</string>
- <string name="notification_conversation_summary_low" msgid="1734433426085468009">"ምንም ድምጽ ወይም ንዝረት የለም እና በውይይት ክፍል ላይ አይታይም"</string>
+ <string name="notification_channel_summary_low" msgid="4860617986908931158">"ምንም ድምፅ ወይም ንዝረት የለም"</string>
+ <string name="notification_conversation_summary_low" msgid="1734433426085468009">"ምንም ድምፅ ወይም ንዝረት የለም እና በውይይት ክፍል ላይ አይታይም"</string>
<string name="notification_channel_summary_default" msgid="3282930979307248890">"በእርስዎ የስልክ ቅንብሮች የሚወሰን ሆኖ ሊደውል ወይም ሊነዝር ይችላል"</string>
<string name="notification_channel_summary_default_with_bubbles" msgid="1782419896613644568">"በእርስዎ የስልክ ቅንብሮች የሚወሰን ሆኖ ሊደውል ወይም ሊነዝር ይችላል። የ<xliff:g id="APP_NAME">%1$s</xliff:g> አረፋ ውይይቶች በነባሪነት።"</string>
- <string name="notification_channel_summary_automatic" msgid="5813109268050235275">"ይህ ማሳወቂያ ድምጽ ወይም ንዝረት መደረግ ካለበት ስርዓቱ እንዲወሰን ያድርጉት"</string>
+ <string name="notification_channel_summary_automatic" msgid="5813109268050235275">"ይህ ማሳወቂያ ድምፅ ወይም ንዝረት መደረግ ካለበት ስርዓቱ እንዲወሰን ያድርጉት"</string>
<string name="notification_channel_summary_automatic_alerted" msgid="954166812246932240">"<b>ሁኔታ:</b> ለነባሪ ከፍ ተዋውቋል።"</string>
<string name="notification_channel_summary_automatic_silenced" msgid="7403004439649872047">"<b>ሁኔታ:</b> ወደ ዝምታ ዝቅ ተደርጓል"</string>
<string name="notification_channel_summary_automatic_promoted" msgid="1301710305149590426">"<b>ሁኔታ:</b> ክፍተኛ ደረጃ ተሰጥቶታል"</string>
diff --git a/packages/SystemUI/res/values-ar/strings.xml b/packages/SystemUI/res/values-ar/strings.xml
index 732c2e6..8f996fb 100644
--- a/packages/SystemUI/res/values-ar/strings.xml
+++ b/packages/SystemUI/res/values-ar/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"إشعار مستمر لجلسة تسجيل شاشة"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"هل تريد بدء التسجيل؟"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"أثناء التسجيل، يمكن أن يسجّل نظام Android أي معلومات حساسة مرئية على شاشتك أو يتم تشغيلها على جهازك. ويشمل ذلك كلمات المرور ومعلومات الدفع والصور والرسائل والمقاطع الصوتية."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"تسجيل الشاشة بالكامل"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"تسجيل محتوى تطبيق واحد"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"أثناء التسجيل، يمكن لنظام Android الوصول إلى كل العناصر المرئية على شاشتك أو التي يتم تشغيلها على جهازك، لذا يُرجى توخي الحذر بشأن كلمات المرور أو تفاصيل الدفع أو الرسائل أو المعلومات الحساسة الأخرى."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"أثناء تسجيل محتوى أحد التطبيقات، يمكن لنظام Android الوصول إلى كل العناصر المعروضة أو التي يتم تشغيلها في ذلك التطبيق، لذا يُرجى توخي الحذر بشأن كلمات المرور أو تفاصيل الدفع أو الرسائل أو المعلومات الحساسة الأخرى."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"بدء التسجيل"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"تسجيل الصوت"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"صوت الجهاز"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"الصوت من جهازك، مثلاً الموسيقى والمكالمات ونغمات الرنين"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"ستتمكن الخدمة التي تقدّم هذه الوظيفة من الوصول إلى كل المعلومات المرئية لك على الشاشة أو التي يتم تشغيلها على جهازك أثناء التسجيل أو الإرسال. ويشمل ذلك معلومات مثل كلمات المرور وتفاصيل الدفع والصور والرسائل والمقاطع الصوتية التي تشغِّلها."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"هل تريد بدء التسجيل أو الإرسال؟"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"هل تريد بدء التسجيل أو الإرسال باستخدام <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>؟"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"هل تريد السماح لتطبيق <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>بالمشاركة أو التسجيل؟"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"الشاشة بالكامل"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"تطبيق واحد"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"أثناء المشاركة أو التسجيل أو البث، يمكن لتطبيق <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> الوصول إلى كل العناصر المرئية على شاشتك أو التي يتم تشغيلها على جهازك، لذا يُرجى توخي الحذر بشأن كلمات المرور أو تفاصيل الدفع أو الرسائل أو المعلومات الحساسة الأخرى."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"أثناء مشاركة محتوى تطبيق أو تسجيله أو بثه، يمكن لتطبيق <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> الوصول إلى كل العناصر المعروضة أو التي يتم تشغيلها في ذلك التطبيق، لذا يُرجى توخي الحذر بشأن كلمات المرور أو تفاصيل الدفع أو الرسائل أو المعلومات الحساسة الأخرى."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"متابعة"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"مشاركة محتوى تطبيق أو تسجيله"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"محو الكل"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"إدارة"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"السجلّ"</string>
diff --git a/packages/SystemUI/res/values-as/strings.xml b/packages/SystemUI/res/values-as/strings.xml
index c2f8e43..fe95fa0 100644
--- a/packages/SystemUI/res/values-as/strings.xml
+++ b/packages/SystemUI/res/values-as/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"স্ক্রীন ৰেকৰ্ডিং ছেশ্বন চলি থকা সময়ত পোৱা জাননী"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"ৰেকৰ্ড কৰা আৰম্ভ কৰিবনে?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"ৰেকৰ্ড কৰি থাকোঁতে, Android Systemএ আপোনাৰ স্ক্রীনত দৃশ্যমান হোৱা অথবা আপোনাৰ ডিভাইচত প্লে’ হৈ থকা যিকোনো সংবেনদশীল তথ্য কেপচাৰ কৰিব পাৰে। এইটোত পাছৱর্ড, পৰিশোধৰ তথ্য, ফট’, বার্তাসমূহ আৰু অডিঅ’ অন্তর্ভুক্ত হয়।"</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"গোটেই স্ক্ৰীনখন ৰেকৰ্ড কৰক"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"এটা এপ্ ৰেকৰ্ড কৰক"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"আপুনি ৰেকৰ্ড কৰাৰ সময়ত, আপোনাৰ স্ক্ৰীনখনত দৃশ্যমান যিকোনো বস্তু অথবা আপোনাৰ ডিভাইচত প্লে’ কৰা যিকোনো সমললৈ Androidৰ এক্সেছ থাকে। গতিকে, পাছৱৰ্ড, পৰিশোধৰ সবিশেষ, বাৰ্তা অথবা অন্য সংবেদনশীল তথ্যৰ ক্ষেত্ৰত সাৱধান হওক।"</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"আপুনি এপ এপ্ ৰেকৰ্ড কৰাৰ সময়ত সেইটো এপত দৃশ্যমান যিকোনো বস্তু অথবা আপোনাৰ ডিভাইচত প্লে’ কৰা যিকোনো সমললৈ Androidৰ এক্সেছ থাকে। গতিকে, পাছৱৰ্ড, পৰিশোধৰ সবিশেষ, বাৰ্তা অথবা অন্য সংবেদনশীল তথ্যৰ ক্ষেত্ৰত সাৱধান হওক।"</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"ৰেকৰ্ডিং আৰম্ভ কৰক"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"অডিঅ’ ৰেকৰ্ড কৰক"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"ডিভাইচৰ অডিঅ’"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"সংগীত, কল আৰু ৰিংট’নসমূহৰ দৰে আপোনাৰ ডিভাইচৰ পৰা কেপচাৰ কৰিব পৰা ধ্বনি"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"এই সুবিধাটো প্ৰদান কৰা সেৱাটোৱে আপোনাৰ স্ক্ৰীনত দৃশ্যমান হোৱা অথবা ৰেকর্ডিং অথবা কাষ্টিঙৰ সময়ত আপোনাৰ ডিভাইচত প্লে\' কৰা আটাইবোৰ তথ্যলৈ এক্সেছ পাব। এইটোত পাছৱর্ড, পৰিশোধৰ সবিশেষ, ফট\', বার্তাসমূহ আৰু আপুনি প্লে\' কৰা অডিঅ\'ৰ দৰে তথ্য অন্তর্ভুক্ত হয়।"</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"ৰেকর্ডিং অথবা কাষ্টিং আৰম্ভ কৰিবনে?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>ৰ জৰিয়তে ৰেকর্ডিং অথবা কাষ্টিং আৰম্ভ কৰিবনে ?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>ক শ্বেয়াৰ অথবা ৰেকৰ্ড কৰিবলৈ অনুমতি দিবনে?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"সম্পূৰ্ণ স্ক্ৰীন"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"এটা একক এপ্"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"আপুনি শ্বেয়াৰ কৰা, ৰেকৰ্ড কৰা অথবা কাষ্ট কৰাৰ সময়ত, আপোনাৰ স্ক্ৰীনখনত দৃশ্যমান যিকোনো বস্তু অথবা আপোনাৰ ডিভাইচত প্লে’ কৰা যিকোনো সমললৈ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>ৰ এক্সেছ থাকে। গতিকে, পাছৱৰ্ড, পৰিশোধৰ সবিশেষ, বাৰ্তা অথবা অন্য সংবেদনশীল তথ্যৰ ক্ষেত্ৰত সাৱধান হওক।"</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"আপুনি শ্বেয়াৰ কৰা, ৰেকৰ্ড কৰা অথবা কাষ্ট কৰাৰ সময়ত, সেইটো এপত দৃশ্যমান যিকোনো বস্তু অথবা আপোনাৰ ডিভাইচত প্লে’ কৰা যিকোনো সমললৈ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>ৰ এক্সেছ থাকে। গতিকে, পাছৱৰ্ড, পৰিশোধৰ সবিশেষ, বাৰ্তা অথবা অন্য সংবেদনশীল তথ্যৰ ক্ষেত্ৰত সাৱধান হওক।"</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"অব্যাহত ৰাখক"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"এটা এপ্ শ্বেয়াৰ অথবা ৰেকৰ্ড কৰক"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"আটাইবোৰ মচক"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"পৰিচালনা"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"ইতিহাস"</string>
@@ -575,7 +563,7 @@
<string name="keyboard_key_dpad_down" msgid="2110172278574325796">"তললৈ"</string>
<string name="keyboard_key_dpad_left" msgid="8329738048908755640">"বাওঁফালে"</string>
<string name="keyboard_key_dpad_right" msgid="6282105433822321767">"সোঁফালে"</string>
- <string name="keyboard_key_dpad_center" msgid="4079412840715672825">"স্ক্ৰীণৰ মাজত"</string>
+ <string name="keyboard_key_dpad_center" msgid="4079412840715672825">"স্ক্ৰীনৰ মাজত"</string>
<string name="keyboard_key_tab" msgid="4592772350906496730">"Tab"</string>
<string name="keyboard_key_space" msgid="6980847564173394012">"স্পেচ"</string>
<string name="keyboard_key_enter" msgid="8633362970109751646">"এণ্টাৰ"</string>
@@ -596,7 +584,7 @@
<string name="keyboard_key_numpad_template" msgid="7316338238459991821">"নামপেড <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="notif_inline_reply_remove_attachment_description" msgid="7954075334095405429">"সংলগ্নক আঁতৰাওক"</string>
<string name="keyboard_shortcut_group_system" msgid="1583416273777875970">"ছিষ্টেম"</string>
- <string name="keyboard_shortcut_group_system_home" msgid="7465138628692109907">"গৃহ স্ক্ৰীণ"</string>
+ <string name="keyboard_shortcut_group_system_home" msgid="7465138628692109907">"গৃহ স্ক্ৰীন"</string>
<string name="keyboard_shortcut_group_system_recents" msgid="8628108256824616927">"শেহতীয়াসমূহ"</string>
<string name="keyboard_shortcut_group_system_back" msgid="1055709713218453863">"উভতি যাওক"</string>
<string name="keyboard_shortcut_group_system_notifications" msgid="3615971650562485878">"জাননীসমূহ"</string>
@@ -693,7 +681,7 @@
<string name="thermal_shutdown_dialog_help_text" msgid="6413474593462902901">"যত্ন লোৱাৰ পদক্ষেপসমূহ চাওক"</string>
<string name="high_temp_title" msgid="2218333576838496100">"ফ\'নটো গৰম হ\'বলৈ ধৰিছে"</string>
<string name="high_temp_notif_message" msgid="1277346543068257549">"ফ’নটো ঠাণ্ডা হৈ থকাৰ সময়ত কিছুমান সুবিধা উপলব্ধ নহয়।\nঅধিক তথ্যৰ বাবে টিপক"</string>
- <string name="high_temp_dialog_message" msgid="3793606072661253968">"আপোনাৰ ফ\'নটোৱে নিজে নিজে ঠাণ্ডা হ\'বলৈ স্বয়ংক্ৰিয়ভাৱে চেষ্টা কৰিব। আপুনি ফ\'নটো ব্যৱহাৰ কৰি থাকিব পাৰে কিন্তু ই লাহে লাহে চলিব পাৰে।\n\nফ\'নটো সম্পূৰ্ণভাৱে ঠাণ্ডা হোৱাৰ পিছত ই আগৰ নিচিনাকৈয়েই চলিব।"</string>
+ <string name="high_temp_dialog_message" msgid="3793606072661253968">"আপোনাৰ ফ\'নটোৱে নিজে নিজে ঠাণ্ডা হ\'বলৈ স্বয়ংক্ৰিয়ভাৱে চেষ্টা কৰিব। আপুনি ফ\'নটো ব্যৱহাৰ কৰি থাকিব পাৰে কিন্তু ই লাহে লাহে চলিব পাৰে।\n\nফ\'নটো সম্পূৰ্ণভাৱে ঠাণ্ডা হোৱাৰ পাছত ই আগৰ নিচিনাকৈয়েই চলিব।"</string>
<string name="high_temp_dialog_help_text" msgid="7380171287943345858">"যত্ন লোৱাৰ পদক্ষেপসমূহ চাওক"</string>
<string name="high_temp_alarm_title" msgid="8654754369605452169">"আপোনাৰ ডিভাইচটো আনপ্লাগ কৰক"</string>
<string name="high_temp_alarm_notify_message" msgid="3917622943609118956">"আপোনাৰ ডিভাইচটো চাৰ্জিং প’ৰ্টৰ ওচৰত গৰম হৈছে। যদি এইটো কোনো চার্জাৰ অথবা ইউএছবিৰ সহায়ক সামগ্ৰীৰ সৈতে সংযুক্ত হৈ আছে, ইয়াক আনপ্লাগ কৰক আৰু কে’বলডালো গৰম হ\'ব পাৰে, গতিকে যত্ন লওক।"</string>
@@ -722,7 +710,7 @@
<string name="instant_apps" msgid="8337185853050247304">"Instant Apps"</string>
<string name="instant_apps_title" msgid="8942706782103036910">"<xliff:g id="APP">%1$s</xliff:g> চলি আছে"</string>
<string name="instant_apps_message" msgid="6112428971833011754">"এপ্টো ইনষ্ট\'ল নকৰাকৈ খোলা হৈছে।"</string>
- <string name="instant_apps_message_with_help" msgid="1816952263531203932">"ইনষ্ট\'ল নকৰাকৈয়েই এপটো খোলা হৈছে। অধিক জানিবলৈ টিপক।"</string>
+ <string name="instant_apps_message_with_help" msgid="1816952263531203932">"ইনষ্ট\'ল নকৰাকৈয়েই এপ্টো খোলা হৈছে। অধিক জানিবলৈ টিপক।"</string>
<string name="app_info" msgid="5153758994129963243">"এপৰ তথ্য"</string>
<string name="go_to_web" msgid="636673528981366511">"ব্ৰাউজাৰলৈ যাওক"</string>
<string name="mobile_data" msgid="4564407557775397216">"ম’বাইল ডেটা"</string>
diff --git a/packages/SystemUI/res/values-bg/strings.xml b/packages/SystemUI/res/values-bg/strings.xml
index fa6da94..cbc7c8a 100644
--- a/packages/SystemUI/res/values-bg/strings.xml
+++ b/packages/SystemUI/res/values-bg/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Текущо известие за сесия за записване на екрана"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Да се стартира ли записът?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"По време на записване системата Android може да запише и поверителна информация, която е показана на екрана или възпроизвеждана на устройството ви. Това включва пароли, данни за плащане, снимки, съобщения и аудио."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Записване на целия екран"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Записване на едно приложение"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Когато записвате, Android има достъп до всичко, което се вижда на екрана ви или се възпроизвежда на устройството ви, затова бъдете внимателни с пароли, подробности за начини на плащане, съобщения или друга поверителна информация."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Когато записвате приложение, Android има достъп до всичко, което се показва или възпроизвежда в това приложение, затова бъдете внимателни с пароли, подробности за начини на плащане, съобщения или друга поверителна информация."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Стартиране на записа"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Записване на звук"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Аудио от устройството"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Звук от устройството ви, като например музика, обаждания и мелодии"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Услугата, предоставяща тази функция, ще има достъп до цялата информация, която е видима на екрана или възпроизвеждана от устройството ви по време на записване или предаване. Това включва различна информация, като например пароли, данни за плащане, снимки, съобщения и възпроизвеждано аудио."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Да се стартира ли записване или предаване?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Да се стартира ли записване или предаване чрез <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Разрешавате ли на <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> да споделя и записва?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Цял екран"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Едно приложение"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Когато споделяте, записвате или предавате, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> има достъп до всичко, което се вижда на екрана ви или се възпроизвежда на устройството ви, затова бъдете внимателни с пароли, подробности за начини на плащане, съобщения или друга поверителна информация."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Когато споделяте, записвате или предавате, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> има достъп до всичко, което се показва или възпроизвежда в това приложение, затова бъдете внимателни с пароли, подробности за начини на плащане, съобщения или друга поверителна информация."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Напред"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Споделяне или записване на приложение"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Изчистване на всички"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Управление"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"История"</string>
diff --git a/packages/SystemUI/res/values-bn/strings.xml b/packages/SystemUI/res/values-bn/strings.xml
index 8dc5f33..44fb0e6 100644
--- a/packages/SystemUI/res/values-bn/strings.xml
+++ b/packages/SystemUI/res/values-bn/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"স্ক্রিন রেকর্ডিং সেশন চলার বিজ্ঞপ্তি"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"রেকর্ডিং শুরু করবেন?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"রেকর্ড করার সময়, আপনার স্ক্রিনে দেখানো বা ডিভাইসে চালানো যেকোনও ধরনের সংবেদনশীল তথ্য Android সিস্টেম ক্যাপচার করতে পারে। এর মধ্যে পাসওয়ার্ড, পেমেন্টের তথ্য, ফটো, মেসেজ এবং অডিও সম্পর্কিত তথ্য থাকে।"</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"সম্পূর্ণ স্ক্রিন রেকর্ড করুন"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"একটিমাত্র অ্যাপ রেকর্ড করুন"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"আপনার রেকর্ড করার সময়, স্ক্রিনে দেখা যায় বা ডিভাইসে খেলা হয় এমন সব কিছু অ্যাক্সেস করার অনুমতি Android-এর আছে। তাই পাসওয়ার্ড, পেমেন্টের বিবরণ, মেসেজ বা অন্য সংবেদনশীল তথ্য সম্পর্কে সতর্ক থাকুন।"</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"কোনও অ্যাপ আপনার রেকর্ড করার সময়, সেই অ্যাপে দেখা যায় বা খেলা হয় এমন সব কিছু অ্যাক্সেস করার অনুমতি Android-এর আছে। তাই পাসওয়ার্ড, পেমেন্টের বিবরণ, মেসেজ বা অন্য সংবেদনশীল তথ্য সম্পর্কে সতর্ক থাকুন।"</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"রেকর্ড করা শুরু করুন"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"অডিও রেকর্ড করুন"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"ডিভাইস অডিও"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"মিউজিক, কল এবং রিংটোনগুলির মতো আপনার ডিভাইস থেকে সাউন্ড"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"রেকর্ড করা বা কাস্টিং করার সময় আপনার স্ক্রিনে দেখানো বা ডিভাইসে চালানো হয়েছে এমন সমস্ত তথ্যের অ্যাক্সেস এই ফাংশন প্রদানকারী পরিষেবার কাছে থাকবে। এর মধ্যে আপনার পাসওয়ার্ড, পেমেন্টের বিবরণ, ফটো, মেসেজ এবং যে অডিও আপনি চালান সেগুলি সম্পর্কিত তথ্য রয়েছে।"</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"রেকর্ড অথবা কাস্টিং শুরু করতে চান?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> দিয়ে রেকর্ড করা বা কাস্টিং শুরু করবেন?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>-কে শেয়ার বা রেকর্ড করার অনুমতি দেবেন?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"সম্পূর্ণ স্ক্রিন"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"একটি মাত্র অ্যাপ"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"আপনার শেয়ার করা, রেকর্ড করা বা কাস্ট করার সময়, স্ক্রিনে দেখা যায় বা ডিভাইসে খেলা হয় এমন সব কিছু অ্যাক্সেস করার অনুমতি <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>-এর আছে। তাই পাসওয়ার্ড, পেমেন্টের বিবরণ, মেসেজ বা অন্য সংবেদনশীল তথ্য সম্পর্কে সতর্ক থাকুন।"</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"কোনও অ্যাপ আপনার শেয়ার করা, রেকর্ড করা বা কাস্ট করার সময়, সেই অ্যাপে দেখা যায় বা খেলা হয় এমন সব কিছু অ্যাক্সেস করার অনুমতি <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>-এর আছে। তাই পাসওয়ার্ড, পেমেন্টের বিবরণ, মেসেজ বা অন্য সংবেদনশীল তথ্য সম্পর্কে সতর্ক থাকুন।"</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"চালিয়ে যান"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"অ্যাপ শেয়ার বা রেকর্ড করা"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"সবকিছু সাফ করুন"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"পরিচালনা করুন"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"ইতিহাস"</string>
diff --git a/packages/SystemUI/res/values-ca/strings.xml b/packages/SystemUI/res/values-ca/strings.xml
index 5e3fb33..172b14d 100644
--- a/packages/SystemUI/res/values-ca/strings.xml
+++ b/packages/SystemUI/res/values-ca/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Notificació en curs d\'una sessió de gravació de la pantalla"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Vols iniciar la gravació?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Durant la gravació, el sistema Android pot capturar qualsevol informació sensible que es mostri a la pantalla o que es reprodueixi al dispositiu. Això inclou contrasenyes, informació de pagament, fotos, missatges i àudio."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Grava la pantalla completa"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Grava una sola aplicació"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Mentre graves, Android té accés a qualsevol cosa que es vegi a la pantalla o que es reprodueixi al dispositiu. Per aquest motiu, ves amb compte amb les contrasenyes, les dades de pagament, els missatges o altra informació sensible."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Mentre graves una aplicació, Android té accés a qualsevol cosa que es vegi a la pantalla o que es reprodueixi a l\'aplicació. Per aquest motiu, ves amb compte amb les contrasenyes, les dades de pagament, els missatges o altra informació sensible."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Inicia la gravació"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Grava l\'àudio"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Àudio del dispositiu"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"So del dispositiu, com ara música, trucades i sons de trucada"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"El servei que ofereix aquesta funció tindrà accés a tota la informació visible a la teva pantalla o que es reprodueix al dispositiu mentre graves o emets contingut, com ara contrasenyes, detalls dels pagaments, fotos, missatges i àudio que reprodueixis."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Vols començar a gravar o emetre contingut?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Vols començar a gravar o emetre contingut amb <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Vols permetre que <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> comparteixi o gravi?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Tota la pantalla"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Una sola aplicació"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Quan estàs compartint, gravant o emetent, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> té accés a qualsevol cosa que es vegi a la pantalla o que es reprodueixi al dispositiu. Per aquest motiu, ves amb compte amb les contrasenyes, les dades de pagament, els missatges o altra informació sensible."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Quan estàs compartint, gravant o emetent, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> té accés a qualsevol cosa que es vegi a la pantalla o que es reprodueixi a l\'aplicació. Per aquest motiu, ves amb compte amb les contrasenyes, les dades de pagament, els missatges o altra informació sensible."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continua"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Comparteix o grava una aplicació"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Esborra-ho tot"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Gestiona"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Historial"</string>
diff --git a/packages/SystemUI/res/values-cs/strings.xml b/packages/SystemUI/res/values-cs/strings.xml
index b22656c..034fe9a 100644
--- a/packages/SystemUI/res/values-cs/strings.xml
+++ b/packages/SystemUI/res/values-cs/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Trvalé oznámení o relaci nahrávání"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Spustit nahrávání?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Při nahrávání může systém Android zaznamenávat citlivé údaje, které jsou viditelné na obrazovce nebo které jsou přehrávány na zařízení. Týká se to hesel, údajů o platbě, fotek, zpráv a zvuků."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Nahrát celou obrazovku"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Nahrát samostatnou aplikaci"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Během nahrávání má Android přístup k veškerému obsahu, který je viditelný na obrazovce nebo se přehrává v zařízení. Dejte proto pozor na hesla, platební údaje, zprávy nebo jiné citlivé informace."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Během nahrávání aplikace má Android přístup k veškerému obsahu, který je v této aplikaci zobrazen nebo přehráván. Dejte proto pozor na hesla, platební údaje, zprávy nebo jiné citlivé informace."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Spustit nahrávání"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Nahrávat zvuk"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Zvuk zařízení"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Zvuk ze zařízení, například hudba, hovory a vyzvánění"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Služba, která tuto funkci poskytuje, bude mít při nahrávání nebo odesílání přístup ke všem informacím, které jsou viditelné na obrazovce nebo které jsou přehrávány ze zařízení. Týká se to i hesel, údajů o platbě, fotek, zpráv a přehrávaných zvuků."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Začít nahrávat nebo odesílat?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Začít nahrávat nebo odesílat s aplikací <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Povolit aplikaci <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> sdílení nebo nahrávání?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Celá obrazovka"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Samostatná aplikace"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Když sdílíte, nahráváte nebo odesíláte obsah, aplikace <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> má přístup k veškerému obsahu, který je viditelný na obrazovce nebo se přehrává v zařízení. Dejte proto pozor na hesla, platební údaje, zprávy nebo jiné citlivé informace."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Když sdílíte, nahráváte nebo odesíláte aplikaci, aplikace <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> má přístup k veškerému obsahu, který je v této aplikaci zobrazen nebo přehráván. Dejte proto pozor na hesla, platební údaje, zprávy nebo jiné citlivé informace."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Pokračovat"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Sdílení nebo nahrání aplikace"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Smazat vše"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Spravovat"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Historie"</string>
diff --git a/packages/SystemUI/res/values-da/strings.xml b/packages/SystemUI/res/values-da/strings.xml
index 9edb3377..03d12af 100644
--- a/packages/SystemUI/res/values-da/strings.xml
+++ b/packages/SystemUI/res/values-da/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Konstant notifikation om skærmoptagelse"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Vil du starte optagelse?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Når du optager, kan Android-systemet registrere følsomme oplysninger, der er synlige på din skærm, eller som afspilles på din enhed. Dette inkluderer adgangskoder, betalingsoplysninger, fotos, meddelelser og lyd."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Optag hele skærmen"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Optag én app"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Mens du optager, har Android adgang til alt, der er synligt på din skærm eller afspilles på din enhed. Vær derfor forsigtig med adgangskoder, betalingsoplysninger, beskeder og andre følsomme oplysninger."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Mens du optager en app, har Android adgang til alt, der vises eller afspilles i den pågældende app. Vær derfor forsigtig med adgangskoder, betalingsoplysninger, beskeder og andre følsomme oplysninger."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Start optagelse"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Optag lyd"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Enhedslyd"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Lyd fra din enhed såsom musik, opkald og ringetoner"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Tjenesten, der tilbyder denne funktion, får adgang til alle de oplysninger, der er synlige på din skærm, eller som afspilles på din enhed, når du optager eller caster. Dette omfatter oplysninger som f.eks. adgangskoder, betalingsoplysninger, billeder, beskeder og afspillet lyd."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Vil du begynde at optage eller caste?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Vil du begynde at optage eller caste via <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Vil du tillade, at <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> deler eller optager?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Hele skærmen"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Én app"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Når du deler, optager eller caster, har <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> adgang til alt, der er synligt på din skærm eller afspilles på din enhed. Vær derfor forsigtig med adgangskoder, betalingsoplysninger, beskeder og andre følsomme oplysninger."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Når du deler, optager eller caster en app, har <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> adgang til alt, der vises eller afspilles i den pågældende app. Vær derfor forsigtig med adgangskoder, betalingsoplysninger, beskeder og andre følsomme oplysninger."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Fortsæt"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Del eller optag en app"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Ryd alle"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Administrer"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Historik"</string>
diff --git a/packages/SystemUI/res/values-de/strings.xml b/packages/SystemUI/res/values-de/strings.xml
index 7698065..1d13c87 100644
--- a/packages/SystemUI/res/values-de/strings.xml
+++ b/packages/SystemUI/res/values-de/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Fortlaufende Benachrichtigung für eine Bildschirmaufzeichnung"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Aufzeichnung starten?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Beim Aufnehmen kann das Android-System vertrauliche Informationen erfassen, die auf deinem Bildschirm angezeigt oder von deinem Gerät wiedergegeben werden. Das können Passwörter, Zahlungsinformationen, Fotos, Nachrichten und Audioinhalte sein."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Gesamten Bildschirm aufnehmen"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Eine einzelne App aufnehmen"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Während der Aufnahme hat Android Zugriff auf alle Inhalte, die auf dem Bildschirm sichtbar sind oder auf dem Gerät wiedergegeben werden. Sei daher mit Passwörtern, Zahlungsdetails, Nachrichten oder anderen vertraulichen Informationen vorsichtig."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Während der Aufnahme einer App hat Android Zugriff auf alle Inhalte, die in dieser App sichtbar sind oder wiedergegeben werden. Sei daher mit Passwörtern, Zahlungsdetails, Nachrichten oder anderen vertraulichen Informationen vorsichtig."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Aufnahme starten"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Audio aufnehmen"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Audio des Geräts"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Audioinhalte auf deinem Gerät, wie Musik, Anrufe und Klingeltöne"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Der Anbieter dieser App erhält Zugriff auf alle Informationen, die auf deinem Bildschirm sichtbar sind oder von deinem Gerät wiedergegeben werden, während du aufnimmst oder streamst. Dazu gehören beispielsweise angezeigte Passwörter, Zahlungsdetails, Fotos, Nachrichten und Audioinhalte."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Aufnahme oder Stream starten?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Aufnehmen oder Streamen mit der App \"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>\" starten?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Zulassen, dass <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> Inhalte teilt oder aufnimmt?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Gesamter Bildschirm"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Eine einzelne App"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Beim Teilen, Aufnehmen oder Übertragen hat <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> Zugriff auf alle Inhalte, die auf dem Bildschirm sichtbar sind oder auf dem Gerät wiedergegeben werden. Sei daher mit Passwörtern, Zahlungsdetails, Nachrichten oder anderen vertraulichen Informationen vorsichtig."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Beim Teilen, Aufnehmen oder Übertragen einer App hat <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> Zugriff auf alle Inhalte, die in dieser App sichtbar sind oder wiedergegeben werden. Sei daher mit Passwörtern, Zahlungsdetails, Nachrichten oder anderen vertraulichen Informationen vorsichtig."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Weiter"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"App teilen oder aufnehmen"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Alle löschen"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Verwalten"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Verlauf"</string>
diff --git a/packages/SystemUI/res/values-es/strings.xml b/packages/SystemUI/res/values-es/strings.xml
index e9d1534..0df5f16 100644
--- a/packages/SystemUI/res/values-es/strings.xml
+++ b/packages/SystemUI/res/values-es/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Notificación continua de una sesión de grabación de la pantalla"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"¿Empezar a grabar?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Mientras grabas, el sistema Android puede capturar información sensible que se muestre o se reproduzca en tu dispositivo, como contraseñas, datos de pago, fotos, mensajes y audio."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Grabar toda la pantalla"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Grabar una sola aplicación"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Mientras grabes contenido, Android podrá acceder a todo lo que sea visible en tu pantalla o que reproduzcas en tu dispositivo. Debes tener cuidado con contraseñas, detalles de pagos, mensajes o cualquier otra información sensible."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Mientras grabes una aplicación, Android podrá acceder a todo lo que muestre o reproduzca la aplicación. Debes tener cuidado con contraseñas, detalles de pagos, mensajes o cualquier otra información sensible."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Iniciar grabación"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Grabar audio"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Audio del dispositivo"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Sonido de tu dispositivo, como música, llamadas y tonos de llamada"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"El servicio que ofrece esta función tendrá acceso a toda la información que se muestre en la pantalla o se reproduzca en el dispositivo mientras grabas o envías contenido, incluyendo contraseñas, detalles de pagos, fotos, mensajes y audios que reproduzcas."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"¿Empezar a grabar o enviar contenido?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"¿Iniciar grabación o el envío de contenido en <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"¿Permitir que <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> comparta o grabe contenido?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Toda la pantalla"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Una sola aplicación"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Cuando compartas, grabes o envíes contenido, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> podrá acceder a todo lo que sea visible en tu pantalla o que reproduzcas en tu dispositivo. Debes tener cuidado con contraseñas, detalles de pagos, mensajes o cualquier otra información sensible."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Cuando compartas, grabes o envíes una aplicación, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> podrá acceder a todo lo que muestre o reproduzca la aplicación. Debes tener cuidado con contraseñas, detalles de pagos, mensajes o cualquier otra información sensible."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuar"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Compartir o grabar una aplicación"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Borrar todo"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Gestionar"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Historial"</string>
diff --git a/packages/SystemUI/res/values-et/strings.xml b/packages/SystemUI/res/values-et/strings.xml
index fae8364..d42b397 100644
--- a/packages/SystemUI/res/values-et/strings.xml
+++ b/packages/SystemUI/res/values-et/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Pooleli märguanne ekraanikuva salvestamise seansi puhul"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Kas alustada salvestamist?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Salvestamise ajal võib Androidi süsteem jäädvustada tundlikku teavet, mis on ekraanikuval nähtav või mida seadmes esitatakse. See hõlmab paroole, makseteavet, fotosid, sõnumeid ja heli."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Kogu ekraanikuva salvestamine"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Ühe rakenduse salvestamine"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Salvestamise ajal on Androidil juurdepääs kõigele, mis on teie ekraanikuval nähtaval või mida teie seadmes esitatakse. Seega olge paroolide, makseteabe, sõnumite ja muu tundliku teabega ettevaatlik."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Rakenduse salvestamise ajal on Androidil juurdepääs kõigele, mis on selles rakenduses nähtaval või mida selles esitatakse. Seega olge paroolide, makseteabe, sõnumite ja muu tundliku teabega ettevaatlik."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Alusta salvestamist"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Heli salvestamine"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Seadme heli"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Seadmest pärinev heli, nt muusika, kõned ja helinad"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Seda funktsiooni pakkuv teenus saab juurdepääsu kogu teabele, mis on teie ekraanikuval nähtav või mida seadmes salvestamise või ülekande ajal esitatakse. See hõlmab teavet, nagu paroolid, maksete üksikasjad, fotod, sõnumid ja esitatav heli."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Kas alustada salvestamist või ülekannet?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Kas alustada rakendusega <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> salvestamist või ülekannet?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Kas lubada rakendusel <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> jagada või salvestada?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Kogu ekraanikuva"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Üks rakendus"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Kui jagate, salvestate või kannate üle, on rakendusel <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> juurdepääs kõigele, mis on teie ekraanikuval nähtaval või mida teie seadmes esitatakse. Seega olge paroolide, makseteabe, sõnumite ja muu tundliku teabega ettevaatlik."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Kui jagate, salvestate või kannate rakendust üle, on rakendusel <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> juurdepääs kõigele, mida selles rakenduses kuvatakse või esitatakse. Seega olge paroolide, makseteabe, sõnumite ja muu tundliku teabega ettevaatlik."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Jätka"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Rakenduse jagamine või salvestamine"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Tühjenda kõik"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Haldamine"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Ajalugu"</string>
diff --git a/packages/SystemUI/res/values-eu/strings.xml b/packages/SystemUI/res/values-eu/strings.xml
index cf53e32..fc93a80 100644
--- a/packages/SystemUI/res/values-eu/strings.xml
+++ b/packages/SystemUI/res/values-eu/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Pantailaren grabaketa-saioaren jakinarazpen jarraitua"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Grabatzen hasi nahi duzu?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Pantaila grabatzen duzun bitartean, baliteke Android sistemak pantailan agertzen den edo gailuak erreproduzitzen duen kontuzko informazioa grabatzea; besteak beste, pasahitzak, ordainketa-informazioa, argazkiak, mezuak eta audioa."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Grabatu pantaila osoko edukia"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Grabatu aplikazio bakar bat"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Grabatzen ari zarenean, pantailan ikusgai dagoen edo gailuan erreproduzitzen ari den guztirako sarbidea du Android-ek. Beraz, kontuz ibili pasahitzekin, ordainketen xehetasunekin, mezuekin edo bestelako kontuzko informazioarekin."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Aplikazio bat grabatzen ari zarenean, aplikazio horretan ikusgai dagoen edo bertan erreproduzitzen ari den guztirako sarbidea du Android-ek. Beraz, kontuz ibili pasahitzekin, ordainketen xehetasunekin, mezuekin edo bestelako kontuzko informazioarekin."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Hasi grabatzen"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Grabatu audioa"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Gailuaren audioa"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Gailuko soinuak; adibidez, musika, deiak eta tonuak"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Zerbait grabatzen edo igortzen duzunean, pantailan ikus daitekeen edo gailuak erreproduzitzen duen informazio guztia atzitu ahalko du funtzio hori eskaintzen duen zerbitzuak; besteak beste, pasahitzak, ordainketen xehetasunak, argazkiak, mezuak eta erreproduzitzen dituzun audioak."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Grabatzen edo igortzen hasi nahi duzu?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> aplikazioarekin grabatzen edo igortzen hasi nahi duzu?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Edukia partekatu edo grabatzeko baimena eman nahi diozu <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> aplikazioari?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Pantaila osoa"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Aplikazio bakar bat"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Edukia partekatzen, grabatzen edo igortzen ari zarenean, pantailan ikusgai dagoen edo gailuan erreproduzitzen ari den guztirako sarbidea du <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> aplikazioak. Beraz, kontuz ibili pasahitzekin, ordainketen xehetasunekin, mezuekin edo bestelako kontuzko informazioarekin."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Aplikazio bat partekatzen, grabatzen edo igortzen ari zarenean, aplikazio horretan ikusgai dagoen edo bertan erreproduzitzen ari den guztirako sarbidea du <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> aplikazioak. Beraz, kontuz ibili pasahitzekin, ordainketen xehetasunekin, mezuekin edo bestelako kontuzko informazioarekin."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Egin aurrera"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Partekatu edo grabatu aplikazioak"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Garbitu guztiak"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Kudeatu"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Historia"</string>
diff --git a/packages/SystemUI/res/values-fi/strings.xml b/packages/SystemUI/res/values-fi/strings.xml
index 14a65dc..0882342 100644
--- a/packages/SystemUI/res/values-fi/strings.xml
+++ b/packages/SystemUI/res/values-fi/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Pysyvä ilmoitus näytön tallentamisesta"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Aloitetaanko tallennus?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Tallennuksen aikana Android-järjestelmä voi tallentaa mitä tahansa näytöllä näkyvää tai laitteen toistamaa arkaluontoista tietoa. Näitä tietoja ovat esimerkiksi salasanat, maksutiedot, kuvat, viestit ja audio."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Tallenna koko näyttö"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Tallenna yhtä sovellusta"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Kun tallennat, Android saa pääsyn kaikkeen näytölläsi näkyvään tai laitteellasi toistettuun sisältöön. Ole siis varovainen, kun lisäät salasanoja, maksutietoja, viestejä tai muita arkaluontoisia tietoja."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Kun tallennat sovellusta, Android saa pääsyn kaikkeen sovelluksessa näkyvään tai toistettuun sisältöön. Ole siis varovainen, kun lisäät salasanoja, maksutietoja, viestejä tai muita arkaluontoisia tietoja."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Aloita tallennus"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Tallenna audiota"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Laitteen audio"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Musiikki, puhelut, soittoäänet ja muut äänet laitteesta"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Ominaisuuden tarjoavalla palvelulla on pääsy kaikkiin näytölläsi näkyviin tietoihin ja tietoihin laitteesi toistamasta sisällöstä tallennuksen tai striimauksen aikana. Näitä tietoja ovat esimerkiksi salasanat, maksutiedot, kuvat, viestit ja toistettava audiosisältö."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Aloitetaanko tallentaminen tai striimaus?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Haluatko, että <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> aloittaa tallennuksen tai striimauksen?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Sallitaanko, että <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> jaetaan tai tallennetaan?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Koko näyttö"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Yksittäinen sovellus"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Kun jaat, tallennat tai striimaat, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> saa pääsyn kaikkeen näytölläsi näkyvään tai laitteellasi toistettuun sisältöön. Ole siis varovainen, kun lisäät salasanoja, maksutietoja, viestejä tai muita arkaluontoisia tietoja."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Kun jaat, tallennat tai striimaat sovellusta, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> saa pääsyn kaikkeen sovelluksessa näkyvään tai toistettuun sisältöön. Ole siis varovainen, kun lisäät salasanoja, maksutietoja, viestejä tai muita arkaluontoisia tietoja."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Jatka"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Jaa sovellus tai tallenna sen sisältöä"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Tyhjennä kaikki"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Muuta asetuksia"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Historia"</string>
diff --git a/packages/SystemUI/res/values-fr/strings.xml b/packages/SystemUI/res/values-fr/strings.xml
index 9fdae3e..a80b037 100644
--- a/packages/SystemUI/res/values-fr/strings.xml
+++ b/packages/SystemUI/res/values-fr/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Notification en cours pour une session d\'enregistrement de l\'écran"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Démarrer l\'enregistrement ?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Durant l\'enregistrement, le système Android peut capturer les infos sensibles affichées à l\'écran ou lues sur votre appareil. Cela inclut les mots de passe, les infos de paiement, les photos, les messages et l\'audio."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Enregistrer tout l\'écran"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Enregistrer une seule appli"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Lorsque vous enregistrez une appli, Android à accès à tout ce qui est visible sur votre écran ou lu sur votre appareil. Faites donc attention à vos mots de passe, détails de mode de paiement, messages ou autres informations sensibles."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Lorsque vous enregistrez une appli, Android a accès à tout ce qui est visible sur votre écran ou lu sur votre appareil. Faites donc attention à vos mots de passe, détails de mode de paiement, messages ou autres informations sensibles."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Lancer l\'enregistrement"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Enregistrer l\'audio"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Audio de l\'appareil"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Son provenant de l\'appareil (musique, appels et sonneries, etc.)"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Le service qui fournit cette fonction aura accès à toutes les infos visibles sur votre écran ou lues depuis votre appareil lors d\'un enregistrement ou de la diffusion d\'un contenu. Cela comprend, entre autres, vos mots de passe, les détails de vos paiements, vos photos, vos messages ou les contenus audio que vous écoutez."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Démarrer l\'enregistrement ou la diffusion ?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Démarrer l\'enregistrement ou la diffusion avec <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Autoriser <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> à partager ou enregistrer ?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Tout l\'écran"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Une seule appli"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Lorsque vous partagez, enregistrez ou castez, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> a accès à tout ce qui est visible sur votre écran ou lu sur votre appareil. Faites donc attention à vos mots de passe, détails de mode de paiement, messages ou autres informations sensibles."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Lorsque vous partagez, enregistrez ou castez une appli, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> a accès à tout ce qui est visible sur votre écran ou lu sur votre appareil. Faites donc attention à vos mots de passe, détails de mode de paiement, messages ou autres informations sensibles."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuer"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Partager ou enregistrer une appli"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Tout effacer"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Gérer"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Historique"</string>
diff --git a/packages/SystemUI/res/values-gl/strings.xml b/packages/SystemUI/res/values-gl/strings.xml
index 36036aa..bb3894a 100644
--- a/packages/SystemUI/res/values-gl/strings.xml
+++ b/packages/SystemUI/res/values-gl/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Notificación en curso sobre unha sesión de gravación de pantalla"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Queres iniciar a gravación?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Durante a gravación, o sistema Android pode captar información confidencial que apareza na pantalla ou se reproduza no dispositivo, como contrasinais, información de pago, fotos, mensaxes e audio."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Gravar pantalla completa"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Gravar unha soa aplicación"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Durante a gravación, Android ten acceso a todo o que se vexa na pantalla ou se reproduza no teu dispositivo. Polo tanto, debes ter coidado cos contrasinais, os detalles de pago, as mensaxes ou outra información confidencial."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Cando gravas unha aplicación, Android ten acceso a todo o que se vexa ou se reproduza nela. Polo tanto, debes ter coidado cos contrasinais, os detalles de pago, as mensaxes ou outra información confidencial."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Iniciar gravación"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Gravar audio"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Audio do dispositivo"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Son do dispositivo (por exemplo, música, chamadas e tons de chamada)"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"O servizo que proporciona esta función terá acceso a toda a información visible na pantalla ou reproducida desde o teu dispositivo mentres graves ou emitas contido. Isto inclúe información como contrasinais, detalles de pago, fotos, mensaxes e o audio que reproduzas."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Queres iniciar a gravación ou a emisión?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Queres comezar a gravar ou emitir contido con <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Queres permitir que <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> comparta ou grave contido?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Pantalla completa"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Unha soa aplicación"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Cando compartes, gravas ou emites contido, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ten acceso a todo o que se vexa na pantalla ou se reproduza no teu dispositivo. Polo tanto, debes ter coidado cos contrasinais, os detalles de pago, as mensaxes ou outra información confidencial."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Cando compartes, gravas ou emites unha aplicación, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ten acceso a todo o que se vexa ou se reproduza nela. Polo tanto, debes ter coidado cos contrasinais, os detalles de pago, as mensaxes ou outra información confidencial."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuar"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Compartir ou gravar unha aplicación"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Eliminar todas"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Xestionar"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Historial"</string>
diff --git a/packages/SystemUI/res/values-hi/strings.xml b/packages/SystemUI/res/values-hi/strings.xml
index 3d1b1ea..9dc6f67 100644
--- a/packages/SystemUI/res/values-hi/strings.xml
+++ b/packages/SystemUI/res/values-hi/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"स्क्रीन रिकॉर्ड सेशन के लिए जारी सूचना"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"क्या आपको रिकॉर्डिंग शुरू करनी है?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"रिकॉर्ड करते समय, Android सिस्टम आपकी स्क्रीन पर दिखने वाली या चलाई जाने वाली संवेदनशील जानकारी को कैप्चर कर सकता है. इसमें पासवर्ड, पैसे चुकाने से जुड़ी जानकारी, फ़ोटो, मैसेज, और ऑडियो शामिल हैं."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"पूरी स्क्रीन रिकॉर्ड करें"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"सिर्फ़ एक ऐप्लिकेशन रिकॉर्ड करें"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Android के पास, रिकॉर्ड करने के दौरान, स्क्रीन पर दिख रही हर चीज़ या डिवाइस पर चल रहे हर मीडिया का ऐक्सेस होता है. इसलिए, शेयर, रिकॉर्ड या कास्ट करते समय, पासवर्ड, पेमेंट के तरीके की जानकारी, मैसेज या किसी और संवेदनशील जानकारी को लेकर खास सावधानी बरतें."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"कोई ऐप्लिकेशन रिकॉर्ड करने के दौरान, Android के पास उस पर दिख रही हर चीज़ या उस पर चल रहे हर मीडिया का ऐक्सेस होता है. इसलिए, पासवर्ड, पेमेंट के तरीके की जानकारी, मैसेज या किसी और संवेदनशील जानकारी को लेकर खास सावधानी बरतें."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"रिकॉर्ड करना शुरू करें"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"ऑडियो रिकॉर्ड करें"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"डिवाइस ऑडियो"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"आपके डिवाइस से आने वाली आवाज़ जैसे कि संगीत, कॉल, और रिंगटोन"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"इस फ़ंक्शन को उपलब्ध कराने वाली सेवा, रिकॉर्ड या कास्ट करते समय, आपकी स्क्रीन पर दिखने वाली या चलाई जाने वाली जानकारी को ऐक्सेस कर सकती है. इसमें पासवर्ड, पैसे चुकाने से जुड़ी जानकारी, फ़ोटो, मैसेज, और चलाए जाने वाले ऑडियो शामिल हैं."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"रिकॉर्डिंग या कास्ट करना शुरू करें?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> का इस्तेमाल करके रिकॉर्ड और कास्ट करना शुरू करें?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> को शेयर या रिकॉर्ड करने की अनुमति दें?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"पूरी स्क्रीन"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"सिर्फ़ एक ऐप्लिकेशन"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"शेयर, रिकॉर्ड या कास्ट करते समय, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> के पास स्क्रीन पर दिख रही हर चीज़ या डिवाइस पर चल रहे हर मीडिया का ऐक्सेस होता है. इसलिए, शेयर, रिकॉर्ड या कास्ट करते समय, पासवर्ड, पेमेंट के तरीके की जानकारी, मैसेज या किसी और संवेदनशील जानकारी को लेकर खास सावधानी बरतें."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"शेयर, रिकॉर्ड या कास्ट करते समय, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> के पास उस ऐप्लिकेशन पर दिख रही हर चीज़ या उस पर चल रहे हर मीडिया का ऐक्सेस होता है. इसलिए, शेयर, रिकॉर्ड या कास्ट करते समय, पासवर्ड, पेमेंट के तरीके की जानकारी, मैसेज या किसी और संवेदनशील जानकारी को लेकर खास सावधानी बरतें."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"जारी रखें"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ऐप्लिकेशन शेयर करें या उसकी रिकॉर्डिंग करें"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"सभी को हटाएं"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"मैनेज करें"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"इतिहास"</string>
diff --git a/packages/SystemUI/res/values-hy/strings.xml b/packages/SystemUI/res/values-hy/strings.xml
index fed009f..5e5e249 100644
--- a/packages/SystemUI/res/values-hy/strings.xml
+++ b/packages/SystemUI/res/values-hy/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Էկրանի տեսագրման աշխատաշրջանի ընթացիկ ծանուցում"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Սկսե՞լ տեսագրումը"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Տեսագրման ընթացքում Android համակարգը կարող է գրանցել անձնական տեղեկություններ, որոնք տեսանելի են էկրանին կամ նվագարկվում են ձեր սարքում։ Սա ներառում է այնպիսի տեղեկություններ, ինչպիսիք են, օրինակ, գաղտնաբառերը, վճարային տվյալները, լուսանկարները, հաղորդագրությունները և նվագարկվող աուդիո ֆայլերը։"</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Տեսագրել ամբողջ էկրանը"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Տեսագրել մեկ հավելված"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Երբ դուք տեսագրում եք էկրանը, Android-ին հասանելի է դառնում այն ամենը, ինչ տեսանելի է էկրանին և նվագարկվում է ձեր սարքում։ Հիշեք այդ մասին, երբ պատրաստվում եք դիտել կամ մուտքագրել գաղտնաբառեր, վճարային տվյալներ, հաղորդագրություններ և այլ կոնֆիդենցիալ տեղեկություններ։"</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Երբ դուք տեսագրում եք որևէ հավելվածի էկրանը, Android-ին հասանելի է դառնում այն ամենը, ինչ ցուցադրվում է կամ նվագարկվում այդ հավելվածում։ Հիշեք այդ մասին, երբ պատրաստվում եք դիտել կամ մուտքագրել գաղտնաբառեր, վճարային տվյալներ, հաղորդագրություններ և այլ կոնֆիդենցիալ տեղեկություններ։"</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Սկսել տեսագրումը"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Ձայնագրել"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Սարքի ձայները"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Ձեր սարքի ձայները, օրինակ՝ երաժշտությունը, զանգերն ու զանգերանգները"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Ձայնագրման և հեռարձակման ընթացքում ծառայությունների մատակարարին հասանելի կլինեն ձեր սարքի էկրանին ցուցադրվող տեղեկությունները և ձեր սարքով նվագարկվող նյութերը։ Սա ներառում է այնպիսի տեղեկություններ, ինչպիսիք են, օրինակ, գաղտնաբառերը, վճարային տվյալները, լուսանկարները, հաղորդագրությունները և նվագարկվող աուդիո ֆայլերը։"</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Սկսե՞լ ձայնագրումը կամ հեռարձակումը"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Սկսե՞լ ձայնագրումը կամ հեռարձակումը <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> հավելվածով"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Թույլատրե՞լ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> հավելվածին ցուցադրել կամ տեսագրել էկրանը"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Ամբողջ էկրանը"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Մեկ հավելված"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Երբ դուք ցուցադրում, տեսագրում կամ հեռարձակում եք էկրանը, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> հավելվածին հասանելի է դառնում այն ամենը, ինչ տեսանելի է էկրանին և նվագարկվում է ձեր սարքում։ Հիշեք այդ մասին, երբ պատրաստվում եք դիտել կամ մուտքագրել գաղտնաբառեր, վճարային տվյալներ, հաղորդագրություններ և այլ կոնֆիդենցիալ տեղեկություններ։"</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Երբ դուք ցուցադրում, տեսագրում կամ հեռարձակում եք որևէ հավելվածի էկրանը, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> հավելվածին հասանելի է դառնում այն ամենը, ինչ ցուցադրվում է կամ նվագարկվում այդ հավելվածում։ Հիշեք այդ մասին, երբ պատրաստվում եք դիտել կամ մուտքագրել գաղտնաբառեր, վճարային տվյալներ, հաղորդագրություններ և այլ կոնֆիդենցիալ տեղեկություններ։"</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Շարունակել"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Հավելվածի էկրանի ցուցադրում կամ տեսագրում"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Մաքրել բոլորը"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Կառավարել"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Պատմություն"</string>
diff --git a/packages/SystemUI/res/values-in/strings.xml b/packages/SystemUI/res/values-in/strings.xml
index 0156900..e372c37 100644
--- a/packages/SystemUI/res/values-in/strings.xml
+++ b/packages/SystemUI/res/values-in/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Notifikasi yang sedang berjalan untuk sesi rekaman layar"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Mulai merekam?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Saat merekam, Sistem Android dapat ikut merekam informasi sensitif yang terlihat di layar atau diputar di perangkat Anda. Informasi ini mencakup sandi, info pembayaran, foto, pesan, dan audio."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Rekam seluruh layar"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Rekam satu aplikasi"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Saat Anda merekam, Android akan memiliki akses ke semua hal yang ditampilkan di layar atau yang diputar di perangkat Anda. Jadi, berhati-hatilah saat memasukkan sandi, detail pembayaran, pesan, atau informasi sensitif lainnya."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Saat Anda merekam aplikasi, Android akan memiliki akses ke semua hal yang ditampilkan atau yang diputar di aplikasi tersebut. Jadi, berhati-hatilah saat memasukkan sandi, detail pembayaran, pesan, atau informasi sensitif lainnya."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Mulai merekam"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Rekam audio"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Audio perangkat"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Suara dari perangkat Anda, seperti musik, panggilan, dan nada dering"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Layanan yang menyediakan fungsi ini akan memiliki akses ke semua informasi yang terlihat di layar atau diputar dari perangkat saat merekam atau melakukan transmisi. Ini mencakup informasi seperti sandi, detail pembayaran, foto, pesan, dan audio yang Anda putar."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Mulai merekam atau melakukan transmisi?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Mulai merekam atau melakukan transmisi dengan <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Izinkan <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> untuk membagikan atau merekam?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Seluruh layar"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Satu aplikasi"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Jika Anda membagikan, merekam, atau mentransmisikan, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> akan memiliki akses ke semua hal yang ditampilkan di layar atau yang diputar di perangkat Anda. Jadi, berhati-hatilah saat memasukkan sandi, detail pembayaran, pesan, atau informasi sensitif lainnya."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Jika Anda membagikan, merekam, atau mentransmisikan suatu aplikasi, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> akan memiliki akses ke semua hal yang ditampilkan atau yang diputar di aplikasi tersebut. Jadi, berhati-hatilah saat memasukkan sandi, detail pembayaran, pesan, atau informasi sensitif lainnya."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Lanjutkan"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Bagikan atau rekam aplikasi"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Hapus semua"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Kelola"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Histori"</string>
diff --git a/packages/SystemUI/res/values-is/strings.xml b/packages/SystemUI/res/values-is/strings.xml
index 7622ad4..136db9e 100644
--- a/packages/SystemUI/res/values-is/strings.xml
+++ b/packages/SystemUI/res/values-is/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Áframhaldandi tilkynning fyrir skjáupptökulotu"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Hefja upptöku?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Á meðan tekið er upp getur Android kerfið fangað viðkvæmar upplýsingar sem sjást á skjánum eða spilast í tækinu. Þar á meðal eru upplýsingar á borð við aðgangsorð, greiðsluupplýsingar, myndir, skilaboð og hljóð."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Taka upp allan skjáinn"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Taka upp eitt forrit"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Þegar þú tekur upp hefur Android aðgang að öllu sem sést á skjánum eða spilast í tækinu. Passaðu því upp á aðgangsorð, greiðsluupplýsingar, skilaboð eða aðrar viðkvæmar upplýsingar."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Þegar þú tekur upp forrit hefur Android aðgang að öllu sem sést eða spilast í viðkomandi forriti. Passaðu því upp á aðgangsorð, greiðsluupplýsingar, skilaboð eða aðrar viðkvæmar upplýsingar."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Hefja upptöku"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Taka upp hljóð"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Hljóð tækis"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Hljóð úr tækinu á borð við tónlist, símtöl og hringitóna"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Þjónustan sem býður upp á þennan eiginleika fær aðgang að öllum upplýsingum sem sjást á skjánum eða eru spilaðar í tækinu á meðan upptaka eða útsending er í gangi, þar á meðal aðgangsorði, greiðsluupplýsingum, myndum, skilaboðum og hljóðefni sem þú spilar."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Viltu hefja upptöku eða útsendingu?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Viltu hefja upptöku eða útsendingu með <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Leyfa <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> að deila eða taka upp?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Allur skjárinn"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Eitt forrit"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Þegar þú deilir, tekur upp eða sendir út hefur<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> aðgang að öllu sem sést á skjánum eða spilast í tækinu. Passaðu því upp á aðgangsorð, greiðsluupplýsingar, skilaboð eða aðrar viðkvæmar upplýsingar."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Þegar þú deilir, tekur upp eða sendir út forrit hefur <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> aðgang að öllu sem sést eða spilast í viðkomandi forriti. Passaðu því upp á aðgangsorð, greiðsluupplýsingar, skilaboð eða aðrar viðkvæmar upplýsingar."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Áfram"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Deila eða taka upp forrit"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Hreinsa allt"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Stjórna"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Ferill"</string>
diff --git a/packages/SystemUI/res/values-kk/strings.xml b/packages/SystemUI/res/values-kk/strings.xml
index d6631e7..df3e6fd 100644
--- a/packages/SystemUI/res/values-kk/strings.xml
+++ b/packages/SystemUI/res/values-kk/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Экранды бейнеге жазудың ағымдағы хабарландыруы"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Жазу басталсын ба?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Android жүйесі экранда көрсетілетін немесе құрылғыда ойнатылатын құпия ақпаратты жазып алуы мүмкін. Ондай ақпаратқа құпия сөздер, төлем ақпараты, фотосуреттер, хабарлар және аудио жатады."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Бүкіл экранды жазу"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Жалғыз қолданбаны жазу"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Жазу кезінде Android жүйесі экраныңызда көрінетін не құрылғыңызда ойнатылатын барлық нәрсені пайдалана алады. Сондықтан құпия сөздерді, төлем туралы мәліметті, хабарларды немесе басқа құпия ақпаратты енгізу кезінде сақ болыңыз."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Қолданба экранын жазу кезінде Android жүйесі қолданбада көрінетін не ойнатылатын барлық нәрсені пайдалана алады. Сондықтан құпия сөздерді, төлем туралы мәліметті, хабарларды немесе басқа құпия ақпаратты енгізу кезінде сақ болыңыз."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Жазуды бастау"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Аудио жазу"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Құрылғыдан шығатын дыбыс"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Музыка, қоңыраулар және рингтондар сияқты құрылғыдан шығатын дыбыс"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Осы функцияны ұсынатын қызмет жазу не трансляциялау кезінде экранда көрсетілетін немесе құрылғыда дыбысталатын ақпаратты пайдалана алады. Бұған құпия сөздер, төлем туралы мәліметтер, суреттер, хабарлар және аудиоматериалдар кіреді."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Жазу немесе трансляциялау басталсын ба?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> арқылы жазу немесе трансляциялау басталсын ба?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> қолданбасына экранды бөлісуге не жазуға рұқсат берілсін бе?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Бүкіл экран"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Жалғыз қолданба"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Бөлісу, жазу не трансляциялау кезінде <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> қолданбасы экраныңызда көрінетін не құрылғыңызда ойнатылатын барлық нәрсені пайдалана алады. Сондықтан құпия сөздерді, төлем туралы мәліметті, хабарларды немесе басқа құпия ақпаратты енгізу кезінде сақ болыңыз."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Қолданба экранын бөлісу, жазу не трансляциялау кезінде <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> қолданбасы онда көрінетін не ойнатылатын барлық нәрсені пайдалана алады. Сондықтан құпия сөздерді, төлем туралы мәліметті, хабарларды немесе басқа құпия ақпаратты енгізу кезінде сақ болыңыз."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Жалғастыру"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Қолданба экранын бөлісу не жазу"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Барлығын тазалау"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Басқару"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Тарих"</string>
diff --git a/packages/SystemUI/res/values-kn/strings.xml b/packages/SystemUI/res/values-kn/strings.xml
index 713d20d..83a5e87 100644
--- a/packages/SystemUI/res/values-kn/strings.xml
+++ b/packages/SystemUI/res/values-kn/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"ಸ್ಕ್ರೀನ್ ರೆಕಾರ್ಡಿಂಗ್ ಸೆಶನ್ಗಾಗಿ ಚಾಲ್ತಿಯಲ್ಲಿರುವ ಅಧಿಸೂಚನೆ"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"ರೆಕಾರ್ಡಿಂಗ್ ಪ್ರಾರಂಭಿಸಬೇಕೆ?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"ರೆಕಾರ್ಡಿಂಗ್ ಸಮಯದಲ್ಲಿ, ಸ್ಕ್ರೀನ್ನಲ್ಲಿ ಗೋಚರಿಸುವ ಅಥವಾ ನಿಮ್ಮ ಸಾಧನದಲ್ಲಿ ಪ್ಲೇ ಮಾಡಲಾದ ಸೂಕ್ಷ್ಮ ಮಾಹಿತಿಯನ್ನು Android ಸಿಸ್ಟಂ ಕ್ಯಾಪ್ಚರ್ ಮಾಡಬಹುದು. ಇದು ಪಾಸ್ವರ್ಡ್ಗಳು, ಪಾವತಿ ಮಾಹಿತಿ, ಫೋಟೋಗಳು, ಸಂದೇಶಗಳು ಮತ್ತು ಆಡಿಯೋವನ್ನು ಒಳಗೊಂಡಿದೆ."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"ಸಂಪೂರ್ಣ ಸ್ಕ್ರೀನ್ ರೆಕಾರ್ಡ್ ಮಾಡಿ"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"ಒಂದೇ ಆ್ಯಪ್ ಅನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಿ"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"ನೀವು ರೆಕಾರ್ಡ್ ಮಾಡುತ್ತಿರುವಾಗ, ನಿಮ್ಮ ಸ್ಕ್ರೀನ್ ಮೇಲೆ ಕಾಣಿಸುವ ಅಥವಾ ನಿಮ್ಮ ಸಾಧನದಲ್ಲಿ ಪ್ಲೇ ಆಗುವ ಯಾವುದೇ ವಿಷಯಕ್ಕೆ Android ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ಹೊಂದಿರುತ್ತದೆ. ಹಾಗಾಗಿ, ಪಾಸ್ವರ್ಡ್ಗಳು, ಪಾವತಿ ವಿವರಗಳು, ಸಂದೇಶಗಳು ಅಥವಾ ಇತರ ಸೂಕ್ಷ್ಮ ಮಾಹಿತಿಯ ಕುರಿತು ಜಾಗರೂಕರಾಗಿರಿ."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"ನೀವು ಆ್ಯಪ್ ಅನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡುತ್ತಿರುವಾಗ, ಆ ಆ್ಯಪ್ನಲ್ಲಿ ತೋರಿಸಲಾಗುವ ಅಥವಾ ಪ್ಲೇ ಆಗುವ ಯಾವುದೇ ವಿಷಯಕ್ಕೆ Android ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ಹೊಂದಿರುತ್ತದೆ. ಹಾಗಾಗಿ, ಪಾಸ್ವರ್ಡ್ಗಳು, ಪಾವತಿ ವಿವರಗಳು, ಸಂದೇಶಗಳು ಅಥವಾ ಇತರ ಸೂಕ್ಷ್ಮ ಮಾಹಿತಿಯ ಕುರಿತು ಜಾಗರೂಕರಾಗಿರಿ."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"ರೆಕಾರ್ಡಿಂಗ್ ಪ್ರಾರಂಭಿಸಿ"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"ಆಡಿಯೋ ರೆಕಾರ್ಡ್ ಮಾಡಿ"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"ಸಾಧನದ ಆಡಿಯೋ"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"ನಿಮ್ಮ ಸಾಧನದ ಧ್ವನಿ ಉದಾ: ಸಂಗೀತ, ಕರೆಗಳು ಮತ್ತು ರಿಂಗ್ಟೋನ್ಗಳು"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"ಈ ವೈಶಿಷ್ಟ್ಯವು ಒದಗಿಸುವ ಸೇವೆಗಳು, ಸ್ಕ್ರೀನ್ ಮೇಲೆ ಗೋಚರಿಸುವ ಅಥವಾ ರೆಕಾರ್ಡಿಂಗ್ ಅಥವಾ ಬಿತ್ತರಿಸುವಾಗ ಸಾಧನದಲ್ಲಿ ಪ್ಲೇ ಆಗುವ ಎಲ್ಲಾ ಮಾಹಿತಿಗಳಿಗೆ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿರುತ್ತವೆ. ಪಾಸ್ವರ್ಡ್ಗಳು, ಪಾವತಿ ವಿವರಗಳು, ಫೋಟೋಗಳು, ಸಂದೇಶಗಳು ಮತ್ತು ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ನಂತಹ ಮಾಹಿತಿಯನ್ನು ಇದು ಒಳಗೊಂಡಿದೆ."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"ರೆಕಾರ್ಡಿಂಗ್ ಅಥವಾ ಬಿತ್ತರಿಸುವಿಕೆಯನ್ನು ಪ್ರಾರಂಭಿಸಬೇಕೆ?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ಮೂಲಕ ರೆಕಾರ್ಡಿಂಗ್, ಬಿತ್ತರಿಸುವುದನ್ನು ಪ್ರಾರಂಭಿಸುವುದೇ?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"ಹಂಚಿಕೊಳ್ಳಲು ಅಥವಾ ರೆಕಾರ್ಡ್ ಮಾಡಲು <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ಅನ್ನು ಅನುಮತಿಸಬೇಕೆ?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"ಸಂಪೂರ್ಣ ಸ್ಕ್ರೀನ್"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"ಒಂದೇ ಆ್ಯಪ್"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"ನೀವು ಹಂಚಿಕೊಳ್ಳುತ್ತಿರುವಾಗ, ರೆಕಾರ್ಡ್ ಮಾಡುತ್ತಿರುವಾಗ ಅಥವಾ ಬಿತ್ತರಿಸುತ್ತಿರುವಾಗ, ನಿಮ್ಮ ಸ್ಕ್ರೀನ್ ಮೇಲೆ ಕಾಣಿಸುವ ಅಥವಾ ನಿಮ್ಮ ಸಾಧನದಲ್ಲಿ ಪ್ಲೇ ಆಗುವ ಯಾವುದೇ ವಿಷಯಕ್ಕೆ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ಹೊಂದಿರುತ್ತದೆ. ಹಾಗಾಗಿ, ಪಾಸ್ವರ್ಡ್ಗಳು, ಪಾವತಿ ವಿವರಗಳು, ಸಂದೇಶಗಳು ಅಥವಾ ಇತರ ಸೂಕ್ಷ್ಮ ಮಾಹಿತಿಯ ಕುರಿತು ಜಾಗರೂಕರಾಗಿರಿ."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ನೀವು ಆ್ಯಪ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳುತ್ತಿರುವಾಗ, ರೆಕಾರ್ಡ್ ಮಾಡುತ್ತಿರುವಾಗ ಅಥವಾ ಬಿತ್ತರಿಸುತ್ತಿರುವಾಗ, ಆ ಆ್ಯಪ್ನಲ್ಲಿ ತೋರಿಸಲಾಗುವ ಅಥವಾ ಪ್ಲೇ ಆಗುವ ಯಾವುದೇ ವಿಷಯಕ್ಕೆ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ಹೊಂದಿರುತ್ತದೆ. ಹಾಗಾಗಿ, ಪಾಸ್ವರ್ಡ್ಗಳು, ಪಾವತಿ ವಿವರಗಳು, ಸಂದೇಶಗಳು ಅಥವಾ ಇತರ ಸೂಕ್ಷ್ಮ ಮಾಹಿತಿಯ ಕುರಿತು ಜಾಗರೂಕರಾಗಿರಿ."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ಮುಂದುವರಿಸಿ"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ಆ್ಯಪ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಿ ಅಥವಾ ರೆಕಾರ್ಡ್ ಮಾಡಿ"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"ಎಲ್ಲವನ್ನೂ ತೆರವುಗೊಳಿಸಿ"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"ನಿರ್ವಹಿಸಿ"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"ಇತಿಹಾಸ"</string>
diff --git a/packages/SystemUI/res/values-ko/strings.xml b/packages/SystemUI/res/values-ko/strings.xml
index 290c517..f3fb7a0 100644
--- a/packages/SystemUI/res/values-ko/strings.xml
+++ b/packages/SystemUI/res/values-ko/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"화면 녹화 세션에 관한 지속적인 알림"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"녹화를 시작하시겠습니까?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Android 시스템이 녹화 중에 화면에 표시되거나 기기에서 재생되는 민감한 정보를 캡처할 수 있습니다. 여기에는 비밀번호, 결제 정보, 사진, 메시지 및 오디오가 포함됩니다."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"전체 화면 녹화"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"단일 앱 녹화"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"녹화할 때 Android에서 화면에 표시되거나 기기에서 재생되는 모든 항목에 액세스할 수 있습니다. 따라서 비밀번호, 결제 세부정보, 메시지 등 민감한 정보가 노출되지 않도록 주의하세요."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"앱을 녹화할 때 Android에서 해당 앱에서 표시되거나 재생되는 모든 항목에 액세스할 수 있으므로 비밀번호, 결제 세부정보, 메시지 등 민감한 정보가 노출되지 않도록 주의하세요."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"녹화 시작"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"오디오 녹음"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"기기 오디오"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"음악, 통화, 벨소리와 같이 기기에서 나는 소리"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"이 기능을 제공하는 서비스는 녹화 또는 전송 중에 화면에 표시되거나 기기에서 재생되는 모든 정보에 액세스할 수 있습니다. 여기에는 비밀번호, 결제 세부정보, 사진, 메시지, 재생하는 오디오 같은 정보가 포함됩니다."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"녹화 또는 전송을 시작하시겠습니까?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>으로 녹화 또는 전송을 시작하시겠습니까?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>에서 공유 또는 녹화를 허용할까요?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"전체 화면"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"단일 앱"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"공유하거나 녹화하거나 전송할 때 <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 앱에서 화면에 표시되거나 기기에서 재생되는 모든 항목에 액세스할 수 있습니다. 따라서 비밀번호, 결제 세부정보, 메시지 등 민감한 정보가 노출되지 않도록 주의하세요."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"앱을 공유하거나 녹화하거나 전송할 때는 <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>에서 해당 앱에 표시되거나 재생되는 모든 항목에 액세스할 수 있으므로 비밀번호, 결제 세부정보, 메시지 등 민감한 정보가 노출되지 않도록 주의하세요."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"계속"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"앱 공유 또는 녹화"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"모두 지우기"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"관리"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"기록"</string>
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-lv/strings.xml b/packages/SystemUI/res/values-lv/strings.xml
index 9cb8bfd..5a63b62 100644
--- a/packages/SystemUI/res/values-lv/strings.xml
+++ b/packages/SystemUI/res/values-lv/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Aktīvs paziņojums par ekrāna ierakstīšanas sesiju"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Vai sākt ierakstīšanu?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Ierakstīšanas laikā Android sistēmā var tikt tverta jebkura sensitīvā informācija, kas ir redzama jūsu ekrānā vai tiek atskaņota jūsu ierīcē. Šī informācija ir paroles, maksājumu informācija, fotoattēli, ziņojumi un audio."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Ierakstīt visu ekrānu"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Ierakstīt vienu lietotni"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Ierakstīšanas laikā Android var piekļūt visam, kas tiek rādīts jūsu ekrānā vai atskaņots jūsu ierīcē. Tāpēc piesardzīgi apejieties ar parolēm, maksājumu informāciju, ziņojumiem un citu sensitīvu informāciju."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Lietotnes ierakstīšanas laikā Android var piekļūt visam, kas tiek rādīts vai atskaņots attiecīgajā lietotnē. Tāpēc piesardzīgi apejieties ar parolēm, maksājumu informāciju, ziņojumiem un citu sensitīvu informāciju."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Sākt ierakstīšanu"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Ierakstīt audio"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Ierīces audio"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Skaņa no jūsu ierīces, piemēram, mūzika, sarunas un zvana signāli"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Pakalpojums, kas nodrošina šo funkciju, iegūs piekļuvi visai informācijai, kas ierakstīšanas vai apraides laikā tiks rādīta jūsu ekrānā vai atskaņota jūsu ierīcē. Atļauja attiecas uz tādu informāciju kā paroles, maksājumu informācija, fotoattēli, ziņojumi un jūsu atskaņotais audio saturs."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Vai vēlaties sākt ierakstīšanu/apraidi?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Vai vēlaties sākt ierakstīšanu vai apraidi, izmantojot lietotni <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Vai atļaujat lietotnei <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> veikt kopīgošanu vai ierakstīšanu?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Viss ekrāns"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Viena lietotne"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Kopīgošanas, ierakstīšanas vai apraides laikā <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> var piekļūt visam, kas tiek rādīts jūsu ekrānā vai atskaņots jūsu ierīcē. Tāpēc piesardzīgi apejieties ar parolēm, maksājumu informāciju, ziņojumiem un citu sensitīvu informāciju."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Lietotnes kopīgošanas, ierakstīšanas vai apraides laikā <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> var piekļūt visam, kas tiek rādīts vai atskaņots attiecīgajā lietotnē. Tāpēc piesardzīgi apejieties ar parolēm, maksājumu informāciju, ziņojumiem un citu sensitīvu informāciju."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Turpināt"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Lietotnes kopīgošana vai ierakstīšana"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Dzēst visu"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Pārvaldīt"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Vēsture"</string>
diff --git a/packages/SystemUI/res/values-ml/strings.xml b/packages/SystemUI/res/values-ml/strings.xml
index bf1c262..7611822 100644
--- a/packages/SystemUI/res/values-ml/strings.xml
+++ b/packages/SystemUI/res/values-ml/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"ഒരു സ്ക്രീൻ റെക്കോർഡിംഗ് സെഷനായി നിലവിലുള്ള അറിയിപ്പ്"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"റെക്കോർഡിംഗ് ആരംഭിക്കണോ?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"റെക്കോർഡ് ചെയ്യുമ്പോൾ, നിങ്ങളുടെ സ്ക്രീനിൽ ദൃശ്യമാകുന്നതോ ഉപകരണത്തിൽ പ്ലേ ചെയ്യുന്നതോ ആയ ഏത് തന്ത്രപ്രധാന വിവരങ്ങളും Android സിസ്റ്റത്തിന് പകർത്താനാവും. പാസ്വേഡുകൾ, പേയ്മെന്റ് വിവരം, ഫോട്ടോകൾ, സന്ദേശങ്ങൾ, ഓഡിയോ എന്നിവ ഇതിൽ ഉൾപ്പെടുന്നു."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"പൂർണ സ്ക്രീൻ റെക്കോർഡ് ചെയ്യൂ"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"ഒറ്റ ആപ്പ് റെക്കോർഡ് ചെയ്യുക"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"റെക്കോർഡ് ചെയ്യുമ്പോൾ, Android-ന് സ്ക്രീനിൽ ദൃശ്യമാകുന്നതോ ഉപകരണത്തിൽ പ്ലേ ചെയ്യുന്നതോ ആയ ഏത് കാര്യത്തിലേക്കും ആക്സസ് ഉണ്ട്. അതിനാൽ, പാസ്വേഡുകൾ, പേയ്മെന്റ് വിശദാംശങ്ങൾ, സന്ദേശങ്ങൾ അല്ലെങ്കിൽ സൂക്ഷ്മമായി കൈകാര്യം ചെയ്യേണ്ട മറ്റു വിവരങ്ങൾ എന്നിവ നൽകുമ്പോൾ സൂക്ഷിക്കുക."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"ഒരു ആപ്പ് റെക്കോർഡ് ചെയ്യുമ്പോൾ, Android-ന് ആ ആപ്പിൽ കാണിക്കുന്നതോ പ്ലേ ചെയ്യുന്നതോ ആയ എല്ലാത്തിലേക്കും ആക്സസ് ഉണ്ട്. അതിനാൽ, പാസ്വേഡുകൾ, പേയ്മെന്റ് വിശദാംശങ്ങൾ, സന്ദേശങ്ങൾ അല്ലെങ്കിൽ സൂക്ഷ്മമായി കൈകാര്യം ചെയ്യേണ്ട മറ്റു വിവരങ്ങൾ എന്നിവ നൽകുമ്പോൾ സൂക്ഷിക്കുക."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"റെക്കോർഡിംഗ് ആരംഭിക്കുക"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"ഓഡിയോ റെക്കോർഡ് ചെയ്യുക"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"ഉപകരണത്തിന്റെ ഓഡിയോ"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"സംഗീതം, കോളുകൾ, റിംഗ്ടോണുകൾ എന്നിവപോലെ നിങ്ങളുടെ ഉപകരണത്തിൽ നിന്നുള്ള ശബ്ദം"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"റെക്കോർഡ് ചെയ്യുമ്പോഴോ കാസ്റ്റ് ചെയ്യുമ്പോഴോ നിങ്ങളുടെ ഉപകരണത്തിൽ നിന്ന് പ്ലേ ചെയ്യുന്നതോ നിങ്ങളുടെ സ്ക്രീനിൽ ദൃശ്യമാകുന്നതോ ആയ എല്ലാ വിവരങ്ങളിലേക്കും ഈ ഫംഗ്ഷൻ ലഭ്യമാക്കുന്ന സേവനത്തിന് ആക്സസ് ഉണ്ടായിരിക്കും. നിങ്ങൾ പ്ലേ ചെയ്യുന്ന ഓഡിയോ, സന്ദേശങ്ങൾ, ഫോട്ടോകൾ, പേയ്മെന്റ് വിശദാംശങ്ങൾ, പാസ്വേഡുകൾ എന്നിവ പോലുള്ള വിവരങ്ങൾ ഇതിൽ ഉൾപ്പെടുന്നു."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"റെക്കോർഡ് ചെയ്യൽ അല്ലെങ്കിൽ കാസ്റ്റ് ചെയ്യൽ ആരംഭിക്കണോ?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ഉപയോഗിച്ച് റെക്കോർഡ് ചെയ്യൽ അല്ലെങ്കിൽ കാസ്റ്റ് ചെയ്യൽ ആരംഭിക്കണോ?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"പങ്കിടാനോ റെക്കോർഡ് ചെയ്യാനോ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> എന്നതിനെ അനുവദിക്കണോ?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"മുഴുവൻ സ്ക്രീൻ"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"ഒറ്റ ആപ്പ്"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"പങ്കിടുമ്പോൾ, റെക്കോർഡ് ചെയ്യുമ്പോൾ അല്ലെങ്കിൽ കാസ്റ്റ് ചെയ്യുമ്പോൾ, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> എന്നതിന് നിങ്ങളുടെ സ്ക്രീനിൽ ദൃശ്യമാകുന്നതോ ഉപകരണത്തിൽ പ്ലേ ചെയ്യുന്നതോ ആയ ഏത് കാര്യത്തിലേക്കും ആക്സസ് ഉണ്ട്. അതിനാൽ, പാസ്വേഡുകൾ, പേയ്മെന്റ് വിശദാംശങ്ങൾ, സന്ദേശങ്ങൾ അല്ലെങ്കിൽ സൂക്ഷ്മമായി കൈകാര്യം ചെയ്യേണ്ട മറ്റു വിവരങ്ങൾ എന്നിവ നൽകുമ്പോൾ സൂക്ഷിക്കുക."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ഒരു ആപ്പ് പങ്കിടുമ്പോൾ, റെക്കോർഡ് ചെയ്യുമ്പോൾ അല്ലെങ്കിൽ കാസ്റ്റ് ചെയ്യുമ്പോൾ, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> എന്നതിന് ആപ്പിൽ കാണിക്കുന്ന അല്ലെങ്കിൽ പ്ലേ ചെയ്യുന്ന എല്ലാത്തിലേക്കും ആക്സസ് ഉണ്ട്. അതിനാൽ, പാസ്വേഡുകൾ, പേയ്മെന്റ് വിശദാംശങ്ങൾ, സന്ദേശങ്ങൾ അല്ലെങ്കിൽ സൂക്ഷ്മമായി കൈകാര്യം ചെയ്യേണ്ട മറ്റു വിവരങ്ങൾ എന്നിവ നൽകുമ്പോൾ സൂക്ഷിക്കുക."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"തുടരുക"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ഒരു ആപ്പ് പങ്കിടുക അല്ലെങ്കിൽ റെക്കോർഡ് ചെയ്യുക"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"എല്ലാം മായ്ക്കുക"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"മാനേജ് ചെയ്യുക"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"ചരിത്രം"</string>
diff --git a/packages/SystemUI/res/values-mr/strings.xml b/packages/SystemUI/res/values-mr/strings.xml
index f7b8ef0..2f96f33 100644
--- a/packages/SystemUI/res/values-mr/strings.xml
+++ b/packages/SystemUI/res/values-mr/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"स्क्रीन रेकॉर्ड सत्रासाठी सुरू असलेली सूचना"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"रेकॉर्डिंग सुरू करायचे आहे का?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"रेकॉर्डिंग करताना, Android सिस्टीम तुमच्या स्क्रीनवर दिसणारी किंवा तुमच्या डिव्हाइसवर प्ले केलेली कोणतीही संवेदनशील माहिती कॅप्चर करू शकते. यात पासवर्ड, पेमेंट माहिती, फोटो, मेसेज आणि ऑडिओचा समावेश आहे."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"संपूर्ण स्क्रीन रेकॉर्ड करा"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"एकच अॅप रेकॉर्ड करा"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"तुम्ही रेकॉर्ड करत असताना, Android ला तुमच्या स्क्रीनवर दाखवलेल्या किंवा डिव्हाइसवर प्ले केलेल्या कोणत्याही गोष्टीचा अॅक्सेस असतो. त्यामुळे पासवर्ड, पेमेंट तपशील, मेसेज किंवा इतर संवेदनशील माहिती काळजीपूर्वक वापरा."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"तुम्ही अॅप रेकॉर्ड करत असताना, Android ला त्या अॅपवर दाखवलेल्या किंवा प्ले केलेल्या कोणत्याही गोष्टीचा अॅक्सेस असतो. त्यामुळे पासवर्ड, पेमेंट तपशील, मेसेज किंवा इतर संवेदनशील माहिती काळजीपूर्वक वापरा."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"रेकॉर्डिंग सुरू करा"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"ऑडिओ रेकॉर्ड करा"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"डिव्हाइस ऑडिओ"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"तुमच्या डिव्हाइसवरील आवाज, जसे की संगीत, कॉल आणि रिंगटोन"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"हे कार्य पुरवणाऱ्या सेवेस तुमच्या स्क्रीनवर दृश्यमान असलेल्या किंवा रेकॉर्ड किंवा कास्ट करताना तुमच्या डिव्हाइसमधून प्ले केलेल्या सर्व माहितीचा अॅक्सेस असेल. यामध्ये पासवर्ड, पेमेंट तपशील, फोटो, मेसेज आणि तुम्ही प्ले केलेला ऑडिओ यासारख्या माहितीचा समावेश असतो."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"रेकॉर्ड करणे किंवा कास्ट करणे सुरू करायचे का ?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ने रेकॉर्ड करणे किंवा कास्ट करणे सुरू करायचे का?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ला शेअर किंवा रेकॉर्ड करण्याची अनुमती द्यायची आहे का?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"संपूर्ण स्क्रीन"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"एक अॅप"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"तुम्ही शेअर, रेकॉर्ड किंवा कास्ट करत असताना, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ला तुमच्या स्क्रीनवर दाखवलेल्या किंवा डिव्हाइसवर प्ले केलेल्या कोणत्याही गोष्टीचा अॅक्सेस असतो. त्यामुळे पासवर्ड, पेमेंट तपशील, मेसेज किंवा इतर संवेदनशील माहिती काळजीपूर्वक वापरा."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"तुम्ही अॅप शेअर, रेकॉर्ड किंवा कास्ट करत असताना, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ला त्या अॅपवर दाखवलेल्या किंवा प्ले केलेल्या कोणत्याही गोष्टीचा अॅक्सेस असतो. त्यामुळे पासवर्ड, पेमेंट तपशील, मेसेज किंवा इतर संवेदनशील माहिती काळजीपूर्वक वापरा."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"पुढे सुरू ठेवा"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"अॅप शेअर किंवा रेकॉर्ड करा"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"सर्व साफ करा"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"व्यवस्थापित करा"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"इतिहास"</string>
diff --git a/packages/SystemUI/res/values-my/strings.xml b/packages/SystemUI/res/values-my/strings.xml
index a874e45..fe1b908 100644
--- a/packages/SystemUI/res/values-my/strings.xml
+++ b/packages/SystemUI/res/values-my/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"ဖန်သားပြင် ရိုက်ကူးသည့် စက်ရှင်အတွက် ဆက်တိုက်လာနေသော အကြောင်းကြားချက်"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"စတင် ရိုက်ကူးမလား။"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"ရိုက်ကူးနေစဉ်အတွင်း Android စနစ်သည် သင့်ဖန်သားပြင်ပေါ်တွင် မြင်နိုင်သော (သို့) သင့်စက်ပစ္စည်းတွင် ဖွင့်ထားသော အရေးကြီးသည့် အချက်အလက်များကို ရိုက်ယူနိုင်သည်။ ၎င်းတွင် စကားဝှက်၊ ငွေပေးချေမှု အချက်အလက်၊ ဓာတ်ပုံ၊ မက်ဆေ့ဂျ်နှင့် အသံများ ပါဝင်သည်။"</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"ဖန်သားပြင်တစ်ခုလုံးရိုက်ကူးရန်"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"အက်ပ်တစ်ခုတွင် ရိုက်ကူးရန်"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"ရိုက်ကူးနေစဉ် Android သည် သင့်ဖန်သားပြင်ရှိ မြင်နိုင်သည့် (သို့) သင့်စက်တွင် ဖွင့်ထားသည့် အရာအားလုံးကို တွေ့နိုင်သည်။ ထို့ကြောင့် စကားဝှက်၊ ငွေပေးချေမှု အချက်အလက်၊ မက်ဆေ့ဂျ် (သို့) အခြားအရေးကြီးအချက်အလက်များနှင့်ပတ်သက်၍ ဂရုစိုက်ပါ။"</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"အက်ပ်တစ်ခုကို ရိုက်ကူးနေစဉ် Android သည် ၎င်းအက်ပ်တွင် ပြထားသည့် (သို့) ဖွင့်ထားသည့် အရာအားလုံးကို တွေ့နိုင်သည်။ ထို့ကြောင့် စကားဝှက်၊ ငွေပေးချေမှု အချက်အလက်၊ မက်ဆေ့ဂျ် (သို့) အခြားအရေးကြီးအချက်အလက်များနှင့်ပတ်သက်၍ ဂရုစိုက်ပါ။"</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"စတင်ရိုက်ကူးရန်"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"အသံဖမ်းရန်"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"စက်ပစ္စည်းအသံ"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"သီချင်း၊ ဖုန်းခေါ်ဆိုမှုနှင့် ဖုန်းမြည်သံကဲ့သို့ သင့်စက်ပစ္စည်းမှ အသံ"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"ဤဝန်ဆောင်မှုသည် ရိုက်ကူးဖမ်းယူနေစဉ် (သို့) ကာစ်လုပ်နေစဉ်အတွင်း သင့်ဖန်သားပြင်တွင် မြင်ရသော (သို့) သင့်စက်တွင် ဖွင့်ထားသော အချက်အလက်အားလုံးကို ကြည့်နိုင်ပါမည်။ ၎င်းတွင် စကားဝှက်များ၊ ငွေပေးချေမှုအသေးစိတ်များ၊ ဓာတ်ပုံများ၊ မက်ဆေ့ဂျ်များနှင့် သင်ဖွင့်သည့်အသံကဲ့သို့သော အချက်အလက်များ ပါဝင်သည်။"</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"ရိုက်ကူးဖမ်းယူခြင်း (သို့) ကာစ်လုပ်ခြင်း စတင်မလား။"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> နှင့် ဖမ်းယူခြင်း သို့မဟုတ် ကာစ်လုပ်ခြင်း စတင်မလား။"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"မျှဝေရန် (သို့) ရိုက်ကူးရန် <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ကို ခွင့်ပြုမလား။"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"ဖန်သားပြင်တစ်ခုလုံး"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"အက်ပ်တစ်ခုတွင်"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"မျှဝေ၊ ရိုက်ကူး (သို့) ကာစ်လုပ်သည့်အခါ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> သည် သင့်ဖန်သားပြင်ရှိ မြင်နိုင်သည့် (သို့) သင့်စက်တွင် ဖွင့်ထားသည့် အရာအားလုံးကို တွေ့နိုင်သည်။ ထို့ကြောင့် စကားဝှက်၊ ငွေပေးချေမှု အချက်အလက်၊ မက်ဆေ့ဂျ် (သို့) အခြားအရေးကြီးအချက်အလက်များနှင့်ပတ်သက်၍ ဂရုစိုက်ပါ။"</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"အက်ပ်ဖြင့် မျှဝေ၊ ရိုက်ကူး (သို့) ကာစ်လုပ်သည့်အခါ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> သည် ၎င်းအက်ပ်တွင် ပြထားသည့် (သို့) ဖွင့်ထားသည့် အရာအားလုံးကို တွေ့နိုင်သည်။ ထို့ကြောင့် စကားဝှက်၊ ငွေပေးချေမှု အချက်အလက်၊ မက်ဆေ့ဂျ် (သို့) အခြားအရေးကြီးအချက်အလက်များနှင့်ပတ်သက်၍ ဂရုစိုက်ပါ။"</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ရှေ့ဆက်ရန်"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"အက်ပ် မျှဝေခြင်း (သို့) ရိုက်ကူးခြင်း"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"အားလုံးရှင်းရန်"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"စီမံရန်"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"မှတ်တမ်း"</string>
diff --git a/packages/SystemUI/res/values-nb/strings.xml b/packages/SystemUI/res/values-nb/strings.xml
index 6dd9d8f..951eb8c 100644
--- a/packages/SystemUI/res/values-nb/strings.xml
+++ b/packages/SystemUI/res/values-nb/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Vedvarende varsel for et skjermopptak"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Vil du starte et opptak?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Under opptak kan Android-systemet registrere all sensitiv informasjon som er synlig på skjermen eller spilles av på enheten. Dette inkluderer passord, betalingsinformasjon, bilder, meldinger og lyd."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Ta opp hele skjermen"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Ta opp én app"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Når du tar opp noe, har Android tilgang til alt som vises på skjermen eller spilles av på enheten. Derfor bør du være forsiktig med passord, betalingsopplysninger, meldinger og annen sensitiv informasjon."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Når du tar opp en app, har Android tilgang til alt som vises eller spilles av i appen. Derfor bør du være forsiktig med passord, betalingsopplysninger, meldinger og annen sensitiv informasjon."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Start opptaket"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Spill inn lyd"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Enhetslyd"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Lyd fra enheten, f.eks. musikk, samtaler og ringelyder"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Tjenesten som leverer denne funksjonen, får tilgang til all informasjon som er synlig på skjermen din, eller som spilles av fra enheten når du tar opp eller caster. Dette inkluderer informasjon som passord, betalingsopplysninger, bilder, meldinger og lyd du spiller av."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Vil du starte opptak eller casting?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Vil du starte opptak eller casting med <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Vil du gi <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> tillatelse til å dele eller ta opp?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Hele skjermen"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Én app"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Når du deler, tar opp eller caster noe, har <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> tilgang til alt som vises på skjermen eller spilles av på enheten. Derfor bør du være forsiktig med passord, betalingsopplysninger, meldinger og annen sensitiv informasjon."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Når du deler, tar opp eller caster en app, har <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> tilgang til alt som vises eller spilles av i den aktuelle appen. Derfor bør du være forsiktig med passord, betalingsopplysninger, meldinger og annen sensitiv informasjon."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Fortsett"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Del eller ta opp en app"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Fjern alt"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Administrer"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Logg"</string>
diff --git a/packages/SystemUI/res/values-ne/strings.xml b/packages/SystemUI/res/values-ne/strings.xml
index 2362b40..35112a7 100644
--- a/packages/SystemUI/res/values-ne/strings.xml
+++ b/packages/SystemUI/res/values-ne/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"कुनै स्क्रिन रेकर्ड गर्ने सत्रका लागि चलिरहेको सूचना"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"रेकर्ड गर्न थाल्ने हो?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"रेकर्ड गर्दा, Android सिस्टमले तपाईंको स्क्रिनमा देखिने वा तपाईंको डिभाइसमा प्ले गरिने सबै संवेदनशील जानकारी रेकर्ड गर्न सक्छ। यो जानकारीमा पासवर्ड, भुक्तानीसम्बन्धी जानकारी, फोटो, सन्देश र अडियो समावेश हुन्छ।"</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"पूरै स्क्रिन रेकर्ड गर्नुहोस्"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"एउटा एप मात्र रेकर्ड गर्नुहोस्"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"तपाईंले रेकर्ड गर्दा Android ले तपाईंको स्क्रिनमा देखिने वा डिभाइसमा प्ले गरिएका सबै कुरा खिच्न सक्छ। त्यसैले रेकर्ड गर्दा पासवर्ड, भुक्तानीको विवरण, म्यासेज वा अन्य संवेदनशील जानकारी सुरक्षित राख्नुहोला।"</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"तपाईंले रेकर्ड गर्दा Android ले तपाईंको स्क्रिनमा देखिने वा डिभाइसमा प्ले गरिएका सबै कुरा खिच्न सक्छ। त्यसैले रेकर्ड गर्दा पासवर्ड, भुक्तानीको विवरण, म्यासेज वा अन्य संवेदनशील जानकारी सुरक्षित राख्नुहोला।"</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"रेकर्ड गर्न थाल्नुहोस्"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"अडियो रेकर्ड गरियोस्"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"डिभाइसको अडियो"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"तपाईंको डिभाइसका सङ्गीत, कल र रिङटोन जस्ता साउन्ड"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"यो कार्य गर्ने सेवाले तपाईंको स्क्रिनमा देख्न सकिने सबै जानकारी अथवा रेकर्ड वा कास्ट गर्दा तपाईंको डिभाइसबाट प्ले गरिएका कुरा हेर्न तथा प्रयोग गर्न सक्छ। यसले हेर्न तथा प्रयोग गर्न सक्ने कुरामा पासवर्ड, भुक्तानीका विवरण, फोटो, सन्देश र तपाईंले प्ले गर्ने अडियो कुराहरू समावेश हुन सक्छन्।"</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"रेकर्ड गर्न वा cast गर्न थाल्ने हो?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> मार्फत रेकर्ड गर्न वा cast गर्न थाल्ने हो?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> लाई सेयर गर्न वा रेकर्ड गर्न दिने हो?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"पूर्ण स्क्रिन"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"एकल एप"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"तपाईंले सेयर गर्दा, रेकर्ड गर्दा वा कास्ट गर्दा<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ले तपाईंको स्क्रिनमा देखिने वा डिभाइसमा प्ले गरिएका सबै कुरा खिच्न सक्छ। त्यसैले सेयर, रेकर्ड वा कास्ट गर्दा पासवर्ड, भुक्तानीको विवरण, म्यासेज वा अन्य संवेदनशील जानकारी सुरक्षित राख्नुहोला।"</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"तपाईंले सेयर गर्दा, रेकर्ड गर्दा वा कास्ट गर्दा<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ले तपाईंको स्क्रिनमा देखिने वा डिभाइसमा प्ले गरिएका सबै कुरा खिच्न सक्छ। त्यसैले पासवर्ड, भुक्तानीको विवरण, म्यासेज वा अन्य संवेदनशील जानकारी सुरक्षित राख्नुहोला।"</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"जारी राख्नुहोस्"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"सेयर वा रेकर्ड गर्नका लागि एप चयन गर्नुहोस्"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"सबै हटाउनुहोस्"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"व्यवस्थित गर्नुहोस्"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"इतिहास"</string>
diff --git a/packages/SystemUI/res/values-or/strings.xml b/packages/SystemUI/res/values-or/strings.xml
index 01d0d9f..04c3774 100644
--- a/packages/SystemUI/res/values-or/strings.xml
+++ b/packages/SystemUI/res/values-or/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"ଏକ ସ୍କ୍ରିନ୍ ରେକର୍ଡ୍ ସେସନ୍ ପାଇଁ ଚାଲୁଥିବା ବିଜ୍ଞପ୍ତି"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"ରେକର୍ଡିଂ ଆରମ୍ଭ କରିବେ?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"ରେକର୍ଡିଂ ସମୟରେ, Android ସିଷ୍ଟମ୍ ଆପଣଙ୍କ ସ୍କ୍ରିନରେ ଦେଖାଯାଉଥିବା ବା ଆପଣଙ୍କ ଡିଭାଇସରେ ଚାଲୁଥିବା ଯେ କୌଣସି ସମ୍ବେଦନଶୀଳ ସୂଚନାକୁ କ୍ୟାପଚର୍ କରିପାରିବ। ଏଥିରେ ପାସୱାର୍ଡ, ପେମେଣ୍ଟ ସୂଚନା, ଫଟୋ, ମେସେଜ ଏବଂ ଅଡିଓ ଅନ୍ତର୍ଭୁକ୍ତ।"</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"ସମ୍ପୂର୍ଣ୍ଣ ସ୍କ୍ରିନ ରେକର୍ଡ କର"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"ଏକ ସିଙ୍ଗଲ ଆପ ରେକର୍ଡ କରନ୍ତୁ"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"ଆପଣ ରେକର୍ଡିଂ କରିବା ବେଳେ, ଆପଣଙ୍କ ସ୍କ୍ରିନରେ ଦେଖାଯାଉଥିବା କିମ୍ବା ଆପଣଙ୍କ ଡିଭାଇସରେ ପ୍ଲେ ହେଉଥିବା ସବୁକିଛିକୁ Androidର ଆକ୍ସେସ ଅଛି। ତେଣୁ ପାସୱାର୍ଡ, ପେମେଣ୍ଟ ବିବରଣୀ, ମେସେଜ କିମ୍ବା ଅନ୍ୟ ସମ୍ବେଦନଶୀଳ ସୂଚନା ପ୍ରତି ସତର୍କ ରୁହନ୍ତୁ।"</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"ଆପଣ ଏକ ଆପ ରେକର୍ଡିଂ କରିବା ବେଳେ, ସେହି ଆପରେ ଦେଖାଯାଉଥିବା କିମ୍ବା ପ୍ଲେ ହେଉଥିବା ସବୁକିଛିକୁ Androidର ଆକ୍ସେସ ଅଛି। ତେଣୁ ପାସୱାର୍ଡ, ପେମେଣ୍ଟ ବିବରଣୀ, ମେସେଜ କିମ୍ବା ଅନ୍ୟ ସମ୍ବେଦନଶୀଳ ସୂଚନା ପ୍ରତି ସତର୍କ ରୁହନ୍ତୁ।"</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"ରେକର୍ଡିଂ ଆରମ୍ଭ କରନ୍ତୁ"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"ଅଡିଓ ରେକର୍ଡ କରନ୍ତୁ"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"ଡିଭାଇସ୍ ଅଡିଓ"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"ଆପଣଙ୍କ ଡିଭାଇସରୁ ସାଉଣ୍ଡ, ଯେପରିକି ସଙ୍ଗୀତ, କଲ୍ ଏବଂ ରିଂଟୋନଗୁଡ଼ିକ"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"ରେକର୍ଡିଂ ବା କାଷ୍ଟିଂ ବେଳେ ଆପଣଙ୍କର ଡିଭାଇସରେ ଦେଖାଯାଉଥିବା ବା ଆପଣଙ୍କ ଡିଭାଇସରୁ ପ୍ଲେ କରାଯାଉଥିବା ସବୁ ସୂଚନାକୁ ଏହି ଫଙ୍କସନ୍ ପ୍ରଦାନ କରୁଥିବା ସେବାର ଆକ୍ସେସ୍ ରହିବ। ପାସ୍ୱାର୍ଡ, ପେମେଣ୍ଟ ବିବରଣୀ, ଫଟୋ, ମେସେଜ୍ ଏବଂ ଆପଣ ଚଲାଉଥିବା ଅଡିଓ ପରି ସୂଚନା ଏଥିରେ ଅନ୍ତର୍ଭୁକ୍ତ ଅଛି।"</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"ରେକର୍ଡିଂ ବା କାଷ୍ଟିଂ ଆରମ୍ଭ କରିବେ?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ସହ ରେକର୍ଡିଂ ବା କାଷ୍ଟିଂ ଆରମ୍ଭ କରିବେ?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"ସେୟାର କିମ୍ବା ରେକର୍ଡ କରିବା ପାଇଁ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>କୁ ଅନୁମତି ଦେବେ?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"ସମ୍ପୂର୍ଣ୍ଣ ସ୍କ୍ରିନ"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"ଏକ ସିଙ୍ଗଲ ଆପ"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"ଆପଣ ସେୟାର, ରେକର୍ଡ କିମ୍ବା କାଷ୍ଟ କରିବା ସମୟରେ, ଆପଣଙ୍କ ସ୍କ୍ରିନରେ ଦେଖାଯାଉଥିବା କିମ୍ବା ଆପଣଙ୍କ ଡିଭାଇସରେ ପ୍ଲେ ହେଉଥିବା ସବୁକିଛିକୁ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>ର ଆକ୍ସେସ ଅଛି। ତେଣୁ ପାସୱାର୍ଡ, ପେମେଣ୍ଟ ବିବରଣୀ, ମେସେଜ କିମ୍ବା ଅନ୍ୟ ସମ୍ବେଦନଶୀଳ ସୂଚନା ପ୍ରତି ସତର୍କ ରୁହନ୍ତୁ।"</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ଆପଣ ସେୟାର, ରେକର୍ଡ କିମ୍ବା କାଷ୍ଟ କରିବା ସମୟରେ, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ଆପରେ ଦେଖାଯାଉଥିବା କିମ୍ବା ପ୍ଲେ ହେଉଥିବା ସବୁକିଛିକୁ ସେହି ଆପର ଆକ୍ସେସ ଅଛି। ତେଣୁ ପାସୱାର୍ଡ, ପେମେଣ୍ଟ ବିବରଣୀ, ମେସେଜ କିମ୍ବା ଅନ୍ୟ ସମ୍ବେଦନଶୀଳ ସୂଚନା ପ୍ରତି ସତର୍କ ରୁହନ୍ତୁ।"</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ଜାରି ରଖନ୍ତୁ"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ଏକ ଆପକୁ ସେୟାର କିମ୍ବା ରେକର୍ଡ କରନ୍ତୁ"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"ସମସ୍ତ ଖାଲି କରନ୍ତୁ"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"ପରିଚାଳନା କରନ୍ତୁ"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"ଇତିହାସ"</string>
diff --git a/packages/SystemUI/res/values-pa/strings.xml b/packages/SystemUI/res/values-pa/strings.xml
index e9f4cf9..5f9709a 100644
--- a/packages/SystemUI/res/values-pa/strings.xml
+++ b/packages/SystemUI/res/values-pa/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"ਕਿਸੇ ਸਕ੍ਰੀਨ ਰਿਕਾਰਡ ਸੈਸ਼ਨ ਲਈ ਚੱਲ ਰਹੀ ਸੂਚਨਾ"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"ਕੀ ਰਿਕਾਰਡਿੰਗ ਸ਼ੁਰੂ ਕਰਨੀ ਹੈ?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"ਰਿਕਾਰਡਿੰਗ ਕਰਨ ਵੇਲੇ, Android ਸਿਸਟਮ ਕੋਈ ਵੀ ਅਜਿਹੀ ਸੰਵੇਦਨਸ਼ੀਲ ਜਾਣਕਾਰੀ ਕੈਪਚਰ ਕਰ ਸਕਦਾ ਹੈ ਜੋ ਤੁਹਾਡੀ ਸਕ੍ਰੀਨ \'ਤੇ ਦਿਖਣਯੋਗ ਹੈ ਜਾਂ ਤੁਹਾਡੇ ਡੀਵਾਈਸ \'ਤੇ ਚਲਾਈ ਜਾਂਦੀ ਹੈ। ਇਸ ਵਿੱਚ ਪਾਸਵਰਡ, ਭੁਗਤਾਨ ਵੇਰਵੇ, ਫ਼ੋਟੋਆਂ, ਸੁਨੇਹੇ ਅਤੇ ਆਡੀਓ ਸ਼ਾਮਲ ਹਨ।"</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"ਪੂਰੀ ਸਕ੍ਰੀਨ ਨੂੰ ਰਿਕਾਰਡ ਕਰੋ"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"ਇਕਹਿਰੀ ਐਪ ਨੂੰ ਰਿਕਾਰਡ ਕਰੋ"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"ਤੁਹਾਡੇ ਵੱਲੋਂ ਰਿਕਾਰਡਿੰਗ ਕਰਨ ਵੇਲੇ, Android ਕੋਲ ਤੁਹਾਡੀ ਸਕ੍ਰੀਨ \'ਤੇ ਦਿਸਦੀ ਜਾਂ ਤੁਹਾਡੇ ਡੀਵਾਈਸ \'ਤੇ ਚਲਾਈ ਗਈ ਹਰੇਕ ਚੀਜ਼ ਤੱਕ ਪਹੁੰਚ ਹੁੰਦੀ ਹੈ। ਇਸ ਲਈ ਪਾਸਵਰਡਾਂ, ਭੁਗਤਾਨ ਵੇਰਵਿਆਂ, ਸੁਨੇਹਿਆਂ ਜਾਂ ਹੋਰ ਸੰਵੇਦਨਸ਼ੀਲ ਜਾਣਕਾਰੀ ਸੰਬੰਧੀ ਸਾਵਧਾਨ ਰਹੋ।"</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"ਤੁਹਾਡੇ ਵੱਲੋਂ ਰਿਕਾਰਡਿੰਗ ਕਰਨ ਵੇਲੇ, Android ਕੋਲ ਉਸ ਐਪ \'ਤੇ ਦਿਖਾਈ ਗਈ ਜਾਂ ਚਲਾਈ ਗਈ ਹਰੇਕ ਚੀਜ਼ ਤੱਕ ਪਹੁੰਚ ਹੁੰਦੀ ਹੈ। ਇਸ ਲਈ ਪਾਸਵਰਡਾਂ, ਭੁਗਤਾਨ ਵੇਰਵਿਆਂ, ਸੁਨੇਹਿਆਂ ਜਾਂ ਹੋਰ ਸੰਵੇਦਨਸ਼ੀਲ ਜਾਣਕਾਰੀ ਸੰਬੰਧੀ ਸਾਵਧਾਨ ਰਹੋ।"</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"ਰਿਕਾਰਡਿੰਗ ਸ਼ੁਰੂ ਕਰੋ"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"ਆਡੀਓ ਰਿਕਾਰਡ ਕਰੋ"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"ਡੀਵਾਈਸ ਆਡੀਓ"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"ਤੁਹਾਡੇ ਡੀਵਾਈਸ ਦੀ ਧੁਨੀ, ਜਿਵੇਂ ਕਿ ਸੰਗੀਤ, ਕਾਲਾਂ ਅਤੇ ਰਿੰਗਟੋਨਾਂ"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"ਇਹ ਫੰਕਸ਼ਨ ਪ੍ਰਦਾਨ ਕਰਨ ਵਾਲੀ ਸੇਵਾ ਕੋਲ ਸਾਰੀ ਜਾਣਕਾਰੀ ਤੱਕ ਪਹੁੰਚ ਹੋਵੇਗੀ ਜੋ ਕਿ ਤੁਹਾਡੀ ਸਕ੍ਰੀਨ \'ਤੇ ਦਿਖਣਯੋਗ ਹੁੰਦੀ ਹੈ ਜਾਂ ਰਿਕਾਰਡ ਜਾਂ ਕਾਸਟ ਕਰਨ ਵੇਲੇ ਤੁਹਾਡੇ ਡੀਵਾਈਸ \'ਤੇ ਚਲਾਈ ਜਾਂਦੀ ਹੈ। ਇਸ ਵਿੱਚ ਪਾਸਵਰਡ, ਭੁਗਤਾਨ ਵੇਰਵੇ, ਫ਼ੋਟੋਆਂ, ਸੁਨੇਹੇ ਅਤੇ ਤੁਹਾਡੇ ਵੱਲੋਂ ਚਲਾਏ ਆਡੀਓ ਦੀ ਜਾਣਕਾਰੀ ਸ਼ਾਮਲ ਹੁੰਦੀ ਹੈ।"</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"ਕੀ ਰਿਕਾਰਡ ਜਾਂ ਕਾਸਟ ਕਰਨਾ ਸ਼ੁਰੂ ਕਰਨਾ ਹੈ?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ਨਾਲ ਰਿਕਾਰਡਿੰਗ ਜਾਂ ਕਾਸਟ ਕਰਨਾ ਸ਼ੁਰੂ ਕਰਨਾ ਹੈ?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"ਕੀ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ਨੂੰ ਸਾਂਝਾ ਕਰਨ ਜਾਂ ਰਿਕਾਰਡ ਕਰਨ ਲਈ ਆਗਿਆ ਦੇਣੀ ਹੈ?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"ਪੂਰੀ ਸਕ੍ਰੀਨ"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"ਇਕਹਿਰੀ ਐਪ"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"ਤੁਹਾਡੇ ਵੱਲੋਂ ਸਾਂਝਾ ਕਰਨ, ਰਿਕਾਰਡ ਕਰਨ, ਜਾਂ ਕਾਸਟ ਕਰਨ \'ਤੇ, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ਕੋਲ ਤੁਹਾਡੀ ਸਕ੍ਰੀਨ \'ਤੇ ਦਿਸਦੀ ਜਾਂ ਤੁਹਾਡੇ ਡੀਵਾਈਸ \'ਤੇ ਚਲਾਈ ਗਈ ਹਰੇਕ ਚੀਜ਼ ਤੱਕ ਪਹੁੰਚ ਹੁੰਦੀ ਹੈ। ਇਸ ਲਈ ਪਾਸਵਰਡਾਂ, ਭੁਗਤਾਨ ਵੇਰਵਿਆਂ, ਸੁਨੇਹਿਆਂ ਜਾਂ ਹੋਰ ਸੰਵੇਦਨਸ਼ੀਲ ਜਾਣਕਾਰੀ ਸੰਬੰਧੀ ਸਾਵਧਾਨ ਰਹੋ।"</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ਤੁਹਾਡੇ ਵੱਲੋਂ ਸਾਂਝਾ ਕਰਨ, ਰਿਕਾਰਡ ਕਰਨ, ਜਾਂ ਕਾਸਟ ਕਰਨ \'ਤੇ, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ਕੋਲ ਉਸ ਐਪ \'ਤੇ ਦਿਖਾਈ ਗਈ ਜਾਂ ਚਲਾਈ ਗਈ ਹਰੇਕ ਚੀਜ਼ ਤੱਕ ਪਹੁੰਚ ਹੁੰਦੀ ਹੈ। ਇਸ ਲਈ ਪਾਸਵਰਡਾਂ, ਭੁਗਤਾਨ ਵੇਰਵਿਆਂ, ਸੁਨੇਹਿਆਂ ਜਾਂ ਹੋਰ ਸੰਵੇਦਨਸ਼ੀਲ ਜਾਣਕਾਰੀ ਸੰਬੰਧੀ ਸਾਵਧਾਨ ਰਹੋ।"</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ਜਾਰੀ ਰੱਖੋ"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ਐਪ ਨੂੰ ਸਾਂਝਾ ਕਰੋ ਜਾਂ ਰਿਕਾਰਡ ਕਰੋ"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"ਸਭ ਕਲੀਅਰ ਕਰੋ"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"ਪ੍ਰਬੰਧਨ ਕਰੋ"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"ਇਤਿਹਾਸ"</string>
diff --git a/packages/SystemUI/res/values-pl/strings.xml b/packages/SystemUI/res/values-pl/strings.xml
index dad40e1..563f8dc 100644
--- a/packages/SystemUI/res/values-pl/strings.xml
+++ b/packages/SystemUI/res/values-pl/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Stałe powiadomienie o sesji rejestrowania zawartości ekranu"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Rozpocząć nagrywanie?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Podczas nagrywania system Android może rejestrować wszelkie informacje poufne wyświetlane na ekranie lub odtwarzane na urządzeniu. Dotyczy to m.in. haseł, szczegółów płatności, zdjęć, wiadomości i odtwarzanych dźwięków."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Nagrywaj cały ekran"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Nagrywaj pojedynczą aplikację"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Podczas nagrywania Android ma dostęp do wszystkiego, co jest widoczne na ekranie lub odtwarzane na urządzeniu. Zachowaj ostrożność w przypadku haseł, danych do płatności, wiadomości i innych informacji poufnych."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Podczas nagrywania treści z aplikacji Android ma dostęp do wszystkiego, co jest w niej wyświetlane lub odtwarzane. Zachowaj ostrożność w przypadku haseł, danych do płatności, wiadomości i innych informacji poufnych."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Zacznij nagrywać"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Nagraj dźwięk"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Dźwięki z urządzenia"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Dźwięki odtwarzane na urządzeniu, na przykład muzyka, połączenia i dzwonki"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Podczas nagrywania i przesyłania usługa udostępniająca tę funkcję będzie miała dostęp do wszystkich informacji widocznych na ekranie lub odtwarzanych na urządzeniu. Dotyczy to m.in. haseł, szczegółów płatności, zdjęć, wiadomości i odtwarzanych dźwięków."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Rozpocząć nagrywanie lub przesyłanie?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Rozpocząć nagrywanie lub przesyłanie za pomocą aplikacji <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Zezwolić aplikacji <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> na udostępnianie lub nagrywanie?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Cały ekran"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Pojedyncza aplikacja"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Podczas udostępniania, nagrywania lub przesyłania treści aplikacja <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ma dostęp do wszystkiego, co jest widoczne na ekranie lub odtwarzane na urządzeniu. Zachowaj ostrożność w przypadku haseł, danych do płatności, wiadomości i innych informacji poufnych."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Podczas udostępniania, nagrywania lub przesyłania treści aplikacja <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ma dostęp do wszystkiego, co jest w niej wyświetlane lub odtwarzane. Zachowaj ostrożność w przypadku haseł, danych do płatności, wiadomości i innych informacji poufnych."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Dalej"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Udostępnianie i nagrywanie za pomocą aplikacji"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Usuń wszystkie"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Zarządzaj"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Historia"</string>
diff --git a/packages/SystemUI/res/values-pt-rPT/strings.xml b/packages/SystemUI/res/values-pt-rPT/strings.xml
index 9110aa0..085522a 100644
--- a/packages/SystemUI/res/values-pt-rPT/strings.xml
+++ b/packages/SystemUI/res/values-pt-rPT/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Notificação persistente de uma sessão de gravação de ecrã"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Iniciar a gravação?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Enquanto estiver a gravar, o sistema Android pode capturar quaisquer informações confidenciais que estejam visíveis no ecrã ou que sejam reproduzidas no dispositivo. Isto inclui palavras-passe, informações de pagamento, fotos, mensagens e áudio."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Gravar o ecrã inteiro"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Gravar só uma app"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Enquanto está a gravar, o Android tem acesso a tudo o que está visível no seu ecrã ou é reproduzido no seu dispositivo. Por isso, tenha cuidado com palavras-passe, detalhes de pagamento, mensagens ou outras informações confidenciais."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Enquanto está a gravar uma app, o Android tem acesso a tudo o que é apresentado ou reproduzido nessa app. Por isso, tenha cuidado com palavras-passe, detalhes de pagamento, mensagens ou outras informações confidenciais."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Começar gravação"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Gravar áudio"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Áudio do dispositivo"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"O som do dispositivo, como música, chamadas e toques."</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"O serviço que fornece esta função terá acesso a todas as informações que estiverem visíveis no ecrã ou que forem reproduzidas a partir do dispositivo durante a gravação ou transmissão. Isto inclui informações como palavras-passe, detalhes de pagamentos, fotos, mensagens e áudio reproduzido."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Começar a gravar ou a transmitir?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Começar a gravar ou a transmitir com a app <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Permitir que a app <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> partilhe ou grave?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Ecrã inteiro"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Só uma app"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Quando está a partilhar, gravar ou transmitir, a app <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> tem acesso a tudo o que está visível no seu ecrã ou é reproduzido no seu dispositivo. Por isso, tenha cuidado com palavras-passe, detalhes de pagamento, mensagens ou outras informações confidenciais."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Quando está a partilhar, gravar ou transmitir uma app, a app <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> tem acesso a tudo o que é apresentado ou reproduzido nessa app. Por isso, tenha cuidado com palavras-passe, detalhes de pagamento, mensagens ou outras informações confidenciais."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuar"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Partilhe ou grave uma app"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Limpar tudo"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Gerir"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Histórico"</string>
diff --git a/packages/SystemUI/res/values-ro/strings.xml b/packages/SystemUI/res/values-ro/strings.xml
index f82954e..0db2dd5 100644
--- a/packages/SystemUI/res/values-ro/strings.xml
+++ b/packages/SystemUI/res/values-ro/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Notificare în curs pentru o sesiune de înregistrare a ecranului"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Începi înregistrarea?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"În timpul înregistrării, sistemul Android poate captura informațiile sensibile vizibile pe ecran sau redate pe dispozitiv. Aici sunt incluse parole, informații de plată, fotografii, mesaje și conținut audio."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Înregistrează tot ecranul"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Înregistrează doar o aplicație"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Când înregistrezi, Android are acces la orice este vizibil pe ecran sau se redă pe dispozitiv. Ai grijă cu parolele, detaliile de plată, mesajele sau alte informații sensibile."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Când înregistrezi o aplicație, Android are acces la orice se afișează sau se redă în aplicație. Ai grijă cu parolele, detaliile de plată, mesajele sau alte informații sensibile."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Începe înregistrarea"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Înregistrează audio"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Conținutul audio de la dispozitiv"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Sunetul de la dispozitiv, precum muzică, apeluri și tonuri de sonerie"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Serviciul care oferă această funcție va avea acces la toate informațiile vizibile pe ecran sau redate pe dispozitiv în timp ce înregistrezi sau proiectezi. Între aceste informații se numără parole, detalii de plată, fotografii, mesaje și conținutul audio pe care îl redai."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Începi să înregistrezi sau să proiectezi?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Începi să înregistrezi sau să proiectezi cu <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> poate permite accesul sau înregistra?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Tot ecranul"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"O singură aplicație"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Când permiți accesul, înregistrezi sau proiectezi, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> are acces la orice este vizibil pe ecran sau se redă pe dispozitiv. Ai grijă cu parolele, detaliile de plată, mesajele sau alte informații sensibile."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Când permiți accesul, înregistrezi sau proiectezi o aplicație, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> are acces la orice se afișează pe ecran sau se redă în aplicație. Ai grijă cu parolele, detaliile de plată, mesajele sau alte informații sensibile."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuă"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Permite accesul la o aplicație sau înregistreaz-o"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Șterge toate notificările"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Gestionează"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Istoric"</string>
diff --git a/packages/SystemUI/res/values-ru/strings.xml b/packages/SystemUI/res/values-ru/strings.xml
index 84b5061..7dcf873 100644
--- a/packages/SystemUI/res/values-ru/strings.xml
+++ b/packages/SystemUI/res/values-ru/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Текущее уведомление для записи видео с экрана"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Начать запись?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"В записи может появиться конфиденциальная информация, которая видна на экране или воспроизводится на устройстве, например пароли, сведения о платежах, фотографии, сообщения и аудиозаписи."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Записывать весь экран"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Записывать окно приложения"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Когда вы записываете видео с экрана, Android получает доступ ко всему, что видно и воспроизводится на экране устройства. Помните об этом, если соберетесь вводить или просматривать пароли, платежные данные, сообщения и другую конфиденциальную информацию."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Когда вы записываете видео с окна приложения, Android получает доступ ко всему, что видно и воспроизводится в приложении. Помните об этом, если соберетесь вводить или просматривать пароли, платежные данные, сообщения и другую конфиденциальную информацию."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Начать запись"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Записывать аудио"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Звук с устройства"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Звук с вашего устройства, например музыка, звонки и рингтоны"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Во время записи или трансляции у сервиса, предоставляющего эту функцию, будет доступ ко всей информации, которая видна на экране или воспроизводится на устройстве, включая пароли, сведения о платежах, фотографии, сообщения и звуки."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Начать запись или трансляцию?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Начать запись или трансляцию через приложение \"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>\"?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Разрешить приложению \"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>\" демонстрировать экран или записывать видео с него?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Весь экран"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Отдельное приложение"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Когда вы демонстрируете, транслируете экран или записываете видео с него, приложение \"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>\" получает доступ ко всему, что видно и воспроизводится на экране устройства. Помните об этом, если соберетесь вводить или просматривать пароли, платежные данные, сообщения и другую конфиденциальную информацию."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Когда вы демонстрируете, транслируете экран или записываете видео с него, приложение \"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>\" получает доступ ко всему, что видно и воспроизводится на экране устройства. Помните об этом, если соберетесь вводить или просматривать пароли, платежные данные, сообщения и другую конфиденциальную информацию."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Далее"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Демонстрация экрана или запись видео с него"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Очистить все"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Настроить"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"История"</string>
diff --git a/packages/SystemUI/res/values-si/strings.xml b/packages/SystemUI/res/values-si/strings.xml
index 1ccdbf3..de498cb 100644
--- a/packages/SystemUI/res/values-si/strings.xml
+++ b/packages/SystemUI/res/values-si/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"තිර පටිගත කිරීමේ සැසියක් සඳහා කෙරෙන දැනුම් දීම"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"පටිගත කිරීම ආරම්භ කරන්නද?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"පටිගත කරන අතරතුර, Android පද්ධතියට ඔබේ තිරයේ පෙනෙන හෝ ඔබේ උපාංගයේ වාදනය කරන ඕනෑම සංවේදී තොරතුරක් ග්රහණය කර ගැනීමට හැකිය. මෙයට මුරපද, ගෙවීම් තොරතුරු, ඡායාරූප, පණිවිඩ සහ ඕඩියෝ ඇතුළත් වේ."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"සම්පූර්ණ තිරය පටිගත කරන්න"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"තනි යෙදුමක් පටිගත කරන්න"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"ඔබ පටිගත කරන අතරේ, Android හට ඔබේ තිරයේ පෙනෙන හෝ ඔබේ උපාංගයේ වාදනය වන ඕනෑම දෙයකට ප්රවේශය ඇත. එබැවින් මුරපද, ගෙවීම් විස්තර, පණිවිඩ හෝ වෙනත් සංවේදී තොරතුරු සමග ප්රවේශම් වන්න."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"ඔබ යෙදුමක් පටිගත කරන අතරේ, Android හට එම යෙදුමේ පෙන්වන හෝ වාදනය කරන ඕනෑම දෙයකට ප්රවේශය ඇත. එබැවින් මුරපද, ගෙවීම් විස්තර, පණිවිඩ හෝ වෙනත් සංවේදී තොරතුරු සමග ප්රවේශම් වන්න."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"පටිගත කිරීම අරඹන්න"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"ඕඩියෝ පටිගත කරන්න"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"උපාංග ඕඩියෝ"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"සංගීතය, ඇමතුම් සහ නාද රිද්ම වැනි ඔබේ උපාංගය වෙතින් ශබ්ද"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"මෙම ශ්රිතය සපයන සේවාවට පටිගත කරන හෝ විකාශ කරන අතරතුර ඔබේ තිරයේ දිස් වන හෝ ඔබේ උපාංගයෙන් වාදනය කරන සියලු තොරතුරු වෙත ප්රවේශය ලැබෙනු ඇත. මෙහි මුරපද, ගෙවීම් විස්තර, ඡායාරූප, පණිවිඩ සහ ඔබ වාදනය කරන ඕඩියෝ යනාදි තොරතුරු ඇතුළත් වේ."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"පටිගත කිරීම හෝ විකාශය කිරීම ආරම්භ කරන්නද?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> සමග පටිගත කිරීම හෝ විකාශය කිරීම ආරම්භ කරන්නද?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> හට බෙදා ගැනීමට හෝ පටිගත කිරීමට ඉඩ දෙන්න ද?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"සම්පූර්ණ තිරය"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"තනි යෙදුමක්"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"ඔබ බෙදා ගන්නා විට, පටිගත කරන විට, හෝ විකාශනය කරන විට, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> හට ඔබේ තිරයේ පෙනෙන හෝ ඔබේ උපාංගයේ වාදනය වන ඕනෑම දෙයකට ප්රවේශය ඇත. එබැවින් මුරපද, ගෙවීම් විස්තර, පණිවිඩ හෝ වෙනත් සංවේදී තොරතුරු සමග ප්රවේශම් වන්න."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ඔබ යෙදුමක් බෙදා ගන්නා විට, පටිගත කරන විට හෝ විකාශය කරන විට, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> හට එම යෙදුමේ පෙන්වන හෝ වාදනය කරන ඕනෑම දෙයකට ප්රවේශය ඇත. එබැවින් මුරපද, ගෙවීම් විස්තර, පණිවිඩ හෝ වෙනත් සංවේදී තොරතුරු සමග ප්රවේශම් වන්න."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ඉදිරියට යන්න"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"යෙදුමක් බෙදා ගන්න හෝ පටිගත කරන්න"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"සියල්ල හිස් කරන්න"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"කළමනාකරණය කරන්න"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"ඉතිහාසය"</string>
diff --git a/packages/SystemUI/res/values-sq/strings.xml b/packages/SystemUI/res/values-sq/strings.xml
index dcebddb..c1a83db 100644
--- a/packages/SystemUI/res/values-sq/strings.xml
+++ b/packages/SystemUI/res/values-sq/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Njoftim i vazhdueshëm për një seancë regjistrimi të ekranit"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Të niset regjistrimi?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Gjatë regjistrimit, sistemi Android mund të regjistrojë çdo informacion delikat që është i dukshëm në ekranin tënd ose që luhet në pajisje. Kjo përfshin fjalëkalimet, informacionin e pagesave, fotografitë, mesazhet dhe audion."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Regjistro të gjithë ekranin"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Regjistro vetëm një aplikacion"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Gjatë regjistrimit, Android ka qasje te çdo gjë e dukshme në ekranin tënd ose që po luhet në pajisjen tënde. Prandaj ki kujdes me fjalëkalimet, detajet e pagesës, mesazhet ose informacione të tjera të ndjeshme."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Gjatë regjistrimit të një aplikacioni, Android ka qasje te çdo gjë e dukshme ose që po luhet në atë aplikacion. Prandaj, ki kujdes me fjalëkalimet, detajet e pagesës, mesazhet ose informacione të tjera të ndjeshme."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Nis regjistrimin"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Regjistro audio"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Audioja e pajisjes"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Tingulli nga pajisja, si muzika, telefonatat dhe tonet e ziles"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Shërbimi që e ofron këtë funksion do të ketë qasje te të gjitha informacionet që janë të dukshme në ekran ose që luhen nga pajisja jote gjatë regjistrimit ose transmetimit. Kjo përfshin informacione, si p.sh.: fjalëkalimet, detajet e pagesave, fotografitë, mesazhet dhe audion që luan ti."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Do të fillosh regjistrimin ose transmetimin?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Fillo regjistrimin ose transmetimin me <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Të lejohet <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> të shpërndajë ose regjistrojë?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Ekran i plotë"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Vetëm një aplikacion"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Gjatë shpërndarjes, regjistrimit ose transmetimit, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ka qasje te çdo gjë e dukshme në ekranin tënd ose që po luhet në pajisjen tënde. Prandaj ki kujdes me fjalëkalimet, detajet e pagesës, mesazhet ose informacione të tjera të ndjeshme."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Gjatë shpërndarjes, regjistrimit ose transmetimit të një aplikacioni, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ka qasje te çdo gjë e dukshme në ekranin tënd ose që po luhet në atë aplikacion. Prandaj, ki kujdes me fjalëkalimet, detajet e pagesës, mesazhet ose informacione të tjera të ndjeshme."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Vazhdo"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Shpërndaj ose regjistro një aplikacion"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Pastroji të gjitha"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Menaxho"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Historiku"</string>
diff --git a/packages/SystemUI/res/values-sv/strings.xml b/packages/SystemUI/res/values-sv/strings.xml
index 77f0d30..1778b01 100644
--- a/packages/SystemUI/res/values-sv/strings.xml
+++ b/packages/SystemUI/res/values-sv/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Avisering om att skärminspelning pågår"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Vill du starta inspelningen?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"När du spelar in kan Android-systemet registrera alla känsliga uppgifter som visas på skärmen eller spelas upp på enheten. Detta omfattar lösenord, betalningsuppgifter, foton, meddelanden och ljud."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Spela in hela skärmen"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Spela in en enda app"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"När du spelar in har Android åtkomst till allt som visas på skärmen eller spelas upp på enheten. Så var försiktig med lösenord, betalningsuppgifter, meddelanden och andra känsliga uppgifter."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"När du spelar in en app har Android åtkomst till allt som visas eller spelas upp i appen. Så var försiktig med lösenord, betalningsuppgifter, meddelanden och andra känsliga uppgifter."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Börja spela in"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Spela in ljud"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Ljud på enheten"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Ljud från enheten, till exempel musik, samtal och ringsignaler"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Den tjänst som tillhandahåller funktionen får åtkomst till all information som visas på skärmen eller spelas upp från enheten när du spelar in eller castar. Detta omfattar uppgifter som lösenord, betalningsinformation, foton, meddelanden och ljud som du spelar upp."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Vill du börja spela in eller casta?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Vill du börja spela in eller casta med <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Vill du tillåta att <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> delar eller spelar in?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Hela skärmen"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"En enda app"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"När du delar, spelar in eller castar har <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> åtkomst till allt som visas på skärmen eller spelas upp på enheten. Så var försiktig med lösenord, betalningsuppgifter, meddelanden och andra känsliga uppgifter."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"När du delar, spelar in eller castar en app har <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> åtkomst till allt som visas eller spelas upp i appen. Så var försiktig med lösenord, betalningsuppgifter, meddelanden och andra känsliga uppgifter."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Fortsätt"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Dela eller spela in en app"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Rensa alla"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Hantera"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Historik"</string>
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-ta/strings.xml b/packages/SystemUI/res/values-ta/strings.xml
index c823f04..dbb90b3 100644
--- a/packages/SystemUI/res/values-ta/strings.xml
+++ b/packages/SystemUI/res/values-ta/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"திரை ரெக்கார்டிங் அமர்விற்கான தொடர் அறிவிப்பு"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"ரெக்கார்டிங்கைத் தொடங்கவா?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"ரெக்கார்டு செய்யும்போது, உங்கள் திரையில் தோன்றக்கூடிய அல்லது சாதனத்தில் பிளே ஆகக்கூடிய பாதுகாக்கப்பட வேண்டிய தகவலை Android சிஸ்டம் படமெடுக்க முடியும். கடவுச்சொற்கள், பேமெண்ட் தகவல், படங்கள், மெசேஜ்கள், ஆடியோ ஆகியவை இதில் அடங்கும்."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"முழு திரையை ரெக்கார்டு செய்தல்"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"ஓர் ஆப்ஸை ரெக்கார்டு செய்தல்"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"நீங்கள் ரெக்கார்டு செய்யும்போது அந்தச் சாதனத்தில் காட்டப்படும் அல்லது பிளே செய்யப்படும் அனைத்தையும் Android அணுக முடியும். எனவே கடவுச்சொற்கள், பேமெண்ட் விவரங்கள், மெசேஜ்கள், பிற பாதுகாக்கப்பட வேண்டிய தகவல்கள் ஆகியவை குறித்து கவனத்துடன் இருங்கள்."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"ஓர் ஆப்ஸை நீங்கள் ரெக்கார்டு செய்யும்போது அந்த ஆப்ஸில் காட்டப்படும் அல்லது பிளே செய்யப்படும் அனைத்தையும் Android அணுக முடியும். எனவே கடவுச்சொற்கள், பேமெண்ட் விவரங்கள், மெசேஜ்கள், பிற பாதுகாக்கப்பட வேண்டிய தகவல்கள் ஆகியவை குறித்து கவனத்துடன் இருங்கள்."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"ரெக்கார்டிங்கைத் தொடங்கு"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"ஆடியோவை ரெக்கார்டு செய்"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"சாதன ஆடியோ"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"இசை, அழைப்புகள், ரிங்டோன்கள் போன்ற உங்கள் சாதனத்திலிருந்து வரும் ஒலி"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"இந்தச் செயல்பாட்டை வழங்கும் சேவையானது உங்கள் திரையில் தெரியும் தகவல்கள், ரெக்கார்டு செய்யும்போதோ அனுப்பும்போதோ உங்கள் சாதனத்திலிருந்து பிளே ஆகும் அனைத்துத் தகவல்கள் ஆகியவற்றுக்கான அணுகலைக் கொண்டிருக்கும். கடவுச்சொற்கள், பேமெண்ட் தொடர்பான தகவல்கள், படங்கள், மெசேஜ்கள், நீங்கள் பிளே செய்யும் ஆடியோ போன்ற அனைத்துத் தகவல்களும் இதில் அடங்கும்."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"ரெக்கார்டிங் செய்யவோ அனுப்புவோ தொடங்கவா?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> மூலம் ரெக்கார்டிங் செய்யவோ அனுப்புவதற்கோ தொடங்கிவீட்டீர்களா?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"பகிர அல்லது ரெக்கார்டு செய்ய <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ஆப்ஸை அனுமதிக்கலாமா?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"திரை முழுவதும்"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"ஓர் ஆப்ஸ்"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"நீங்கள் பகிரும்போதோ ரெக்கார்டு செய்யும்போதோ அலைபரப்பும்போதோ உங்கள் சாதனத்தில் காட்டப்படும் அல்லது பிளே செய்யப்படும் அனைத்தையும் <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ஆப்ஸால் அணுக முடியும். எனவே கடவுச்சொற்கள், பேமெண்ட் விவரங்கள், மெசேஜ்கள், பிற பாதுகாக்கப்பட வேண்டிய தகவல்கள் ஆகியவை குறித்து கவனத்துடன் இருங்கள்."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ஓர் ஆப்ஸை நீங்கள் பகிரும்போதோ ரெக்கார்டு செய்யும்போதோ அலைபரப்பும்போதோ அந்த ஆப்ஸில் காட்டப்படும் அல்லது பிளே செய்யப்படும் அனைத்தையும் <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ஆப்ஸால் அணுக முடியும். எனவே கடவுச்சொற்கள், பேமெண்ட் விவரங்கள், மெசேஜ்கள், பிற பாதுகாக்கப்பட வேண்டிய தகவல்கள் ஆகியவை குறித்து கவனத்துடன் இருங்கள்."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"தொடர்க"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ஆப்ஸைப் பகிர்தல் அல்லது ரெக்கார்டு செய்தல்"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"எல்லாவற்றையும் அழி"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"நிர்வகி"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"இதுவரை வந்த அறிவிப்புகள்"</string>
diff --git a/packages/SystemUI/res/values-tr/strings.xml b/packages/SystemUI/res/values-tr/strings.xml
index e911bb2..a238f3f 100644
--- a/packages/SystemUI/res/values-tr/strings.xml
+++ b/packages/SystemUI/res/values-tr/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Ekran kaydı oturumu için devam eden bildirim"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Kayıt başlatılsın mı?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Kayıt sırasında Android Sistemi, ekranınızda görünen veya cihazınızda oynatılan hassas bilgileri yakalayabilir. Buna şifreler, ödeme bilgileri, fotoğraflar, mesajlar ve sesler dahildir."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Tüm ekranı kaydedin"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Tek bir uygulamayı kaydedin"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Kayıt özelliğini kullandığınızda Android, ekranınızda görünen veya cihazınızda oynatılan her şeye erişebilir. Dolayısıyla şifreler, ödeme ayrıntıları, mesajlar veya diğer hassas bilgiler konusunda dikkatli olun."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Bir uygulamayı kaydetme özelliğini kullandığınızda Android, söz konusu uygulamada gösterilen veya oynatılan her şeye erişebilir. Dolayısıyla şifreler, ödeme ayrıntıları, mesajlar veya diğer hassas bilgiler konusunda dikkatli olun."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Kaydı başlat"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Ses kaydet"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Cihaz sesi"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Müzik, aramalar, zil sesleri gibi cihazınızdan sesler"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Bu işlevi sağlayan hizmet, ekranınızda görünen veya kayıt ya da yayın sırasında cihazınızdan oynatılan tüm bilgilere erişecektir. Bu bilgiler arasında şifreler, ödeme detayları, fotoğraflar, mesajlar ve çaldığınız sesler gibi bilgiler yer alır."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Kayıt veya yayınlama başlatılsın mı?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ile kayıt veya yayınlama başlatılsın mı?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> uygulamasının paylaşmasına veya kaydetmesine izin verilsin mi?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Tüm ekran"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Tek bir uygulama"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Paylaşım, kayıt ve yayınlama özelliklerini kullandığınızda <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>, ekranınızda görünen veya cihazınızda oynatılan her şeye erişebilir. Dolayısıyla şifreler, ödeme ayrıntıları, mesajlar veya diğer hassas bilgiler konusunda dikkatli olun."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Bir uygulamayı paylaşma, kaydetme ve yayınlama özelliklerini kullandığınızda <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>, söz konusu uygulamada gösterilen veya oynatılan her şeye erişebilir. Dolayısıyla şifreler, ödeme ayrıntıları, mesajlar veya diğer hassas bilgiler konusunda dikkatli olun."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Devam"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Uygulamayı paylaşın veya kaydedin"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Tümünü temizle"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Yönet"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Geçmiş"</string>
diff --git a/packages/SystemUI/res/values-uk/strings.xml b/packages/SystemUI/res/values-uk/strings.xml
index 011c8bc..d3f392a 100644
--- a/packages/SystemUI/res/values-uk/strings.xml
+++ b/packages/SystemUI/res/values-uk/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"Сповіщення про сеанс запису екрана"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"Почати запис?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"Під час запису система Android може фіксувати будь-яку конфіденційну інформацію, яка з\'являється на екрані або відтворюється на пристрої, зокрема паролі, платіжну інформацію, фотографії, повідомлення та звуки."</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"Записувати весь екран"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"Записувати окремий додаток"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"Коли ви записуєте вміст екрана, ОС Android отримує доступ до всього, що відображається на ньому або відтворюється на пристрої. Тому будьте уважні з паролями, повідомленнями, платіжною й іншою конфіденційною інформацією."</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"Коли ви записуєте додаток, ОС Android отримує доступ до всього, що відображається або відтворюється в цьому додатку. Тому будьте уважні з паролями, повідомленнями, платіжною й іншою конфіденційною інформацією."</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"Почати запис"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"Записувати звук"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"Звук із пристрою"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"Звук із пристрою, зокрема музика, виклики та сигнали дзвінка"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"Сервіс, що надає цю функцію, матиме доступ до всієї інформації, яка з\'являється на екрані або відтворюється на пристрої під час запису чи трансляції, зокрема до паролів, інформації про платежі, фотографій, повідомлень і аудіофайлів."</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"Почати запис або трансляцію?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"Почати запис або трансляцію за допомогою додатка <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"Дозволити додатку <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> показувати або записувати?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"Увесь екран"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"Окремий додаток"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"Коли ви показуєте, записуєте або транслюєте екран, додаток <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> отримує доступ до всього, що відображається на екрані чи відтворюється на пристрої. Тому будьте уважні з паролями, повідомленнями, платіжною й іншою конфіденційною інформацією."</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Коли ви показуєте, записуєте або транслюєте додаток, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> отримує доступ до всього, що відображається або відтворюється в цьому додатку. Тому будьте уважні з паролями, повідомленнями, платіжною й іншою конфіденційною інформацією."</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Продовжити"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Показувати або записувати додаток"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"Очистити все"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"Керувати"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"Історія"</string>
diff --git a/packages/SystemUI/res/values-ur/strings.xml b/packages/SystemUI/res/values-ur/strings.xml
index 7d0df6c..1e71b14 100644
--- a/packages/SystemUI/res/values-ur/strings.xml
+++ b/packages/SystemUI/res/values-ur/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"اسکرین ریکارڈ سیشن کیلئے جاری اطلاع"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"ریکارڈنگ شروع کریں؟"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"ریکارڈ کرنے کے دوران، Android سسٹم آپ کی اسکرین پر نظر آنے والی یا آپ کے آلہ پر چلنے والی کسی بھی حساس معلومات کو کیپچر کر سکتا ہے۔ اس میں پاس ورڈز، ادائیگی کی معلومات، تصاویر، پیغامات اور آڈیو شامل ہیں۔"</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"پوری اسکرین کو ریکارڈ کریں"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"واحد ایپ کو ریکارڈ کریں"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"جب آپ ریکارڈنگ کر رہے ہوتے ہیں تو Android کو آپ کی اسکرین پر دکھائی دینے والی یا آپ کے آلے پر چلائی گئی ہر چیز تک رسائی حاصل ہوتی ہے۔ اس لیے پاس ورڈز، ادائیگی کی تفصیلات، پیغامات، یا دیگر حساس معلومات کے سلسلے میں محتاط رہیں۔"</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"جب آپ کسی ایپ کو ریکارڈ کر رہے ہوتے ہیں تو Android کو اس ایپ پر دکھائی گئی یا چلائی گئی ہر چیز تک رسائی حاصل ہوتی ہے۔ اس لیے پاس ورڈز، ادائیگی کی تفصیلات، پیغامات یا دیگر حساس معلومات کے سلسلے میں محتاط رہیں۔"</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"ریکارڈنگ شروع کریں"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"آڈیو ریکارڈ کریں"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"آلہ کا آڈیو"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"آپ کے آلے سے آواز، جیسے موسیقی، کالز اور رِنگ ٹونز"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"یہ فنکشن فراہم کرنے والی سروس کو اس تمام معلومات تک رسائی حاصل ہوگی جو آپ کی اسکرین پر نظر آتی ہے یا ریکارڈنگ یا کاسٹنگ کے دوران آپ کے آلے سے چلائی جاتی ہے۔ اس میں پاس ورڈز، ادائیگی کی تفصیلات، تصاویر، پیغامات اور وہ آڈیو جو آپ چلاتے ہیں جیسی معلومات شامل ہے۔"</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"ریکارڈنگ یا کاسٹنگ شروع کریں؟"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> کے ذریعے ریکارڈنگ یا کاسٹنگ شروع کریں؟"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> کو اشتراک یا ریکارڈ کرنے کی اجازت دیں؟"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"پوری اسکرین"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"واحد ایپ"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"جب آپ اشتراک، ریکارڈنگ یا کاسٹ کر رہے ہوتے ہیں تو <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> کو آپ کی اسکرین پر دکھائی دینے والی یا آپ کے آلے پر چلائی گئی ہر چیز تک رسائی حاصل ہوتی ہے۔ اس لیے پاس ورڈز، ادائیگی کی تفصیلات، پیغامات، یا دیگر حساس معلومات کے سلسلے میں محتاط رہیں۔"</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"جب آپ اشتراک، ریکارڈنگ یا کاسٹ کر رہے ہوتے ہیں تو <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> کو آپ کی اسکرین پر دکھائی گئی یا آپ کے آلے پر چلائی گئی ہر چیز تک رسائی حاصل ہوتی ہے۔ اس لیے پاس ورڈز، ادائیگی کی تفصیلات، پیغامات، یا دیگر حساس معلومات کے سلسلے میں محتاط رہیں۔"</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"جاری رکھیں"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ایپ کا اشتراک یا ریکارڈ کریں"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"سبھی کو صاف کریں"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"نظم کریں"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"سرگزشت"</string>
diff --git a/packages/SystemUI/res/values-zh-rCN/strings.xml b/packages/SystemUI/res/values-zh-rCN/strings.xml
index d881fa2..87821bf 100644
--- a/packages/SystemUI/res/values-zh-rCN/strings.xml
+++ b/packages/SystemUI/res/values-zh-rCN/strings.xml
@@ -94,16 +94,11 @@
<string name="screenrecord_channel_description" msgid="4147077128486138351">"持续显示屏幕录制会话通知"</string>
<string name="screenrecord_start_label" msgid="1750350278888217473">"要开始录制吗?"</string>
<string name="screenrecord_description" msgid="1123231719680353736">"在录制内容时,Android 系统可以捕捉到您屏幕上显示或设备中播放的敏感信息,其中包括密码、付款信息、照片、消息和音频。"</string>
- <!-- no translation found for screenrecord_option_entire_screen (1732437834603426934) -->
- <skip />
- <!-- no translation found for screenrecord_option_single_app (5954863081500035825) -->
- <skip />
- <!-- no translation found for screenrecord_warning_entire_screen (8141407178104195610) -->
- <skip />
- <!-- no translation found for screenrecord_warning_single_app (7760723997065948283) -->
- <skip />
- <!-- no translation found for screenrecord_start_recording (348286842544768740) -->
- <skip />
+ <string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"录制整个屏幕"</string>
+ <string name="screenrecord_option_single_app" msgid="5954863081500035825">"录制单个应用"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"在您进行录制时,Android 可以访问您的屏幕显示或设备播放的所有内容。因此,请注意保护密码、付款信息、消息或其他敏感信息。"</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"在您录制某个应用时,Android 可以访问此应用显示或播放的所有内容。因此,请注意保护密码、付款信息、消息或其他敏感信息。"</string>
+ <string name="screenrecord_start_recording" msgid="348286842544768740">"开始录制"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"录制音频"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"设备音频"</string>
<string name="screenrecord_device_audio_description" msgid="4922694220572186193">"设备发出的声音,例如音乐、通话和铃声"</string>
@@ -371,20 +366,13 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"在录制或投放内容时,提供此功能的服务将可获取您屏幕上显示或设备中播放的所有信息,其中包括密码、付款明细、照片、消息以及您播放的音频等信息。"</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"要开始录制或投放内容吗?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"要开始使用<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>录制或投放内容吗?"</string>
- <!-- no translation found for media_projection_permission_dialog_title (7130975432309482596) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_entire_screen (392086473225692983) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_option_single_app (1591110238124910521) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_entire_screen (3989078820637452717) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_warning_single_app (1659532781536753059) -->
- <skip />
- <!-- no translation found for media_projection_permission_dialog_continue (1827799658916736006) -->
- <skip />
- <!-- no translation found for media_projection_permission_app_selector_title (894251621057480704) -->
- <skip />
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"允许 <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 分享或录制吗?"</string>
+ <string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"整个屏幕"</string>
+ <string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"单个应用"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"在您进行分享、录制或投射时,<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 可以访问您的屏幕显示或设备播放的所有内容。因此,请注意保护密码、付款信息、消息或其他敏感信息。"</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"在您进行分享、录制或投射时,<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 可以访问通过此应用显示或播放的所有内容。因此,请注意保护密码、付款信息、消息或其他敏感信息。"</string>
+ <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"继续"</string>
+ <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"分享或录制应用"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"全部清除"</string>
<string name="manage_notifications_text" msgid="6885645344647733116">"管理"</string>
<string name="manage_notifications_history_text" msgid="57055985396576230">"历史记录"</string>
diff --git a/packages/SystemUI/res/values-zh-rHK/strings.xml b/packages/SystemUI/res/values-zh-rHK/strings.xml
index d934786..c703619 100644
--- a/packages/SystemUI/res/values-zh-rHK/strings.xml
+++ b/packages/SystemUI/res/values-zh-rHK/strings.xml
@@ -96,8 +96,8 @@
<string name="screenrecord_description" msgid="1123231719680353736">"錄影時,Android 系統可擷取螢幕上顯示或裝置播放的任何敏感資料,包括密碼、付款資料、相片、訊息和音訊。"</string>
<string name="screenrecord_option_entire_screen" msgid="1732437834603426934">"錄製整個螢幕畫面"</string>
<string name="screenrecord_option_single_app" msgid="5954863081500035825">"錄製單一應用程式"</string>
- <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"進行錄製時,Android 可以存取顯示在螢幕畫面上或在裝置上播放的所有內容。因此請謹慎處理密碼、付款資料、訊息或其他機密資訊。"</string>
- <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"錄製應用程式時,Android 可以存取在該應用程式中顯示或播放的所有內容。因此請謹慎處理密碼、付款資料、訊息或其他機密資訊。"</string>
+ <string name="screenrecord_warning_entire_screen" msgid="8141407178104195610">"進行錄製時,Android 可存取顯示在螢幕畫面上或在裝置上播放的所有內容。因此請謹慎處理密碼、付款資料、訊息或其他敏感資料。"</string>
+ <string name="screenrecord_warning_single_app" msgid="7760723997065948283">"錄製應用程式時,Android 可存取在該應用程式中顯示或播放的所有內容。因此請謹慎處理密碼、付款資料、訊息或其他敏感資料。"</string>
<string name="screenrecord_start_recording" msgid="348286842544768740">"開始錄製"</string>
<string name="screenrecord_audio_label" msgid="6183558856175159629">"錄音"</string>
<string name="screenrecord_device_audio_label" msgid="9016927171280567791">"裝置音訊"</string>
@@ -366,11 +366,11 @@
<string name="media_projection_dialog_service_text" msgid="958000992162214611">"在錄影或投放時,此功能的服務供應商可以存取螢幕顯示或裝置播放的任何資料,當中包括密碼、付款詳情、相片、訊息和播放的語音等。"</string>
<string name="media_projection_dialog_service_title" msgid="2888507074107884040">"要開始錄影或投放嗎?"</string>
<string name="media_projection_dialog_title" msgid="3316063622495360646">"要使用「<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>」開始錄影或投放嗎?"</string>
- <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"允許 <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 分享或錄製?"</string>
+ <string name="media_projection_permission_dialog_title" msgid="7130975432309482596">"允許 <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 分享或錄製嗎?"</string>
<string name="media_projection_permission_dialog_option_entire_screen" msgid="392086473225692983">"整個螢幕畫面"</string>
<string name="media_projection_permission_dialog_option_single_app" msgid="1591110238124910521">"單一應用程式"</string>
- <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"進行分享、錄製或投放時,<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 可以存取顯示在螢幕畫面上或在裝置上播放的所有內容。因此請謹慎處理密碼、付款資料、訊息或其他機密資訊。"</string>
- <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"進行分享、錄製或投放應用程式時,<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 可以存取在該應用程式中顯示或播放的所有內容。因此請謹慎處理密碼、付款資料、訊息或其他機密資訊。"</string>
+ <string name="media_projection_permission_dialog_warning_entire_screen" msgid="3989078820637452717">"進行分享、錄製或投放時,<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 可存取顯示在螢幕畫面上或在裝置上播放的所有內容。因此請謹慎處理密碼、付款資料、訊息或其他敏感資料。"</string>
+ <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"進行分享、錄製或投放時,<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 可存取顯示在螢幕畫面上或在裝置上播放的所有內容。因此請謹慎處理密碼、付款資料、訊息或其他敏感資料。"</string>
<string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"繼續"</string>
<string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"分享或錄製應用程式"</string>
<string name="clear_all_notifications_text" msgid="348312370303046130">"全部清除"</string>
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/bools.xml b/packages/SystemUI/res/values/bools.xml
index c67ac8d..8221d78 100644
--- a/packages/SystemUI/res/values/bools.xml
+++ b/packages/SystemUI/res/values/bools.xml
@@ -18,6 +18,13 @@
<resources>
<!-- Whether to show the user switcher in quick settings when only a single user is present. -->
<bool name="qs_show_user_switcher_for_single_user">false</bool>
+
<!-- Whether to show a custom biometric prompt size-->
<bool name="use_custom_bp_size">false</bool>
+
+ <!-- Whether to enable clipping on Quick Settings -->
+ <bool name="qs_enable_clipping">true</bool>
+
+ <!-- Whether to enable transparent background for notification scrims -->
+ <bool name="notification_scrim_transparent">false</bool>
</resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 778dd45..93926ef9 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -519,7 +519,7 @@
<dimen name="qs_tile_margin_horizontal">8dp</dimen>
<dimen name="qs_tile_margin_vertical">@dimen/qs_tile_margin_horizontal</dimen>
<dimen name="qs_tile_margin_top_bottom">4dp</dimen>
- <dimen name="qs_brightness_margin_top">8dp</dimen>
+ <dimen name="qs_brightness_margin_top">12dp</dimen>
<dimen name="qs_brightness_margin_bottom">16dp</dimen>
<dimen name="qqs_layout_margin_top">16dp</dimen>
<dimen name="qqs_layout_padding_bottom">24dp</dimen>
@@ -572,6 +572,7 @@
<dimen name="qs_header_row_min_height">48dp</dimen>
<dimen name="qs_header_non_clickable_element_height">24dp</dimen>
+ <dimen name="new_qs_header_non_clickable_element_height">20dp</dimen>
<dimen name="qs_footer_padding">20dp</dimen>
<dimen name="qs_security_footer_height">88dp</dimen>
@@ -949,6 +950,9 @@
<dimen name="biometric_dialog_width">240dp</dimen>
<dimen name="biometric_dialog_height">240dp</dimen>
+ <!-- Biometric Auth Credential values -->
+ <dimen name="biometric_auth_icon_size">48dp</dimen>
+
<!-- Starting text size in sp of batteryLevel for wireless charging animation -->
<item name="wireless_charging_anim_battery_level_text_size_start" format="float" type="dimen">
0
@@ -1569,4 +1573,9 @@
<dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen>
<dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen>
<dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen>
+
+ <!-- Default device corner radius, used for assist UI -->
+ <dimen name="config_rounded_mask_size">0px</dimen>
+ <dimen name="config_rounded_mask_size_top">0px</dimen>
+ <dimen name="config_rounded_mask_size_bottom">0px</dimen>
</resources>
diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml
index 3164ed1..e30d441 100644
--- a/packages/SystemUI/res/values/integers.xml
+++ b/packages/SystemUI/res/values/integers.xml
@@ -28,4 +28,11 @@
<!-- The time it takes for the over scroll release animation to complete, in milli seconds. -->
<integer name="lockscreen_shade_over_scroll_release_duration">0</integer>
+
+ <!-- Values for transition of QS Headers -->
+ <integer name="fade_out_complete_frame">14</integer>
+ <integer name="fade_in_start_frame">58</integer>
+ <!-- Percentage of displacement for items in QQS to guarantee matching with bottom of clock at
+ fade_out_complete_frame -->
+ <dimen name="percent_displacement_at_fade_out" format="float">0.1066</dimen>
</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 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 ac3eb7e..e76887b 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -128,11 +128,10 @@
<!-- This is hard coded to be sans-serif-condensed to match the icons -->
<style name="TextAppearance.QS.Status">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:textSize">14sp</item>
<item name="android:letterSpacing">0.01</item>
- <item name="android:lineHeight">20sp</item>
</style>
<style name="TextAppearance.QS.SecurityFooter" parent="@style/TextAppearance.QS.Status">
@@ -143,12 +142,10 @@
<style name="TextAppearance.QS.Status.Carriers" />
<style name="TextAppearance.QS.Status.Carriers.NoCarrierText">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
<style name="TextAppearance.QS.Status.Build">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
@@ -200,8 +197,9 @@
<style name="TextAppearance.AuthNonBioCredential.Title">
<item name="android:fontFamily">google-sans</item>
- <item name="android:layout_marginTop">20dp</item>
- <item name="android:textSize">36sp</item>
+ <item name="android:layout_marginTop">24dp</item>
+ <item name="android:textSize">36dp</item>
+ <item name="android:focusable">true</item>
</style>
<style name="TextAppearance.AuthNonBioCredential.Subtitle">
@@ -213,12 +211,10 @@
<style name="TextAppearance.AuthNonBioCredential.Description">
<item name="android:fontFamily">google-sans</item>
<item name="android:layout_marginTop">20dp</item>
- <item name="android:textSize">16sp</item>
+ <item name="android:textSize">18sp</item>
</style>
<style name="TextAppearance.AuthNonBioCredential.Error">
- <item name="android:paddingTop">6dp</item>
- <item name="android:paddingBottom">18dp</item>
<item name="android:paddingHorizontal">24dp</item>
<item name="android:textSize">14sp</item>
<item name="android:textColor">?android:attr/colorError</item>
@@ -227,20 +223,43 @@
<style name="TextAppearance.AuthCredential.PasswordEntry" parent="@android:style/TextAppearance.DeviceDefault">
<item name="android:gravity">center</item>
+ <item name="android:paddingTop">28dp</item>
<item name="android:singleLine">true</item>
<item name="android:textColor">?android:attr/colorForeground</item>
<item name="android:textSize">24sp</item>
+ <item name="android:background">@drawable/edit_text_filled</item>
</style>
<style name="AuthCredentialHeaderStyle">
<item name="android:paddingStart">48dp</item>
- <item name="android:paddingEnd">24dp</item>
- <item name="android:paddingTop">28dp</item>
- <item name="android:paddingBottom">20dp</item>
- <item name="android:orientation">vertical</item>
+ <item name="android:paddingEnd">48dp</item>
+ <item name="android:paddingTop">48dp</item>
+ <item name="android:paddingBottom">10dp</item>
<item name="android:layout_gravity">top</item>
</style>
+ <style name="AuthCredentialIconStyle">
+ <item name="android:layout_width">@dimen/biometric_auth_icon_size</item>
+ <item name="android:layout_height">@dimen/biometric_auth_icon_size</item>
+ </style>
+
+ <style name="AuthCredentialPatternContainerStyle">
+ <item name="android:gravity">center</item>
+ <item name="android:maxHeight">420dp</item>
+ <item name="android:maxWidth">420dp</item>
+ <item name="android:minHeight">200dp</item>
+ <item name="android:minWidth">200dp</item>
+ <item name="android:padding">20dp</item>
+ </style>
+
+ <style name="AuthCredentialPinPasswordContainerStyle">
+ <item name="android:gravity">center</item>
+ <item name="android:maxHeight">48dp</item>
+ <item name="android:maxWidth">600dp</item>
+ <item name="android:minHeight">48dp</item>
+ <item name="android:minWidth">200dp</item>
+ </style>
+
<style name="DeviceManagementDialogTitle">
<item name="android:gravity">center</item>
<item name="android:textAppearance">@style/TextAppearance.DeviceManagementDialog.Title</item>
@@ -278,7 +297,9 @@
<item name="wallpaperTextColorSecondary">@*android:color/secondary_text_material_dark</item>
<item name="wallpaperTextColorAccent">@color/material_dynamic_primary90</item>
<item name="android:colorError">@*android:color/error_color_material_dark</item>
- <item name="*android:lockPatternStyle">@style/LockPatternStyle</item>
+ <item name="*android:lockPatternStyle">@style/LockPatternViewStyle</item>
+ <item name="lockPatternStyle">@style/LockPatternContainerStyle</item>
+ <item name="lockPinPasswordStyle">@style/LockPinPasswordContainerStyle</item>
<item name="passwordStyle">@style/PasswordTheme</item>
<item name="numPadKeyStyle">@style/NumPadKey</item>
<item name="backgroundProtectedStyle">@style/BackgroundProtectedStyle</item>
@@ -304,27 +325,33 @@
<item name="android:textColor">?attr/wallpaperTextColor</item>
</style>
- <style name="LockPatternContainerStyle">
- <item name="android:maxHeight">400dp</item>
- <item name="android:maxWidth">420dp</item>
- <item name="android:minHeight">0dp</item>
- <item name="android:minWidth">0dp</item>
- <item name="android:paddingHorizontal">60dp</item>
- <item name="android:paddingBottom">40dp</item>
+ <style name="AuthCredentialStyle">
+ <item name="*android:regularColor">?android:attr/colorForeground</item>
+ <item name="*android:successColor">?android:attr/colorForeground</item>
+ <item name="*android:errorColor">?android:attr/colorError</item>
+ <item name="*android:dotColor">?android:attr/textColorSecondary</item>
+ <item name="headerStyle">@style/AuthCredentialHeaderStyle</item>
+ <item name="headerIconStyle">@style/AuthCredentialIconStyle</item>
+ <item name="titleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Title</item>
+ <item name="subTitleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Subtitle</item>
+ <item name="descriptionTextAppearance">@style/TextAppearance.AuthNonBioCredential.Description</item>
+ <item name="passwordTextAppearance">@style/TextAppearance.AuthCredential.PasswordEntry</item>
+ <item name="errorTextAppearance">@style/TextAppearance.AuthNonBioCredential.Error</item>
</style>
- <style name="LockPatternStyle">
+ <style name="LockPatternViewStyle" >
<item name="*android:regularColor">?android:attr/colorAccent</item>
<item name="*android:successColor">?android:attr/textColorPrimary</item>
<item name="*android:errorColor">?android:attr/colorError</item>
<item name="*android:dotColor">?android:attr/textColorSecondary</item>
</style>
- <style name="LockPatternStyleBiometricPrompt">
- <item name="*android:regularColor">?android:attr/colorForeground</item>
- <item name="*android:successColor">?android:attr/colorForeground</item>
- <item name="*android:errorColor">?android:attr/colorError</item>
- <item name="*android:dotColor">?android:attr/textColorSecondary</item>
+ <style name="LockPatternContainerStyle" parent="@style/AuthCredentialStyle">
+ <item name="containerStyle">@style/AuthCredentialPatternContainerStyle</item>
+ </style>
+
+ <style name="LockPinPasswordContainerStyle" parent="@style/AuthCredentialStyle">
+ <item name="containerStyle">@style/AuthCredentialPinPasswordContainerStyle</item>
</style>
<style name="Theme.SystemUI.QuickSettings" parent="@*android:style/Theme.DeviceDefault">
diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
index f3866c0..de855e2 100644
--- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml
+++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
@@ -27,67 +27,60 @@
<KeyPosition
app:keyPositionType="deltaRelative"
app:percentX="0"
- app:percentY="0"
- app:framePosition="49"
+ app:percentY="@dimen/percent_displacement_at_fade_out"
+ app:framePosition="@integer/fade_out_complete_frame"
app:sizePercent="0"
app:curveFit="linear"
app:motionTarget="@id/date" />
<KeyPosition
app:keyPositionType="deltaRelative"
app:percentX="1"
- app:percentY="0.51"
+ app:percentY="0.5"
app:sizePercent="1"
- app:framePosition="51"
+ app:framePosition="50"
app:curveFit="linear"
app:motionTarget="@id/date" />
<KeyAttribute
app:motionTarget="@id/date"
- app:framePosition="30"
+ app:framePosition="14"
android:alpha="0"
/>
<KeyAttribute
app:motionTarget="@id/date"
- app:framePosition="70"
+ app:framePosition="@integer/fade_in_start_frame"
android:alpha="0"
/>
<KeyPosition
- app:keyPositionType="pathRelative"
+ app:keyPositionType="deltaRelative"
app:percentX="0"
- app:percentY="0"
- app:framePosition="0"
- app:curveFit="linear"
- app:motionTarget="@id/statusIcons" />
- <KeyPosition
- app:keyPositionType="pathRelative"
- app:percentX="0"
- app:percentY="0"
- app:framePosition="50"
+ app:percentY="@dimen/percent_displacement_at_fade_out"
+ app:framePosition="@integer/fade_out_complete_frame"
app:sizePercent="0"
app:curveFit="linear"
app:motionTarget="@id/statusIcons" />
<KeyPosition
app:keyPositionType="deltaRelative"
app:percentX="1"
- app:percentY="0.51"
- app:framePosition="51"
+ app:percentY="0.5"
+ app:framePosition="50"
app:sizePercent="1"
app:curveFit="linear"
app:motionTarget="@id/statusIcons" />
<KeyAttribute
app:motionTarget="@id/statusIcons"
- app:framePosition="30"
+ app:framePosition="@integer/fade_out_complete_frame"
android:alpha="0"
/>
<KeyAttribute
app:motionTarget="@id/statusIcons"
- app:framePosition="70"
+ app:framePosition="@integer/fade_in_start_frame"
android:alpha="0"
/>
<KeyPosition
app:keyPositionType="deltaRelative"
app:percentX="0"
- app:percentY="0"
- app:framePosition="50"
+ app:percentY="@dimen/percent_displacement_at_fade_out"
+ app:framePosition="@integer/fade_out_complete_frame"
app:percentWidth="1"
app:percentHeight="1"
app:curveFit="linear"
@@ -95,27 +88,27 @@
<KeyPosition
app:keyPositionType="deltaRelative"
app:percentX="1"
- app:percentY="0.51"
- app:framePosition="51"
+ app:percentY="0.5"
+ app:framePosition="50"
app:percentWidth="1"
app:percentHeight="1"
app:curveFit="linear"
app:motionTarget="@id/batteryRemainingIcon" />
<KeyAttribute
app:motionTarget="@id/batteryRemainingIcon"
- app:framePosition="30"
+ app:framePosition="@integer/fade_out_complete_frame"
android:alpha="0"
/>
<KeyAttribute
app:motionTarget="@id/batteryRemainingIcon"
- app:framePosition="70"
+ app:framePosition="@integer/fade_in_start_frame"
android:alpha="0"
/>
<KeyPosition
app:motionTarget="@id/carrier_group"
app:percentX="1"
- app:percentY="0.51"
- app:framePosition="51"
+ app:percentY="0.5"
+ app:framePosition="50"
app:percentWidth="1"
app:percentHeight="1"
app:curveFit="linear"
@@ -126,7 +119,7 @@
android:alpha="0" />
<KeyAttribute
app:motionTarget="@id/carrier_group"
- app:framePosition="70"
+ app:framePosition="@integer/fade_in_start_frame"
android:alpha="0" />
</KeyFrameSet>
</Transition>
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index a82684d03..88b4f43 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -43,7 +43,8 @@
android:id="@+id/date">
<Layout
android:layout_width="0dp"
- android:layout_height="@dimen/qs_header_non_clickable_element_height"
+ android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+ android:layout_marginStart="8dp"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toEndOf="@id/clock"
app:layout_constraintEnd_toStartOf="@id/barrier"
@@ -57,8 +58,8 @@
android:id="@+id/statusIcons">
<Layout
android:layout_width="0dp"
- android:layout_height="@dimen/qs_header_non_clickable_element_height"
- app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
+ android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+ app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constraintStart_toEndOf="@id/date"
app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
app:layout_constraintTop_toTopOf="parent"
@@ -71,9 +72,9 @@
android:id="@+id/batteryRemainingIcon">
<Layout
android:layout_width="wrap_content"
- android:layout_height="@dimen/qs_header_non_clickable_element_height"
+ android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constrainedWidth="true"
- app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
+ app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constraintStart_toEndOf="@id/statusIcons"
app:layout_constraintEnd_toEndOf="@id/end_guide"
app:layout_constraintTop_toTopOf="parent"
diff --git a/packages/SystemUI/res/xml/qs_header_new.xml b/packages/SystemUI/res/xml/qs_header_new.xml
index f39e6bd..d8a4e77 100644
--- a/packages/SystemUI/res/xml/qs_header_new.xml
+++ b/packages/SystemUI/res/xml/qs_header_new.xml
@@ -40,13 +40,13 @@
android:layout_height="@dimen/large_screen_shade_header_min_height"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/privacy_container"
- app:layout_constraintBottom_toTopOf="@id/date"
+ app:layout_constraintBottom_toBottomOf="@id/carrier_group"
app:layout_constraintEnd_toStartOf="@id/carrier_group"
app:layout_constraintHorizontal_bias="0"
/>
<Transform
- android:scaleX="2.4"
- android:scaleY="2.4"
+ android:scaleX="2.57"
+ android:scaleY="2.57"
/>
</Constraint>
@@ -54,11 +54,11 @@
android:id="@+id/date">
<Layout
android:layout_width="0dp"
- android:layout_height="@dimen/qs_header_non_clickable_element_height"
+ android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/space"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintTop_toBottomOf="@id/clock"
+ app:layout_constraintTop_toBottomOf="@id/carrier_group"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="spread_inside"
/>
@@ -87,7 +87,7 @@
android:id="@+id/statusIcons">
<Layout
android:layout_width="0dp"
- android:layout_height="@dimen/qs_header_non_clickable_element_height"
+ android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toEndOf="@id/space"
app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
@@ -101,8 +101,8 @@
android:id="@+id/batteryRemainingIcon">
<Layout
android:layout_width="wrap_content"
- android:layout_height="@dimen/qs_header_non_clickable_element_height"
- app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
+ android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+ app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constraintStart_toEndOf="@id/statusIcons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/date"
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
index 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 18bd6b4..91fd6a6 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -59,7 +59,9 @@
resource_dirs: [
"res",
],
- java_version: "1.8",
+ optimize: {
+ proguard_flags_files: ["proguard.flags"],
+ },
min_sdk_version: "current",
plugins: ["dagger2-compiler"],
}
diff --git a/packages/SystemUI/shared/proguard.flags b/packages/SystemUI/shared/proguard.flags
new file mode 100644
index 0000000..5eda045
--- /dev/null
+++ b/packages/SystemUI/shared/proguard.flags
@@ -0,0 +1,4 @@
+# Retain signatures of TypeToken and its subclasses for gson usage in ClockRegistry
+-keepattributes Signature
+-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
+-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt
index d7a0b47..3efdc5a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt
@@ -17,6 +17,7 @@
import android.graphics.Point
import android.view.Surface
+import android.view.Surface.Rotation
import android.view.View
import android.view.WindowManager
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
@@ -58,14 +59,14 @@
* Updates display properties in order to calculate the initial position for the views
* Must be called before [registerViewForAnimation]
*/
- fun updateDisplayProperties() {
+ @JvmOverloads
+ fun updateDisplayProperties(@Rotation rotation: Int = windowManager.defaultDisplay.rotation) {
windowManager.defaultDisplay.getSize(screenSize)
// Simple implementation to get current fold orientation,
// this might not be correct on all devices
// TODO: use JetPack WindowManager library to get the fold orientation
- isVerticalFold = windowManager.defaultDisplay.rotation == Surface.ROTATION_0 ||
- windowManager.defaultDisplay.rotation == Surface.ROTATION_180
+ isVerticalFold = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
}
/**
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 f03fee4..48821e8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -18,10 +18,9 @@
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Handler
-import android.os.UserHandle
import android.provider.Settings
import android.util.Log
-import com.android.systemui.dagger.qualifiers.Main
+import com.android.internal.annotations.Keep
import com.android.systemui.plugins.ClockController
import com.android.systemui.plugins.ClockId
import com.android.systemui.plugins.ClockMetadata
@@ -30,7 +29,6 @@
import com.android.systemui.plugins.PluginListener
import com.android.systemui.shared.plugins.PluginManager
import com.google.gson.Gson
-import javax.inject.Inject
private val TAG = ClockRegistry::class.simpleName
private const val DEBUG = true
@@ -40,22 +38,15 @@
val context: Context,
val pluginManager: PluginManager,
val handler: Handler,
- defaultClockProvider: ClockProvider
+ val isEnabled: Boolean,
+ userHandle: Int,
+ defaultClockProvider: ClockProvider,
) {
- @Inject constructor(
- context: Context,
- pluginManager: PluginManager,
- @Main handler: Handler,
- defaultClockProvider: DefaultClockProvider
- ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { }
-
// Usually this would be a typealias, but a SAM provides better java interop
fun interface ClockChangeListener {
fun onClockChanged()
}
- var isEnabled: Boolean = false
-
private val gson = Gson()
private val availableClocks = mutableMapOf<ClockId, ClockInfo>()
private val clockChangeListeners = mutableListOf<ClockChangeListener>()
@@ -105,14 +96,19 @@
)
}
- pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java,
- true /* allowMultiple */)
- context.contentResolver.registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
- false,
- settingObserver,
- UserHandle.USER_ALL
- )
+ if (isEnabled) {
+ pluginManager.addPluginListener(
+ pluginListener,
+ ClockProviderPlugin::class.java,
+ /*allowMultiple=*/ true
+ )
+ context.contentResolver.registerContentObserver(
+ Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
+ /*notifyForDescendants=*/ false,
+ settingObserver,
+ userHandle
+ )
+ }
}
private fun connectClocks(provider: ClockProvider) {
@@ -201,6 +197,7 @@
val provider: ClockProvider
)
+ @Keep
private data class ClockSetting(
val clockId: ClockId,
val _applied_timestamp: Long?
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index 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/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
index 2111df5..647dd47 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
@@ -27,6 +27,8 @@
import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
@@ -233,17 +235,14 @@
@ViewDebug.ExportedProperty(category="recents")
public boolean isLocked;
+ public Point positionInParent;
+
+ public Rect appBounds;
+
// Last snapshot data, only used for recent tasks
public ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData =
new ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData();
- /**
- * Indicates that this task for the desktop tile in recents.
- *
- * Used when desktop mode feature is enabled.
- */
- public boolean desktopTile;
-
public Task() {
// Do nothing
}
@@ -274,7 +273,8 @@
this(other.key, other.colorPrimary, other.colorBackground, other.isDockable,
other.isLocked, other.taskDescription, other.topActivity);
lastSnapshotData.set(other.lastSnapshotData);
- desktopTile = other.desktopTile;
+ positionInParent = other.positionInParent;
+ appBounds = other.appBounds;
}
/**
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/regionsampling/RegionSamplingInstance.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
index dd2e55d..cd4b999 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
@@ -15,6 +15,7 @@
*/
package com.android.systemui.shared.regionsampling
+import android.graphics.Color
import android.graphics.Rect
import android.view.View
import androidx.annotation.VisibleForTesting
@@ -33,18 +34,19 @@
regionSamplingEnabled: Boolean,
updateFun: UpdateColorCallback
) {
- private var isDark = RegionDarkness.DEFAULT
+ private var regionDarkness = RegionDarkness.DEFAULT
private var samplingBounds = Rect()
private val tmpScreenLocation = IntArray(2)
@VisibleForTesting var regionSampler: RegionSamplingHelper? = null
-
+ private var lightForegroundColor = Color.WHITE
+ private var darkForegroundColor = Color.BLACK
/**
* Interface for method to be passed into RegionSamplingHelper
*/
@FunctionalInterface
interface UpdateColorCallback {
/**
- * Method to update the text colors after clock darkness changed.
+ * Method to update the foreground colors after clock darkness changed.
*/
fun updateColors()
}
@@ -59,6 +61,30 @@
return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor)
}
+ /**
+ * Sets the colors to be used for Dark and Light Foreground.
+ *
+ * @param lightColor The color used for Light Foreground.
+ * @param darkColor The color used for Dark Foreground.
+ */
+ fun setForegroundColors(lightColor: Int, darkColor: Int) {
+ lightForegroundColor = lightColor
+ darkForegroundColor = darkColor
+ }
+
+ /**
+ * Determines which foreground color to use based on region darkness.
+ *
+ * @return the determined foreground color
+ */
+ fun currentForegroundColor(): Int{
+ return if (regionDarkness.isDark) {
+ lightForegroundColor
+ } else {
+ darkForegroundColor
+ }
+ }
+
private fun convertToClockDarkness(isRegionDark: Boolean): RegionDarkness {
return if (isRegionDark) {
RegionDarkness.DARK
@@ -68,7 +94,7 @@
}
fun currentRegionDarkness(): RegionDarkness {
- return isDark
+ return regionDarkness
}
/**
@@ -97,7 +123,7 @@
regionSampler = createRegionSamplingHelper(sampledView,
object : SamplingCallback {
override fun onRegionDarknessChanged(isRegionDark: Boolean) {
- isDark = convertToClockDarkness(isRegionDark)
+ regionDarkness = convertToClockDarkness(isRegionDark)
updateFun.updateColors()
}
/**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
index 22bffda..6087655 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
@@ -132,8 +132,11 @@
mMainThreadHandler.postAtFrontOfQueue(() -> {
// If the screen rotation changes while locked, potentially update lock to flow with
// new screen rotation and hide any showing suggestions.
- if (isRotationLocked()) {
- if (shouldOverrideUserLockPrefs(rotation)) {
+ boolean rotationLocked = isRotationLocked();
+ // The isVisible check makes the rotation button disappear when we are not locked
+ // (e.g. for tabletop auto-rotate).
+ if (rotationLocked || mRotationButton.isVisible()) {
+ if (shouldOverrideUserLockPrefs(rotation) && rotationLocked) {
setRotationLockedAtAngle(rotation);
}
setRotateSuggestionButtonState(false /* visible */, true /* forced */);
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 7c3b5fc..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 int taskId;
- public final SurfaceControl leash;
- public final boolean isTranslucent;
- public final Rect clipRect;
- public final int prefixOrderIndex;
- public final Point position;
- public final Rect localBounds;
- public final Rect sourceContainerBounds;
- public final Rect screenSpaceBounds;
- public final Rect startScreenSpaceBounds;
- public final boolean isNotInRecents;
- public final Rect contentInsets;
- public ActivityManager.RunningTaskInfo taskInfo;
- public final boolean allowEnterPip;
- public final int rotationChange;
- public final int windowType;
- public final WindowConfiguration windowConfiguration;
-
- private final SurfaceControl mStartLeash;
-
- // Fields used only to unwrap into RemoteAnimationTarget
- private final Rect startBounds;
-
- public final boolean willShowImeOnTarget;
-
- public RemoteAnimationTargetCompat(RemoteAnimationTarget app) {
- taskId = app.taskId;
- mode = app.mode;
- leash = app.leash;
- isTranslucent = app.isTranslucent;
- clipRect = app.clipRect;
- position = app.position;
- localBounds = app.localBounds;
- sourceContainerBounds = app.sourceContainerBounds;
- screenSpaceBounds = app.screenSpaceBounds;
- startScreenSpaceBounds = screenSpaceBounds;
- prefixOrderIndex = app.prefixOrderIndex;
- isNotInRecents = app.isNotInRecents;
- contentInsets = app.contentInsets;
- activityType = app.windowConfiguration.getActivityType();
- taskInfo = app.taskInfo;
- allowEnterPip = app.allowEnterPip;
- rotationChange = 0;
-
- mStartLeash = app.startLeash;
- windowType = app.windowType;
- windowConfiguration = app.windowConfiguration;
- startBounds = app.startBounds;
- willShowImeOnTarget = app.willShowImeOnTarget;
- }
-
private static int newModeToLegacyMode(int newMode) {
switch (newMode) {
case WindowManager.TRANSIT_OPEN:
@@ -120,20 +63,10 @@
case WindowManager.TRANSIT_TO_BACK:
return MODE_CLOSING;
default:
- return 2; // MODE_CHANGING
+ return MODE_CHANGING;
}
}
- public RemoteAnimationTarget unwrap() {
- final RemoteAnimationTarget target = new RemoteAnimationTarget(
- taskId, mode, leash, isTranslucent, clipRect, contentInsets,
- prefixOrderIndex, position, localBounds, screenSpaceBounds, windowConfiguration,
- isNotInRecents, mStartLeash, startBounds, taskInfo, allowEnterPip, windowType
- );
- target.setWillShowImeOnTarget(willShowImeOnTarget);
- return target;
- }
-
/**
* Almost a copy of Transitions#setupStartState.
* TODO: remove when there is proper cross-process transaction sync.
@@ -205,54 +138,61 @@
return leashSurface;
}
- public RemoteAnimationTargetCompat(TransitionInfo.Change change, int order,
- TransitionInfo info, SurfaceControl.Transaction t) {
- mode = newModeToLegacyMode(change.getMode());
+ /**
+ * Creates a new RemoteAnimationTarget from the provided change info
+ */
+ public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
+ TransitionInfo info, SurfaceControl.Transaction t,
+ @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
+ int taskId;
+ boolean isNotInRecents;
+ ActivityManager.RunningTaskInfo taskInfo;
+ WindowConfiguration windowConfiguration;
+
taskInfo = change.getTaskInfo();
if (taskInfo != null) {
taskId = taskInfo.taskId;
isNotInRecents = !taskInfo.isRunning;
- activityType = taskInfo.getActivityType();
windowConfiguration = taskInfo.configuration.windowConfiguration;
} else {
taskId = INVALID_TASK_ID;
isNotInRecents = true;
- activityType = ACTIVITY_TYPE_UNDEFINED;
windowConfiguration = new WindowConfiguration();
}
- // TODO: once we can properly sync transactions across process, then get rid of this leash.
- leash = createLeash(info, change, order, t);
-
- isTranslucent = (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0;
- clipRect = null;
- position = null;
- localBounds = new Rect(change.getEndAbsBounds());
+ Rect localBounds = new Rect(change.getEndAbsBounds());
localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
- sourceContainerBounds = null;
- screenSpaceBounds = new Rect(change.getEndAbsBounds());
- startScreenSpaceBounds = new Rect(change.getStartAbsBounds());
- prefixOrderIndex = order;
- // TODO(shell-transitions): I guess we need to send content insets? evaluate how its used.
- contentInsets = new Rect(0, 0, 0, 0);
- allowEnterPip = change.getAllowEnterPip();
- mStartLeash = null;
- rotationChange = change.getEndRotation() - change.getStartRotation();
- windowType = (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0
- ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE;
-
- startBounds = change.getStartAbsBounds();
- willShowImeOnTarget = (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0;
- }
-
- public static RemoteAnimationTargetCompat[] wrap(RemoteAnimationTarget[] apps) {
- final int length = apps != null ? apps.length : 0;
- final RemoteAnimationTargetCompat[] appsCompat = new RemoteAnimationTargetCompat[length];
- for (int i = 0; i < length; i++) {
- appsCompat[i] = new RemoteAnimationTargetCompat(apps[i]);
+ RemoteAnimationTarget target = new RemoteAnimationTarget(
+ taskId,
+ newModeToLegacyMode(change.getMode()),
+ // TODO: once we can properly sync transactions across process,
+ // then get rid of this leash.
+ createLeash(info, change, order, t),
+ (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0,
+ null,
+ // TODO(shell-transitions): we need to send content insets? evaluate how its used.
+ new Rect(0, 0, 0, 0),
+ order,
+ null,
+ localBounds,
+ new Rect(change.getEndAbsBounds()),
+ windowConfiguration,
+ isNotInRecents,
+ null,
+ new Rect(change.getStartAbsBounds()),
+ taskInfo,
+ change.getAllowEnterPip(),
+ (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0
+ ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE
+ );
+ target.setWillShowImeOnTarget(
+ (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0);
+ target.setRotationChange(change.getEndRotation() - change.getStartRotation());
+ if (leashMap != null) {
+ leashMap.put(change.getLeash(), target.leash);
}
- return appsCompat;
+ return target;
}
/**
@@ -261,35 +201,20 @@
* @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be
* populated by this function. If null, it is ignored.
*/
- public static RemoteAnimationTargetCompat[] wrapApps(TransitionInfo info,
+ public static RemoteAnimationTarget[] wrapApps(TransitionInfo info,
SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
- final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>();
- final SparseArray<TransitionInfo.Change> childTaskTargets = new SparseArray<>();
- for (int i = 0; i < info.getChanges().size(); i++) {
- final TransitionInfo.Change change = info.getChanges().get(i);
- if (change.getTaskInfo() == null) continue;
-
- final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ SparseBooleanArray childTaskTargets = new SparseBooleanArray();
+ return wrap(info, t, leashMap, (change, taskInfo) -> {
// Children always come before parent since changes are in top-to-bottom z-order.
- if (taskInfo != null) {
- if (childTaskTargets.contains(taskInfo.taskId)) {
- // has children, so not a leaf. Skip.
- continue;
- }
- if (taskInfo.hasParentTask()) {
- childTaskTargets.put(taskInfo.parentTaskId, change);
- }
+ if ((taskInfo == null) || childTaskTargets.get(taskInfo.taskId)) {
+ // has children, so not a leaf. Skip.
+ return false;
}
-
- final RemoteAnimationTargetCompat targetCompat =
- new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t);
- if (leashMap != null) {
- leashMap.put(change.getLeash(), targetCompat.leash);
+ if (taskInfo.hasParentTask()) {
+ childTaskTargets.put(taskInfo.parentTaskId, true);
}
- out.add(targetCompat);
- }
-
- return out.toArray(new RemoteAnimationTargetCompat[out.size()]);
+ return true;
+ });
}
/**
@@ -300,38 +225,22 @@
* @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/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java
deleted file mode 100644
index 30c062b..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java
+++ /dev/null
@@ -1,380 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.shared.system;
-
-import android.graphics.HardwareRenderer;
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Handler.Callback;
-import android.os.Message;
-import android.os.Trace;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-import android.view.View;
-import android.view.ViewRootImpl;
-
-import java.util.function.Consumer;
-
-/**
- * Helper class to apply surface transactions in sync with RenderThread.
- *
- * NOTE: This is a modification of {@link android.view.SyncRtSurfaceTransactionApplier}, we can't
- * currently reference that class from the shared lib as it is hidden.
- */
-public class SyncRtSurfaceTransactionApplierCompat {
-
- public static final int FLAG_ALL = 0xffffffff;
- public static final int FLAG_ALPHA = 1;
- public static final int FLAG_MATRIX = 1 << 1;
- public static final int FLAG_WINDOW_CROP = 1 << 2;
- public static final int FLAG_LAYER = 1 << 3;
- public static final int FLAG_CORNER_RADIUS = 1 << 4;
- public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5;
- public static final int FLAG_VISIBILITY = 1 << 6;
- public static final int FLAG_RELATIVE_LAYER = 1 << 7;
- public static final int FLAG_SHADOW_RADIUS = 1 << 8;
-
- private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0;
-
- private final SurfaceControl mBarrierSurfaceControl;
- private final ViewRootImpl mTargetViewRootImpl;
- private final Handler mApplyHandler;
-
- private int mSequenceNumber = 0;
- private int mPendingSequenceNumber = 0;
- private Runnable mAfterApplyCallback;
-
- /**
- * @param targetView The view in the surface that acts as synchronization anchor.
- */
- public SyncRtSurfaceTransactionApplierCompat(View targetView) {
- mTargetViewRootImpl = targetView != null ? targetView.getViewRootImpl() : null;
- mBarrierSurfaceControl = mTargetViewRootImpl != null
- ? mTargetViewRootImpl.getSurfaceControl() : null;
-
- mApplyHandler = new Handler(new Callback() {
- @Override
- public boolean handleMessage(Message msg) {
- if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) {
- onApplyMessage(msg.arg1);
- return true;
- }
- return false;
- }
- });
- }
-
- private void onApplyMessage(int seqNo) {
- mSequenceNumber = seqNo;
- if (mSequenceNumber == mPendingSequenceNumber && mAfterApplyCallback != null) {
- Runnable r = mAfterApplyCallback;
- mAfterApplyCallback = null;
- r.run();
- }
- }
-
- /**
- * Schedules applying surface parameters on the next frame.
- *
- * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into
- * this method to avoid synchronization issues.
- */
- public void scheduleApply(final SyncRtSurfaceTransactionApplierCompat.SurfaceParams... params) {
- if (mTargetViewRootImpl == null || mTargetViewRootImpl.getView() == null) {
- return;
- }
-
- mPendingSequenceNumber++;
- final int toApplySeqNo = mPendingSequenceNumber;
- mTargetViewRootImpl.registerRtFrameCallback(new HardwareRenderer.FrameDrawingCallback() {
- @Override
- public void onFrameDraw(long frame) {
- if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) {
- Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
- .sendToTarget();
- return;
- }
- Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Sync transaction frameNumber=" + frame);
- Transaction t = new Transaction();
- for (int i = params.length - 1; i >= 0; i--) {
- SyncRtSurfaceTransactionApplierCompat.SurfaceParams surfaceParams =
- params[i];
- surfaceParams.applyTo(t);
- }
- if (mTargetViewRootImpl != null) {
- mTargetViewRootImpl.mergeWithNextTransaction(t, frame);
- } else {
- t.apply();
- }
- Trace.traceEnd(Trace.TRACE_TAG_VIEW);
- Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
- .sendToTarget();
- }
- });
-
- // Make sure a frame gets scheduled.
- mTargetViewRootImpl.getView().invalidate();
- }
-
- /**
- * Calls the runnable when any pending apply calls have completed
- */
- public void addAfterApplyCallback(final Runnable afterApplyCallback) {
- if (mSequenceNumber == mPendingSequenceNumber) {
- afterApplyCallback.run();
- } else {
- if (mAfterApplyCallback == null) {
- mAfterApplyCallback = afterApplyCallback;
- } else {
- final Runnable oldCallback = mAfterApplyCallback;
- mAfterApplyCallback = new Runnable() {
- @Override
- public void run() {
- afterApplyCallback.run();
- oldCallback.run();
- }
- };
- }
- }
- }
-
- public static void applyParams(TransactionCompat t,
- SyncRtSurfaceTransactionApplierCompat.SurfaceParams params) {
- params.applyTo(t.mTransaction);
- }
-
- /**
- * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is
- * attached if necessary.
- */
- public static void create(final View targetView,
- final Consumer<SyncRtSurfaceTransactionApplierCompat> callback) {
- if (targetView == null) {
- // No target view, no applier
- callback.accept(null);
- } else if (targetView.getViewRootImpl() != null) {
- // Already attached, we're good to go
- callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView));
- } else {
- // Haven't been attached before we can get the view root
- targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View v) {
- targetView.removeOnAttachStateChangeListener(this);
- callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView));
- }
-
- @Override
- public void onViewDetachedFromWindow(View v) {
- // Do nothing
- }
- });
- }
- }
-
- public static class SurfaceParams {
- public static class Builder {
- final SurfaceControl surface;
- int flags;
- float alpha;
- float cornerRadius;
- int backgroundBlurRadius;
- Matrix matrix;
- Rect windowCrop;
- int layer;
- SurfaceControl relativeTo;
- int relativeLayer;
- boolean visible;
- float shadowRadius;
-
- /**
- * @param surface The surface to modify.
- */
- public Builder(SurfaceControl surface) {
- this.surface = surface;
- }
-
- /**
- * @param alpha The alpha value to apply to the surface.
- * @return this Builder
- */
- public Builder withAlpha(float alpha) {
- this.alpha = alpha;
- flags |= FLAG_ALPHA;
- return this;
- }
-
- /**
- * @param matrix The matrix to apply to the surface.
- * @return this Builder
- */
- public Builder withMatrix(Matrix matrix) {
- this.matrix = new Matrix(matrix);
- flags |= FLAG_MATRIX;
- return this;
- }
-
- /**
- * @param windowCrop The window crop to apply to the surface.
- * @return this Builder
- */
- public Builder withWindowCrop(Rect windowCrop) {
- this.windowCrop = new Rect(windowCrop);
- flags |= FLAG_WINDOW_CROP;
- return this;
- }
-
- /**
- * @param layer The layer to assign the surface.
- * @return this Builder
- */
- public Builder withLayer(int layer) {
- this.layer = layer;
- flags |= FLAG_LAYER;
- return this;
- }
-
- /**
- * @param relativeTo The surface that's set relative layer to.
- * @param relativeLayer The relative layer.
- * @return this Builder
- */
- public Builder withRelativeLayerTo(SurfaceControl relativeTo, int relativeLayer) {
- this.relativeTo = relativeTo;
- this.relativeLayer = relativeLayer;
- flags |= FLAG_RELATIVE_LAYER;
- return this;
- }
-
- /**
- * @param radius the Radius for rounded corners to apply to the surface.
- * @return this Builder
- */
- public Builder withCornerRadius(float radius) {
- this.cornerRadius = radius;
- flags |= FLAG_CORNER_RADIUS;
- return this;
- }
-
- /**
- * @param radius the Radius for the shadows to apply to the surface.
- * @return this Builder
- */
- public Builder withShadowRadius(float radius) {
- this.shadowRadius = radius;
- flags |= FLAG_SHADOW_RADIUS;
- return this;
- }
-
- /**
- * @param radius the Radius for blur to apply to the background surfaces.
- * @return this Builder
- */
- public Builder withBackgroundBlur(int radius) {
- this.backgroundBlurRadius = radius;
- flags |= FLAG_BACKGROUND_BLUR_RADIUS;
- return this;
- }
-
- /**
- * @param visible The visibility to apply to the surface.
- * @return this Builder
- */
- public Builder withVisibility(boolean visible) {
- this.visible = visible;
- flags |= FLAG_VISIBILITY;
- return this;
- }
-
- /**
- * @return a new SurfaceParams instance
- */
- public SurfaceParams build() {
- return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer,
- relativeTo, relativeLayer, cornerRadius, backgroundBlurRadius, visible,
- shadowRadius);
- }
- }
-
- private SurfaceParams(SurfaceControl surface, int flags, float alpha, Matrix matrix,
- Rect windowCrop, int layer, SurfaceControl relativeTo, int relativeLayer,
- float cornerRadius, int backgroundBlurRadius, boolean visible, float shadowRadius) {
- this.flags = flags;
- this.surface = surface;
- this.alpha = alpha;
- this.matrix = matrix;
- this.windowCrop = windowCrop;
- this.layer = layer;
- this.relativeTo = relativeTo;
- this.relativeLayer = relativeLayer;
- this.cornerRadius = cornerRadius;
- this.backgroundBlurRadius = backgroundBlurRadius;
- this.visible = visible;
- this.shadowRadius = shadowRadius;
- }
-
- private final int flags;
- private final float[] mTmpValues = new float[9];
-
- public final SurfaceControl surface;
- public final float alpha;
- public final float cornerRadius;
- public final int backgroundBlurRadius;
- public final Matrix matrix;
- public final Rect windowCrop;
- public final int layer;
- public final SurfaceControl relativeTo;
- public final int relativeLayer;
- public final boolean visible;
- public final float shadowRadius;
-
- public void applyTo(SurfaceControl.Transaction t) {
- if ((flags & FLAG_MATRIX) != 0) {
- t.setMatrix(surface, matrix, mTmpValues);
- }
- if ((flags & FLAG_WINDOW_CROP) != 0) {
- t.setWindowCrop(surface, windowCrop);
- }
- if ((flags & FLAG_ALPHA) != 0) {
- t.setAlpha(surface, alpha);
- }
- if ((flags & FLAG_LAYER) != 0) {
- t.setLayer(surface, layer);
- }
- if ((flags & FLAG_CORNER_RADIUS) != 0) {
- t.setCornerRadius(surface, cornerRadius);
- }
- if ((flags & FLAG_BACKGROUND_BLUR_RADIUS) != 0) {
- t.setBackgroundBlurRadius(surface, backgroundBlurRadius);
- }
- if ((flags & FLAG_VISIBILITY) != 0) {
- if (visible) {
- t.show(surface);
- } else {
- t.hide(surface);
- }
- }
- if ((flags & FLAG_RELATIVE_LAYER) != 0) {
- t.setRelativeLayer(surface, relativeTo, relativeLayer);
- }
- if ((flags & FLAG_SHADOW_RADIUS) != 0) {
- t.setShadowRadius(surface, shadowRadius);
- }
- }
- }
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java
deleted file mode 100644
index 43a882a5..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.shared.system;
-
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-
-public class TransactionCompat {
-
- final Transaction mTransaction;
-
- final float[] mTmpValues = new float[9];
-
- public TransactionCompat() {
- mTransaction = new Transaction();
- }
-
- public void apply() {
- mTransaction.apply();
- }
-
- public TransactionCompat show(SurfaceControl surfaceControl) {
- mTransaction.show(surfaceControl);
- return this;
- }
-
- public TransactionCompat hide(SurfaceControl surfaceControl) {
- mTransaction.hide(surfaceControl);
- return this;
- }
-
- public TransactionCompat setPosition(SurfaceControl surfaceControl, float x, float y) {
- mTransaction.setPosition(surfaceControl, x, y);
- return this;
- }
-
- public TransactionCompat setSize(SurfaceControl surfaceControl, int w, int h) {
- mTransaction.setBufferSize(surfaceControl, w, h);
- return this;
- }
-
- public TransactionCompat setLayer(SurfaceControl surfaceControl, int z) {
- mTransaction.setLayer(surfaceControl, z);
- return this;
- }
-
- public TransactionCompat setAlpha(SurfaceControl surfaceControl, float alpha) {
- mTransaction.setAlpha(surfaceControl, alpha);
- return this;
- }
-
- public TransactionCompat setOpaque(SurfaceControl surfaceControl, boolean opaque) {
- mTransaction.setOpaque(surfaceControl, opaque);
- return this;
- }
-
- public TransactionCompat setMatrix(SurfaceControl surfaceControl, float dsdx, float dtdx,
- float dtdy, float dsdy) {
- mTransaction.setMatrix(surfaceControl, dsdx, dtdx, dtdy, dsdy);
- return this;
- }
-
- public TransactionCompat setMatrix(SurfaceControl surfaceControl, Matrix matrix) {
- mTransaction.setMatrix(surfaceControl, matrix, mTmpValues);
- return this;
- }
-
- public TransactionCompat setWindowCrop(SurfaceControl surfaceControl, Rect crop) {
- mTransaction.setWindowCrop(surfaceControl, crop);
- return this;
- }
-
- public TransactionCompat setCornerRadius(SurfaceControl surfaceControl, float radius) {
- mTransaction.setCornerRadius(surfaceControl, radius);
- return this;
- }
-
- public TransactionCompat setBackgroundBlurRadius(SurfaceControl surfaceControl, int radius) {
- mTransaction.setBackgroundBlurRadius(surfaceControl, radius);
- return this;
- }
-
- public TransactionCompat setColor(SurfaceControl surfaceControl, float[] color) {
- mTransaction.setColor(surfaceControl, color);
- return this;
- }
-
- public static void setRelativeLayer(Transaction t, SurfaceControl surfaceControl,
- SurfaceControl relativeTo, int z) {
- t.setRelativeLayer(surfaceControl, relativeTo, z);
- }
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt
index ec938b2..aca9907 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt
@@ -15,12 +15,11 @@
package com.android.systemui.unfold.util
import android.content.Context
-import android.os.RemoteException
-import android.view.IRotationWatcher
-import android.view.IWindowManager
import android.view.Surface
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.RotationChangeProvider
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
/**
* [UnfoldTransitionProgressProvider] that emits transition progress only when the display has
@@ -29,27 +28,21 @@
*/
class NaturalRotationUnfoldProgressProvider(
private val context: Context,
- private val windowManagerInterface: IWindowManager,
+ private val rotationChangeProvider: RotationChangeProvider,
unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider
) : UnfoldTransitionProgressProvider {
private val scopedUnfoldTransitionProgressProvider =
ScopedUnfoldTransitionProgressProvider(unfoldTransitionProgressProvider)
- private val rotationWatcher = RotationWatcher()
private var isNaturalRotation: Boolean = false
fun init() {
- try {
- windowManagerInterface.watchRotation(rotationWatcher, context.display.displayId)
- } catch (e: RemoteException) {
- throw e.rethrowFromSystemServer()
- }
-
- onRotationChanged(context.display.rotation)
+ rotationChangeProvider.addCallback(rotationListener)
+ rotationListener.onRotationChanged(context.display.rotation)
}
- private fun onRotationChanged(rotation: Int) {
+ private val rotationListener = RotationListener { rotation ->
val isNewRotationNatural =
rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
@@ -60,12 +53,7 @@
}
override fun destroy() {
- try {
- windowManagerInterface.removeRotationWatcher(rotationWatcher)
- } catch (e: RemoteException) {
- e.rethrowFromSystemServer()
- }
-
+ rotationChangeProvider.removeCallback(rotationListener)
scopedUnfoldTransitionProgressProvider.destroy()
}
@@ -76,10 +64,4 @@
override fun removeCallback(listener: TransitionProgressListener) {
scopedUnfoldTransitionProgressProvider.removeCallback(listener)
}
-
- private inner class RotationWatcher : IRotationWatcher.Stub() {
- override fun onRotationChanged(rotation: Int) {
- this@NaturalRotationUnfoldProgressProvider.onRotationChanged(rotation)
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
deleted file mode 100644
index 6064be9..0000000
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
+++ /dev/null
@@ -1,302 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.keyguard;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.icu.text.NumberFormat;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.settingslib.Utils;
-import com.android.systemui.R;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.shared.clocks.AnimatableClockView;
-import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
-import com.android.systemui.statusbar.policy.BatteryController;
-import com.android.systemui.util.ViewController;
-
-import java.io.PrintWriter;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.TimeZone;
-import java.util.concurrent.Executor;
-
-import javax.inject.Inject;
-
-/**
- * Controller for an AnimatableClockView on the keyguard. Instantiated by
- * {@link KeyguardClockSwitchController}.
- */
-public class AnimatableClockController extends ViewController<AnimatableClockView> {
- private static final String TAG = "AnimatableClockCtrl";
- private static final int FORMAT_NUMBER = 1234567890;
-
- private final StatusBarStateController mStatusBarStateController;
- private final BroadcastDispatcher mBroadcastDispatcher;
- private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
- private final BatteryController mBatteryController;
- private final int mDozingColor = Color.WHITE;
- private Optional<RegionSamplingHelper> mRegionSamplingHelper = Optional.empty();
- private Rect mSamplingBounds = new Rect();
- private int mLockScreenColor;
- private final boolean mRegionSamplingEnabled;
-
- private boolean mIsDozing;
- private boolean mIsCharging;
- private float mDozeAmount;
- boolean mKeyguardShowing;
- private Locale mLocale;
-
- private final NumberFormat mBurmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my"));
- private final String mBurmeseNumerals;
- private final float mBurmeseLineSpacing;
- private final float mDefaultLineSpacing;
-
- @Inject
- public AnimatableClockController(
- AnimatableClockView view,
- StatusBarStateController statusBarStateController,
- BroadcastDispatcher broadcastDispatcher,
- BatteryController batteryController,
- KeyguardUpdateMonitor keyguardUpdateMonitor,
- @Main Resources resources,
- @Main Executor mainExecutor,
- @Background Executor bgExecutor,
- FeatureFlags featureFlags
- ) {
- super(view);
- mStatusBarStateController = statusBarStateController;
- mBroadcastDispatcher = broadcastDispatcher;
- mKeyguardUpdateMonitor = keyguardUpdateMonitor;
- mBatteryController = batteryController;
-
- mBurmeseNumerals = mBurmeseNf.format(FORMAT_NUMBER);
- mBurmeseLineSpacing = resources.getFloat(
- R.dimen.keyguard_clock_line_spacing_scale_burmese);
- mDefaultLineSpacing = resources.getFloat(
- R.dimen.keyguard_clock_line_spacing_scale);
-
- mRegionSamplingEnabled = featureFlags.isEnabled(Flags.REGION_SAMPLING);
- if (!mRegionSamplingEnabled) {
- return;
- }
-
- mRegionSamplingHelper = Optional.of(new RegionSamplingHelper(mView,
- new RegionSamplingHelper.SamplingCallback() {
- @Override
- public void onRegionDarknessChanged(boolean isRegionDark) {
- if (isRegionDark) {
- mLockScreenColor = Color.WHITE;
- } else {
- mLockScreenColor = Color.BLACK;
- }
- initColors();
- }
-
- @Override
- public Rect getSampledRegion(View sampledView) {
- mSamplingBounds = new Rect(sampledView.getLeft(), sampledView.getTop(),
- sampledView.getRight(), sampledView.getBottom());
- return mSamplingBounds;
- }
-
- @Override
- public boolean isSamplingEnabled() {
- return mRegionSamplingEnabled;
- }
- }, mainExecutor, bgExecutor)
- );
- mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> {
- regionSamplingHelper.setWindowVisible(true);
- });
- }
-
- private void reset() {
- mView.animateDoze(mIsDozing, false);
- }
-
- private final BatteryController.BatteryStateChangeCallback mBatteryCallback =
- new BatteryController.BatteryStateChangeCallback() {
- @Override
- public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
- if (mKeyguardShowing && !mIsCharging && charging) {
- mView.animateCharge(mStatusBarStateController::isDozing);
- }
- mIsCharging = charging;
- }
- };
-
- private final BroadcastReceiver mLocaleBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- updateLocale();
- }
- };
-
- private final StatusBarStateController.StateListener mStatusBarStateListener =
- new StatusBarStateController.StateListener() {
- @Override
- public void onDozeAmountChanged(float linear, float eased) {
- boolean noAnimation = (mDozeAmount == 0f && linear == 1f)
- || (mDozeAmount == 1f && linear == 0f);
- boolean isDozing = linear > mDozeAmount;
- mDozeAmount = linear;
- if (mIsDozing != isDozing) {
- mIsDozing = isDozing;
- mView.animateDoze(mIsDozing, !noAnimation);
- }
- }
- };
-
- private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
- new KeyguardUpdateMonitorCallback() {
- @Override
- public void onKeyguardVisibilityChanged(boolean showing) {
- mKeyguardShowing = showing;
- if (!mKeyguardShowing) {
- // reset state (ie: after weight animations)
- reset();
- }
- }
-
- @Override
- public void onTimeFormatChanged(String timeFormat) {
- mView.refreshFormat();
- }
-
- @Override
- public void onTimeZoneChanged(TimeZone timeZone) {
- mView.onTimeZoneChanged(timeZone);
- }
-
- @Override
- public void onUserSwitchComplete(int userId) {
- mView.refreshFormat();
- }
- };
-
- @Override
- protected void onInit() {
- mIsDozing = mStatusBarStateController.isDozing();
- }
-
- @Override
- protected void onViewAttached() {
- updateLocale();
- mBroadcastDispatcher.registerReceiver(mLocaleBroadcastReceiver,
- new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
- mDozeAmount = mStatusBarStateController.getDozeAmount();
- mIsDozing = mStatusBarStateController.isDozing() || mDozeAmount != 0;
- mBatteryController.addCallback(mBatteryCallback);
- mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
-
- mStatusBarStateController.addCallback(mStatusBarStateListener);
-
- mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> {
- regionSamplingHelper.start(mSamplingBounds);
- });
-
- mView.onTimeZoneChanged(TimeZone.getDefault());
- initColors();
- mView.animateDoze(mIsDozing, false);
- }
-
- @Override
- protected void onViewDetached() {
- mBroadcastDispatcher.unregisterReceiver(mLocaleBroadcastReceiver);
- mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback);
- mBatteryController.removeCallback(mBatteryCallback);
- mStatusBarStateController.removeCallback(mStatusBarStateListener);
- mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> {
- regionSamplingHelper.stop();
- });
- }
-
- /** Animate the clock appearance */
- public void animateAppear() {
- if (!mIsDozing) mView.animateAppearOnLockscreen();
- }
-
- /** Animate the clock appearance when a foldable device goes from fully-open/half-open state to
- * fully folded state and it goes to sleep (always on display screen) */
- public void animateFoldAppear() {
- mView.animateFoldAppear(true);
- }
-
- /**
- * Updates the time for the view.
- */
- public void refreshTime() {
- mView.refreshTime();
- }
-
- /**
- * Return locallly stored dozing state.
- */
- @VisibleForTesting
- public boolean isDozing() {
- return mIsDozing;
- }
-
- private void updateLocale() {
- Locale currLocale = Locale.getDefault();
- if (!Objects.equals(currLocale, mLocale)) {
- mLocale = currLocale;
- NumberFormat nf = NumberFormat.getInstance(mLocale);
- if (nf.format(FORMAT_NUMBER).equals(mBurmeseNumerals)) {
- mView.setLineSpacingScale(mBurmeseLineSpacing);
- } else {
- mView.setLineSpacingScale(mDefaultLineSpacing);
- }
- mView.refreshFormat();
- }
- }
-
- private void initColors() {
- if (!mRegionSamplingEnabled) {
- mLockScreenColor = Utils.getColorAttrDefaultColor(getContext(),
- com.android.systemui.R.attr.wallpaperTextColorAccent);
- }
- mView.setColors(mDozingColor, mLockScreenColor);
- mView.animateDoze(mIsDozing, false);
- }
-
- /**
- * Dump information for debugging
- */
- public void dump(@NonNull PrintWriter pw) {
- pw.println(this);
- mView.dump(pw);
- mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> {
- regionSamplingHelper.dump(pw);
- });
- }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
index 0075ddd..450784e 100644
--- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
+++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
@@ -16,19 +16,29 @@
package com.android.keyguard
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.graphics.Color
import android.util.AttributeSet
+import android.view.View
import com.android.settingslib.Utils
+import com.android.systemui.animation.Interpolators
/** Displays security messages for the keyguard bouncer. */
-class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) :
+open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) :
KeyguardMessageArea(context, attrs) {
private val DEFAULT_COLOR = -1
private var mDefaultColorState: ColorStateList? = null
private var mNextMessageColorState: ColorStateList? = ColorStateList.valueOf(DEFAULT_COLOR)
+ private val animatorSet = AnimatorSet()
+ private var textAboutToShow: CharSequence? = null
+ protected open val SHOW_DURATION_MILLIS = 150L
+ protected open val HIDE_DURATION_MILLIS = 200L
override fun updateTextColor() {
var colorState = mDefaultColorState
@@ -58,4 +68,46 @@
mDefaultColorState = Utils.getColorAttr(context, android.R.attr.textColorPrimary)
super.reloadColor()
}
+
+ override fun setMessage(msg: CharSequence?) {
+ if ((msg == textAboutToShow && msg != null) || msg == text) {
+ return
+ }
+ textAboutToShow = msg
+
+ if (animatorSet.isRunning) {
+ animatorSet.cancel()
+ textAboutToShow = null
+ }
+
+ val hideAnimator =
+ ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f).apply {
+ duration = HIDE_DURATION_MILLIS
+ interpolator = Interpolators.STANDARD_ACCELERATE
+ }
+
+ hideAnimator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ super@BouncerKeyguardMessageArea.setMessage(msg)
+ }
+ }
+ )
+ val showAnimator =
+ ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f).apply {
+ duration = SHOW_DURATION_MILLIS
+ interpolator = Interpolators.STANDARD_DECELERATE
+ }
+
+ showAnimator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ textAboutToShow = null
+ }
+ }
+ )
+
+ animatorSet.playSequentially(hideAnimator, showAnimator)
+ animatorSet.start()
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 9151238..40a96b0 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -23,12 +23,21 @@
import android.text.format.DateFormat
import android.util.TypedValue
import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.DOZING_MIGRATION_1
+import com.android.systemui.flags.Flags.REGION_SAMPLING
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.log.dagger.KeyguardClockLog
import com.android.systemui.plugins.ClockController
-import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.shared.regionsampling.RegionSamplingInstance
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
@@ -38,13 +47,20 @@
import java.util.TimeZone
import java.util.concurrent.Executor
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
/**
* Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
* [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController].
*/
open class ClockEventController @Inject constructor(
- private val statusBarStateController: StatusBarStateController,
+ private val keyguardInteractor: KeyguardInteractor,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val broadcastDispatcher: BroadcastDispatcher,
private val batteryController: BatteryController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@@ -53,12 +69,14 @@
private val context: Context,
@Main private val mainExecutor: Executor,
@Background private val bgExecutor: Executor,
- private val featureFlags: FeatureFlags,
+ @KeyguardClockLog private val logBuffer: LogBuffer,
+ private val featureFlags: FeatureFlags
) {
var clock: ClockController? = null
set(value) {
field = value
if (value != null) {
+ value.setLogBuffer(logBuffer)
value.initialize(resources, dozeAmount, 0f)
updateRegionSamplers(value)
}
@@ -70,9 +88,9 @@
private var isCharging = false
private var dozeAmount = 0f
private var isKeyguardVisible = false
-
- private val regionSamplingEnabled =
- featureFlags.isEnabled(com.android.systemui.flags.Flags.REGION_SAMPLING)
+ private var isRegistered = false
+ private var disposableHandle: DisposableHandle? = null
+ private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING)
private fun updateColors() {
if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) {
@@ -165,15 +183,6 @@
}
}
- private val statusBarStateListener = object : StatusBarStateController.StateListener {
- override fun onDozeAmountChanged(linear: Float, eased: Float) {
- clock?.animations?.doze(linear)
-
- isDozing = linear > dozeAmount
- dozeAmount = linear
- }
- }
-
private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
override fun onKeyguardVisibilityChanged(visible: Boolean) {
isKeyguardVisible = visible
@@ -195,13 +204,11 @@
}
}
- init {
- isDozing = statusBarStateController.isDozing
- }
-
- fun registerListeners() {
- dozeAmount = statusBarStateController.dozeAmount
- isDozing = statusBarStateController.isDozing || dozeAmount != 0f
+ fun registerListeners(parent: View) {
+ if (isRegistered) {
+ return
+ }
+ isRegistered = true
broadcastDispatcher.registerReceiver(
localeBroadcastReceiver,
@@ -210,17 +217,31 @@
configurationController.addCallback(configListener)
batteryController.addCallback(batteryCallback)
keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
- statusBarStateController.addCallback(statusBarStateListener)
smallRegionSampler?.startRegionSampler()
largeRegionSampler?.startRegionSampler()
+ disposableHandle = parent.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ listenForDozing(this)
+ if (featureFlags.isEnabled(DOZING_MIGRATION_1)) {
+ listenForDozeAmountTransition(this)
+ } else {
+ listenForDozeAmount(this)
+ }
+ }
+ }
}
fun unregisterListeners() {
+ if (!isRegistered) {
+ return
+ }
+ isRegistered = false
+
+ disposableHandle?.dispose()
broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver)
configurationController.removeCallback(configListener)
batteryController.removeCallback(batteryCallback)
keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
- statusBarStateController.removeCallback(statusBarStateListener)
smallRegionSampler?.stopRegionSampler()
largeRegionSampler?.stopRegionSampler()
}
@@ -235,8 +256,38 @@
largeRegionSampler?.dump(pw)
}
- companion object {
- private val TAG = ClockEventController::class.simpleName
- private const val FORMAT_NUMBER = 1234567890
+ @VisibleForTesting
+ internal fun listenForDozeAmount(scope: CoroutineScope): Job {
+ return scope.launch {
+ keyguardInteractor.dozeAmount.collect {
+ dozeAmount = it
+ clock?.animations?.doze(dozeAmount)
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
+ return scope.launch {
+ keyguardTransitionInteractor.dozeAmountTransition.collect {
+ dozeAmount = it.value
+ clock?.animations?.doze(dozeAmount)
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun listenForDozing(scope: CoroutineScope): Job {
+ return scope.launch {
+ combine (
+ keyguardInteractor.dozeAmount,
+ keyguardInteractor.isDozing,
+ ) { localDozeAmount, localIsDozing ->
+ localDozeAmount > dozeAmount || localIsDozing
+ }
+ .collect { localIsDozing ->
+ isDozing = localIsDozing
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt
index 692fe83..e6a2bfa 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt
@@ -17,7 +17,6 @@
package com.android.keyguard
import android.app.StatusBarManager.SESSION_KEYGUARD
-import android.content.Context
import android.hardware.biometrics.BiometricSourceType
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.logging.UiEvent
@@ -41,11 +40,10 @@
*/
@SysUISingleton
class KeyguardBiometricLockoutLogger @Inject constructor(
- context: Context?,
private val uiEventLogger: UiEventLogger,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val sessionTracker: SessionTracker
-) : CoreStartable(context) {
+) : CoreStartable {
private var fingerprintLockedOut = false
private var faceLockedOut = false
private var encryptedOrLockdown = false
@@ -169,4 +167,4 @@
return strongAuthFlags and flagCheck != 0
}
}
-}
\ No newline at end of file
+}
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 b450ec3..d3cc7ed 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -37,9 +37,8 @@
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.plugins.ClockAnimations;
import com.android.systemui.plugins.ClockController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shared.clocks.ClockRegistry;
@@ -119,8 +118,7 @@
SecureSettings secureSettings,
@Main Executor uiExecutor,
DumpManager dumpManager,
- ClockEventController clockEventController,
- FeatureFlags featureFlags) {
+ ClockEventController clockEventController) {
super(keyguardClockSwitch);
mStatusBarStateController = statusBarStateController;
mClockRegistry = clockRegistry;
@@ -133,7 +131,6 @@
mDumpManager = dumpManager;
mClockEventController = clockEventController;
- mClockRegistry.setEnabled(featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS));
mClockChangedListener = () -> {
setClock(mClockRegistry.createCurrentClock());
};
@@ -164,7 +161,7 @@
protected void onViewAttached() {
mClockRegistry.registerClockChangeListener(mClockChangedListener);
setClock(mClockRegistry.createCurrentClock());
- mClockEventController.registerListeners();
+ mClockEventController.registerListeners(mView);
mKeyguardClockTopMargin =
mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
@@ -404,5 +401,10 @@
clock.dump(pw);
}
}
+
+ /** Gets the animations for the current clock. */
+ public ClockAnimations getClockAnimations() {
+ return getClock().getAnimations();
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index f26b905..73229c3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -152,6 +152,7 @@
}
public void startAppearAnimation() {
+ mMessageAreaController.setMessage(getInitialMessageResId());
mView.startAppearAnimation();
}
@@ -169,6 +170,11 @@
return view.indexOfChild(mView);
}
+ /** Determines the message to show in the bouncer when it first appears. */
+ protected int getInitialMessageResId() {
+ return 0;
+ }
+
/** Factory for a {@link KeyguardInputViewController}. */
public static class Factory {
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
index c2802f7..2bd3ca5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
@@ -18,7 +18,6 @@
import android.content.res.ColorStateList;
import android.content.res.Configuration;
-import android.text.TextUtils;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -100,15 +99,6 @@
mView.setMessage(resId);
}
- /**
- * Set Text if KeyguardMessageArea is empty.
- */
- public void setMessageIfEmpty(int resId) {
- if (TextUtils.isEmpty(mView.getText())) {
- setMessage(resId);
- }
- }
-
public void setNextMessageColor(ColorStateList colorState) {
mView.setNextMessageColor(colorState);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
index 29e912f..0025986 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
@@ -187,7 +187,7 @@
@Override
void resetState() {
mPasswordEntry.setTextOperationUser(UserHandle.of(KeyguardUpdateMonitor.getCurrentUser()));
- mMessageAreaController.setMessage("");
+ mMessageAreaController.setMessage(getInitialMessageResId());
final boolean wasDisabled = mPasswordEntry.isEnabled();
mView.setPasswordEntryEnabled(true);
mView.setPasswordEntryInputEnabled(true);
@@ -207,7 +207,6 @@
if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) {
showInput();
}
- mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_password);
}
private void showInput() {
@@ -324,4 +323,9 @@
//enabled input method subtype (The current IME should be LatinIME.)
|| imm.getEnabledInputMethodSubtypeList(null, false).size() > 1;
}
+
+ @Override
+ protected int getInitialMessageResId() {
+ return R.string.keyguard_enter_your_password;
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 9871645..1f0bd54 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -298,12 +298,6 @@
}
@Override
- public void onResume(int reason) {
- super.onResume(reason);
- mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pattern);
- }
-
- @Override
public boolean needsInput() {
return false;
}
@@ -361,7 +355,7 @@
}
private void displayDefaultSecurityMessage() {
- mMessageAreaController.setMessage("");
+ mMessageAreaController.setMessage(getInitialMessageResId());
}
private void handleAttemptLockout(long elapsedRealtimeDeadline) {
@@ -392,4 +386,9 @@
}.start();
}
+
+ @Override
+ protected int getInitialMessageResId() {
+ return R.string.keyguard_enter_your_pattern;
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 59a018a..f7423ed 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -127,7 +127,6 @@
public void onResume(int reason) {
super.onResume(reason);
mPasswordEntry.requestFocus();
- mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
}
@Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index 89fcc47..7876f07 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -76,20 +76,13 @@
}
@Override
- void resetState() {
- super.resetState();
- mMessageAreaController.setMessage("");
- }
-
- @Override
- public void startAppearAnimation() {
- mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
- super.startAppearAnimation();
- }
-
- @Override
public boolean startDisappearAnimation(Runnable finishRunnable) {
return mView.startDisappearAnimation(
mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable);
}
+
+ @Override
+ protected int getInitialMessageResId() {
+ return R.string.keyguard_enter_your_pin;
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index c34db15..93ee151 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -67,7 +67,6 @@
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.WindowManager;
-import android.widget.AdapterView;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@@ -318,7 +317,8 @@
}
void initMode(@Mode int mode, GlobalSettings globalSettings, FalsingManager falsingManager,
- UserSwitcherController userSwitcherController) {
+ UserSwitcherController userSwitcherController,
+ UserSwitcherViewMode.UserSwitcherCallback userSwitcherCallback) {
if (mCurrentMode == mode) return;
Log.i(TAG, "Switching mode from " + modeToString(mCurrentMode) + " to "
+ modeToString(mode));
@@ -330,7 +330,7 @@
mViewMode = new OneHandedViewMode();
break;
case MODE_USER_SWITCHER:
- mViewMode = new UserSwitcherViewMode();
+ mViewMode = new UserSwitcherViewMode(userSwitcherCallback);
break;
default:
mViewMode = new DefaultViewMode();
@@ -864,6 +864,12 @@
private UserSwitcherController.UserSwitchCallback mUserSwitchCallback =
this::setupUserSwitcher;
+ private UserSwitcherCallback mUserSwitcherCallback;
+
+ UserSwitcherViewMode(UserSwitcherCallback userSwitcherCallback) {
+ mUserSwitcherCallback = userSwitcherCallback;
+ }
+
@Override
public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings,
@NonNull KeyguardSecurityViewFlipper viewFlipper,
@@ -1040,34 +1046,25 @@
}
};
- if (adapter.getCount() < 2) {
- // The drop down arrow is at index 1
- ((LayerDrawable) mUserSwitcher.getBackground()).getDrawable(1).setAlpha(0);
- anchor.setClickable(false);
- return;
- } else {
- ((LayerDrawable) mUserSwitcher.getBackground()).getDrawable(1).setAlpha(255);
- }
-
anchor.setOnClickListener((v) -> {
if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
mPopup = new KeyguardUserSwitcherPopupMenu(v.getContext(), mFalsingManager);
mPopup.setAnchorView(anchor);
mPopup.setAdapter(adapter);
- mPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- public void onItemClick(AdapterView parent, View view, int pos, long id) {
- if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
- if (!view.isEnabled()) return;
-
- // Subtract one for the header
- UserRecord user = adapter.getItem(pos - 1);
- if (!user.isCurrent) {
- adapter.onUserListItemClicked(user);
- }
- mPopup.dismiss();
- mPopup = null;
- }
- });
+ mPopup.setOnItemClickListener((parent, view, pos, id) -> {
+ if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
+ if (!view.isEnabled()) return;
+ // Subtract one for the header
+ UserRecord user = adapter.getItem(pos - 1);
+ if (user.isManageUsers || user.isAddSupervisedUser) {
+ mUserSwitcherCallback.showUnlockToContinueMessage();
+ }
+ if (!user.isCurrent) {
+ adapter.onUserListItemClicked(user);
+ }
+ mPopup.dismiss();
+ mPopup = null;
+ });
mPopup.show();
});
}
@@ -1122,6 +1119,10 @@
constraintSet.applyTo(mView);
}
}
+
+ interface UserSwitcherCallback {
+ void showUnlockToContinueMessage();
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index d448f40..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() {
@@ -620,7 +623,9 @@
mode = KeyguardSecurityContainer.MODE_ONE_HANDED;
}
- mView.initMode(mode, mGlobalSettings, mFalsingManager, mUserSwitcherController);
+ mView.initMode(mode, mGlobalSettings, mFalsingManager, mUserSwitcherController,
+ () -> showMessage(getContext().getString(R.string.keyguard_unlock_to_continue),
+ null));
}
public void reportFailedUnlockAttempt(int userId, int timeoutMs) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt
index 9eb2c11..c9128e5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt
@@ -109,12 +109,13 @@
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
runningSecurityShiftAnimator = null
+ if (shouldRestoreLayerType) {
+ v.setLayerType(View.LAYER_TYPE_NONE, /* paint= */ null)
+ }
}
}
)
- var finishedFadingOutNonSecurityView = false
-
runningSecurityShiftAnimator.addUpdateListener { animation: ValueAnimator ->
val switchPoint = SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION
val isFadingOut = animation.animatedFraction < switchPoint
@@ -153,6 +154,13 @@
startRect.right + currentTranslation,
startRect.bottom
)
+ } else {
+ v.setLeftTopRightBottom(
+ startRect.left,
+ startRect.top,
+ startRect.right,
+ startRect.bottom
+ )
}
} else {
// And in again over the remaining (100-X)%.
@@ -175,32 +183,13 @@
endRect.right - translationRemaining,
endRect.bottom
)
- }
- }
- if (animation.animatedFraction == 1.0f && shouldRestoreLayerType) {
- v.setLayerType(View.LAYER_TYPE_NONE, /* paint= */ null)
- }
-
- // For views that are not the security view flipper, we do not want to apply
- // an x translation animation. Instead, we want to fade out, move to final position and
- // then fade in.
- if (v !is KeyguardSecurityViewFlipper) {
- // Opacity goes close to 0 but does not fully get to 0.
- if (opacity - 0.001f < 0f) {
+ } else {
v.setLeftTopRightBottom(
endRect.left,
endRect.top,
endRect.right,
endRect.bottom
)
- finishedFadingOutNonSecurityView = true
- } else if (!finishedFadingOutNonSecurityView) {
- v.setLeftTopRightBottom(
- startRect.left,
- startRect.top,
- startRect.right,
- startRect.bottom
- )
}
}
}
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 d58ba80..cb1330d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -1428,6 +1428,16 @@
}
}
+ private void notifyNonStrongBiometricStateChanged(int userId) {
+ Assert.isMainThread();
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+ if (cb != null) {
+ cb.onNonStrongBiometricAllowedChanged(userId);
+ }
+ }
+ }
+
private void dispatchErrorMessage(CharSequence message) {
Assert.isMainThread();
for (int i = 0; i < mCallbacks.size(); i++) {
@@ -1778,11 +1788,14 @@
public static class StrongAuthTracker extends LockPatternUtils.StrongAuthTracker {
private final Consumer<Integer> mStrongAuthRequiredChangedCallback;
+ private final Consumer<Integer> mNonStrongBiometricAllowedChanged;
public StrongAuthTracker(Context context,
- Consumer<Integer> strongAuthRequiredChangedCallback) {
+ Consumer<Integer> strongAuthRequiredChangedCallback,
+ Consumer<Integer> nonStrongBiometricAllowedChanged) {
super(context);
mStrongAuthRequiredChangedCallback = strongAuthRequiredChangedCallback;
+ mNonStrongBiometricAllowedChanged = nonStrongBiometricAllowedChanged;
}
public boolean isUnlockingWithBiometricAllowed(boolean isStrongBiometric) {
@@ -1800,6 +1813,14 @@
public void onStrongAuthRequiredChanged(int userId) {
mStrongAuthRequiredChangedCallback.accept(userId);
}
+
+ // TODO(b/247091681): Renaming the inappropriate onIsNonStrongBiometricAllowedChanged
+ // callback wording for Weak/Convenience idle timeout constraint that only allow
+ // Strong-Auth
+ @Override
+ public void onIsNonStrongBiometricAllowedChanged(int userId) {
+ mNonStrongBiometricAllowedChanged.accept(userId);
+ }
}
protected void handleStartedWakingUp() {
@@ -1948,7 +1969,8 @@
mSubscriptionManager = subscriptionManager;
mTelephonyListenerManager = telephonyListenerManager;
mDeviceProvisioned = isDeviceProvisionedInSettingsDb();
- mStrongAuthTracker = new StrongAuthTracker(context, this::notifyStrongAuthStateChanged);
+ mStrongAuthTracker = new StrongAuthTracker(context, this::notifyStrongAuthStateChanged,
+ this::notifyNonStrongBiometricStateChanged);
mBackgroundExecutor = backgroundExecutor;
mBroadcastDispatcher = broadcastDispatcher;
mInteractionJankMonitor = interactionJankMonitor;
@@ -2344,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();
}
/**
@@ -2358,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
@@ -2409,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
) {
@@ -2439,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
@@ -2706,7 +2726,7 @@
return shouldListen;
}
- private void maybeLogListenerModelData(KeyguardListenModel model) {
+ private void maybeLogListenerModelData(@NonNull KeyguardListenModel model) {
mLogger.logKeyguardListenerModel(model);
if (model instanceof KeyguardActiveUnlockModel) {
@@ -3178,14 +3198,7 @@
* Whether the keyguard is showing and not occluded.
*/
public boolean isKeyguardVisible() {
- return isKeyguardShowing() && !mKeyguardOccluded;
- }
-
- /**
- * Whether the keyguard is showing. It may still be occluded and not visible.
- */
- public boolean isKeyguardShowing() {
- return mKeyguardShowing;
+ return mKeyguardShowing && !mKeyguardOccluded;
}
/**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index bc5ab88..c06e1dc 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -291,4 +291,9 @@
* Called when the notification shade is expanded or collapsed.
*/
public void onShadeExpandedChanged(boolean expanded) { }
+
+ /**
+ * Called when the non-strong biometric state changed.
+ */
+ public void onNonStrongBiometricAllowedChanged(int userId) { }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
index 8293c74..90f0446 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
@@ -24,10 +24,10 @@
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.phone.BiometricUnlockController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
/**
* Interface to control Keyguard View. It should be implemented by KeyguardViewManagers, which
@@ -94,11 +94,6 @@
void setOccluded(boolean occluded, boolean animate);
/**
- * @return Whether the keyguard is showing
- */
- boolean isShowing();
-
- /**
* Dismisses the keyguard by going to the next screen or making it gone.
*/
void dismissAndCollapse();
@@ -185,7 +180,7 @@
*/
void registerCentralSurfaces(CentralSurfaces centralSurfaces,
NotificationPanelViewController notificationPanelViewController,
- @Nullable PanelExpansionStateManager panelExpansionStateManager,
+ @Nullable ShadeExpansionStateManager shadeExpansionStateManager,
BiometricUnlockController biometricUnlockController,
View notificationContainer,
KeyguardBypassController bypassController);
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..9767313
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard.dagger;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.UserHandle;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.shared.clocks.ClockRegistry;
+import com.android.systemui.shared.clocks.DefaultClockProvider;
+import com.android.systemui.shared.plugins.PluginManager;
+
+import dagger.Module;
+import dagger.Provides;
+
+/** Dagger Module for clocks. */
+@Module
+public abstract class ClockRegistryModule {
+ /** Provide the ClockRegistry as a singleton so that it is not instantiated more than once. */
+ @Provides
+ @SysUISingleton
+ public static ClockRegistry getClockRegistry(
+ @Application Context context,
+ PluginManager pluginManager,
+ @Main Handler handler,
+ DefaultClockProvider defaultClockProvider,
+ FeatureFlags featureFlags) {
+ return new ClockRegistry(
+ context,
+ pluginManager,
+ handler,
+ featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS),
+ UserHandle.USER_ALL,
+ defaultClockProvider);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
index 2c2ab7b..6264ce7 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
@@ -17,9 +17,9 @@
package com.android.keyguard.logging
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
import com.android.systemui.log.dagger.BiometricMessagesLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
import javax.inject.Inject
/** Helper class for logging for [com.android.systemui.biometrics.FaceHelpMessageDeferral] */
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index 50012a5..46f3d4e 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -16,15 +16,15 @@
package com.android.keyguard.logging
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
-import com.android.systemui.log.MessageInitializer
-import com.android.systemui.log.MessagePrinter
import com.android.systemui.log.dagger.KeyguardLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel.WARNING
+import com.android.systemui.plugins.log.MessageInitializer
+import com.android.systemui.plugins.log.MessagePrinter
import com.google.errorprone.annotations.CompileTimeConstant
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 2eee957..2f79e30 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -22,13 +22,13 @@
import com.android.keyguard.ActiveUnlockConfig
import com.android.keyguard.KeyguardListenModel
import com.android.keyguard.KeyguardUpdateMonitorCallback
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel.WARNING
import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog
import com.google.errorprone.annotations.CompileTimeConstant
import javax.inject.Inject
@@ -51,7 +51,7 @@
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/ChooserSelector.kt b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
index 109be40..a89cbf5 100644
--- a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
+++ b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
@@ -3,6 +3,7 @@
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
+import android.util.Log
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
@@ -18,11 +19,11 @@
@SysUISingleton
class ChooserSelector @Inject constructor(
- context: Context,
+ private val context: Context,
private val featureFlags: FeatureFlags,
@Application private val coroutineScope: CoroutineScope,
@Background private val bgDispatcher: CoroutineDispatcher
-) : CoreStartable(context) {
+) : CoreStartable {
private val packageManager = context.packageManager
private val chooserComponent = ComponentName.unflattenFromString(
@@ -55,7 +56,11 @@
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
- packageManager.setComponentEnabledSetting(chooserComponent, newState, /* flags = */ 0)
+ try {
+ packageManager.setComponentEnabledSetting(chooserComponent, newState, /* flags = */ 0)
+ } catch (e: IllegalArgumentException) {
+ Log.w("ChooserSelector", "Unable to set IntentResolver enabled=" + enabled, e)
+ }
}
suspend inline fun awaitCancellation(): Nothing = suspendCancellableCoroutine { }
diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
index 0201cdc..929ebea 100644
--- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java
+++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
@@ -16,39 +16,41 @@
package com.android.systemui;
-import android.content.Context;
import android.content.res.Configuration;
import androidx.annotation.NonNull;
-import com.android.internal.annotations.VisibleForTesting;
-
import java.io.PrintWriter;
/**
- * A top-level module of system UI code (sometimes called "system UI services" elsewhere in code).
- * Which CoreStartable modules are loaded can be controlled via a config resource.
+ * Code that needs to be run when SystemUI is started.
+ *
+ * Which CoreStartable modules are loaded is controlled via the dagger graph. Bind them into the
+ * CoreStartable map with code such as:
+ *
+ * <pre>
+ * @Binds
+ * @IntoMap
+ * @ClassKey(FoobarStartable::class)
+ * abstract fun bind(impl: FoobarStartable): CoreStartable
+ * </pre>
*
* @see SystemUIApplication#startServicesIfNeeded()
*/
-public abstract class CoreStartable implements Dumpable {
- protected final Context mContext;
-
- public CoreStartable(Context context) {
- mContext = context;
- }
+public interface CoreStartable extends Dumpable {
/** Main entry point for implementations. Called shortly after app startup. */
- public abstract void start();
+ void start();
- protected void onConfigurationChanged(Configuration newConfig) {
+ /** */
+ default void onConfigurationChanged(Configuration newConfig) {
}
@Override
- public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ default void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
}
- @VisibleForTesting
- protected void onBootCompleted() {
+ /** Called when the device reports BOOT_COMPLETED. */
+ default void onBootCompleted() {
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt
index a3351e1..5d52056 100644
--- a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt
+++ b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt
@@ -86,30 +86,38 @@
onUpdate()
}
- fun onDisplayChanged(newDisplayUniqueId: String?) {
+ fun updateConfiguration(newDisplayUniqueId: String?) {
+ val info = DisplayInfo()
+ context.display?.getDisplayInfo(info)
val oldMode: Display.Mode? = displayMode
- val display: Display? = context.display
- displayMode = display?.mode
+ displayMode = info.mode
- if (displayUniqueId != display?.uniqueId) {
- displayUniqueId = display?.uniqueId
- shouldDrawCutout = DisplayCutout.getFillBuiltInDisplayCutout(
- context.resources, displayUniqueId
- )
- }
+ updateDisplayUniqueId(info.uniqueId)
// Skip if display mode or cutout hasn't changed.
if (!displayModeChanged(oldMode, displayMode) &&
- display?.cutout == displayInfo.displayCutout) {
+ displayInfo.displayCutout == info.displayCutout &&
+ displayRotation == info.rotation) {
return
}
- if (newDisplayUniqueId == display?.uniqueId) {
+ if (newDisplayUniqueId == info.uniqueId) {
+ displayRotation = info.rotation
updateCutout()
updateProtectionBoundingPath()
onUpdate()
}
}
+ open fun updateDisplayUniqueId(newDisplayUniqueId: String?) {
+ if (displayUniqueId != newDisplayUniqueId) {
+ displayUniqueId = newDisplayUniqueId
+ shouldDrawCutout = DisplayCutout.getFillBuiltInDisplayCutout(
+ context.resources, displayUniqueId
+ )
+ invalidate()
+ }
+ }
+
open fun updateRotation(rotation: Int) {
displayRotation = rotation
updateCutout()
diff --git a/packages/SystemUI/src/com/android/systemui/Dumpable.java b/packages/SystemUI/src/com/android/systemui/Dumpable.java
index 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/LatencyTester.java b/packages/SystemUI/src/com/android/systemui/LatencyTester.java
index 9cdce64..8f41956 100644
--- a/packages/SystemUI/src/com/android/systemui/LatencyTester.java
+++ b/packages/SystemUI/src/com/android/systemui/LatencyTester.java
@@ -46,7 +46,7 @@
* system that are used for testing the latency.
*/
@SysUISingleton
-public class LatencyTester extends CoreStartable {
+public class LatencyTester implements CoreStartable {
private static final boolean DEFAULT_ENABLED = Build.IS_ENG;
private static final String
ACTION_FINGERPRINT_WAKE =
@@ -62,13 +62,11 @@
@Inject
public LatencyTester(
- Context context,
BiometricUnlockController biometricUnlockController,
BroadcastDispatcher broadcastDispatcher,
DeviceConfigProxy deviceConfigProxy,
@Main DelayableExecutor mainExecutor
) {
- super(context);
mBiometricUnlockController = biometricUnlockController;
mBroadcastDispatcher = broadcastDispatcher;
mDeviceConfigProxy = deviceConfigProxy;
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/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 2e13903..11d579d 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -105,7 +105,7 @@
* for antialiasing and emulation purposes.
*/
@SysUISingleton
-public class ScreenDecorations extends CoreStartable implements Tunable , Dumpable {
+public class ScreenDecorations implements CoreStartable, Tunable , Dumpable {
private static final boolean DEBUG = false;
private static final String TAG = "ScreenDecorations";
@@ -130,6 +130,7 @@
@VisibleForTesting
protected boolean mIsRegistered;
private final BroadcastDispatcher mBroadcastDispatcher;
+ private final Context mContext;
private final Executor mMainExecutor;
private final TunerService mTunerService;
private final SecureSettings mSecureSettings;
@@ -308,7 +309,7 @@
ThreadFactory threadFactory,
PrivacyDotDecorProviderFactory dotFactory,
FaceScanningProviderFactory faceScanningFactory) {
- super(context);
+ mContext = context;
mMainExecutor = mainExecutor;
mSecureSettings = secureSettings;
mBroadcastDispatcher = broadcastDispatcher;
@@ -455,7 +456,6 @@
}
}
- boolean needToUpdateProviderViews = false;
final String newUniqueId = mDisplayInfo.uniqueId;
if (!Objects.equals(newUniqueId, mDisplayUniqueId)) {
mDisplayUniqueId = newUniqueId;
@@ -473,37 +473,6 @@
setupDecorations();
return;
}
-
- if (mScreenDecorHwcLayer != null) {
- updateHwLayerRoundedCornerDrawable();
- updateHwLayerRoundedCornerExistAndSize();
- }
- needToUpdateProviderViews = true;
- }
-
- final float newRatio = getPhysicalPixelDisplaySizeRatio();
- if (mRoundedCornerResDelegate.getPhysicalPixelDisplaySizeRatio() != newRatio) {
- mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(newRatio);
- if (mScreenDecorHwcLayer != null) {
- updateHwLayerRoundedCornerExistAndSize();
- }
- needToUpdateProviderViews = true;
- }
-
- if (needToUpdateProviderViews) {
- updateOverlayProviderViews(null);
- } else {
- updateOverlayProviderViews(new Integer[] {
- mFaceScanningViewId,
- R.id.display_cutout,
- R.id.display_cutout_left,
- R.id.display_cutout_right,
- R.id.display_cutout_bottom,
- });
- }
-
- if (mScreenDecorHwcLayer != null) {
- mScreenDecorHwcLayer.onDisplayChanged(newUniqueId);
}
}
};
@@ -973,7 +942,7 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(Configuration newConfig) {
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
Log.i(TAG, "ScreenDecorations is disabled");
return;
@@ -1069,9 +1038,11 @@
&& (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod))) {
mRotation = newRotation;
mDisplayMode = newMod;
+ mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(
+ getPhysicalPixelDisplaySizeRatio());
if (mScreenDecorHwcLayer != null) {
mScreenDecorHwcLayer.pendingConfigChange = false;
- mScreenDecorHwcLayer.updateRotation(mRotation);
+ mScreenDecorHwcLayer.updateConfiguration(mDisplayUniqueId);
updateHwLayerRoundedCornerExistAndSize();
updateHwLayerRoundedCornerDrawable();
}
@@ -1110,7 +1081,8 @@
context.getResources(), context.getDisplay().getUniqueId());
}
- private void updateOverlayProviderViews(@Nullable Integer[] filterIds) {
+ @VisibleForTesting
+ void updateOverlayProviderViews(@Nullable Integer[] filterIds) {
if (mOverlays == null) {
return;
}
diff --git a/packages/SystemUI/src/com/android/systemui/SliceBroadcastRelayHandler.java b/packages/SystemUI/src/com/android/systemui/SliceBroadcastRelayHandler.java
index 1f2de4c..5bd85a7 100644
--- a/packages/SystemUI/src/com/android/systemui/SliceBroadcastRelayHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/SliceBroadcastRelayHandler.java
@@ -38,16 +38,17 @@
* @see SliceBroadcastRelay
*/
@SysUISingleton
-public class SliceBroadcastRelayHandler extends CoreStartable {
+public class SliceBroadcastRelayHandler implements CoreStartable {
private static final String TAG = "SliceBroadcastRelay";
private static final boolean DEBUG = false;
private final ArrayMap<Uri, BroadcastRelay> mRelays = new ArrayMap<>();
+ private final Context mContext;
private final BroadcastDispatcher mBroadcastDispatcher;
@Inject
public SliceBroadcastRelayHandler(Context context, BroadcastDispatcher broadcastDispatcher) {
- super(context);
+ mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 9138b23..8415ef8 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -47,8 +47,6 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.util.NotificationChannels;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
@@ -296,14 +294,10 @@
CoreStartable startable;
if (DEBUG) Log.d(TAG, "loading: " + clsName);
try {
- Constructor<?> constructor = Class.forName(clsName).getConstructor(
- Context.class);
- startable = (CoreStartable) constructor.newInstance(this);
+ startable = (CoreStartable) Class.forName(clsName).newInstance();
} catch (ClassNotFoundException
- | NoSuchMethodException
| IllegalAccessException
- | InstantiationException
- | InvocationTargetException ex) {
+ | InstantiationException ex) {
throw new RuntimeException(ex);
}
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/VendorServices.java b/packages/SystemUI/src/com/android/systemui/VendorServices.java
index 139448c0..a320939 100644
--- a/packages/SystemUI/src/com/android/systemui/VendorServices.java
+++ b/packages/SystemUI/src/com/android/systemui/VendorServices.java
@@ -16,15 +16,12 @@
package com.android.systemui;
-import android.content.Context;
-
/**
* Placeholder for any vendor-specific services.
*/
-public class VendorServices extends CoreStartable {
+public class VendorServices implements CoreStartable {
- public VendorServices(Context context) {
- super(context);
+ public VendorServices() {
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index a1288b5..9f1c9b4 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -69,7 +69,7 @@
* Class to register system actions with accessibility framework.
*/
@SysUISingleton
-public class SystemActions extends CoreStartable {
+public class SystemActions implements CoreStartable {
private static final String TAG = "SystemActions";
/**
@@ -177,6 +177,7 @@
private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
private final SystemActionsBroadcastReceiver mReceiver;
+ private final Context mContext;
private final Optional<Recents> mRecentsOptional;
private Locale mLocale;
private final AccessibilityManager mA11yManager;
@@ -190,7 +191,7 @@
NotificationShadeWindowController notificationShadeController,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
Optional<Recents> recentsOptional) {
- super(context);
+ mContext = context;
mRecentsOptional = recentsOptional;
mReceiver = new SystemActionsBroadcastReceiver();
mLocale = mContext.getResources().getConfiguration().getLocales().get(0);
@@ -219,7 +220,6 @@
@Override
public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
final Locale locale = mContext.getResources().getConfiguration().getLocales().get(0);
if (!locale.equals(mLocale)) {
mLocale = locale;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
index 6e14ddc..ab11fce 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
@@ -52,11 +52,12 @@
* when {@code IStatusBar#requestWindowMagnificationConnection(boolean)} is called.
*/
@SysUISingleton
-public class WindowMagnification extends CoreStartable implements WindowMagnifierCallback,
+public class WindowMagnification implements CoreStartable, WindowMagnifierCallback,
CommandQueue.Callbacks {
private static final String TAG = "WindowMagnification";
private final ModeSwitchesController mModeSwitchesController;
+ private final Context mContext;
private final Handler mHandler;
private final AccessibilityManager mAccessibilityManager;
private final CommandQueue mCommandQueue;
@@ -102,7 +103,7 @@
public WindowMagnification(Context context, @Main Handler mainHandler,
CommandQueue commandQueue, ModeSwitchesController modeSwitchesController,
SysUiState sysUiState, OverviewProxyService overviewProxyService) {
- super(context);
+ mContext = context;
mHandler = mainHandler;
mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
mCommandQueue = commandQueue;
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
index 33e6ca4..9b441ad 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
@@ -21,6 +21,8 @@
import android.view.Display;
import android.view.Surface;
+import com.android.systemui.R;
+
/**
* Utility class for determining screen and corner dimensions.
*/
@@ -82,17 +84,13 @@
* where the curve ends), in pixels.
*/
public static int getCornerRadiusBottom(Context context) {
- int radius = 0;
-
- int resourceId = context.getResources().getIdentifier("config_rounded_mask_size_bottom",
- "dimen", "com.android.systemui");
- if (resourceId > 0) {
- radius = context.getResources().getDimensionPixelSize(resourceId);
- }
+ int radius = context.getResources().getDimensionPixelSize(
+ R.dimen.config_rounded_mask_size_bottom);
if (radius == 0) {
radius = getCornerRadiusDefault(context);
}
+
return radius;
}
@@ -101,28 +99,17 @@
* the curve ends), in pixels.
*/
public static int getCornerRadiusTop(Context context) {
- int radius = 0;
-
- int resourceId = context.getResources().getIdentifier("config_rounded_mask_size_top",
- "dimen", "com.android.systemui");
- if (resourceId > 0) {
- radius = context.getResources().getDimensionPixelSize(resourceId);
- }
+ int radius = context.getResources().getDimensionPixelSize(
+ R.dimen.config_rounded_mask_size_top);
if (radius == 0) {
radius = getCornerRadiusDefault(context);
}
+
return radius;
}
private static int getCornerRadiusDefault(Context context) {
- int radius = 0;
-
- int resourceId = context.getResources().getIdentifier("config_rounded_mask_size",
- "dimen", "com.android.systemui");
- if (resourceId > 0) {
- radius = context.getResources().getDimensionPixelSize(resourceId);
- }
- return radius;
+ return context.getResources().getDimensionPixelSize(R.dimen.config_rounded_mask_size);
}
}
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 aae92ad..c015a21 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -102,7 +102,7 @@
* {@link com.android.keyguard.KeyguardUpdateMonitor}
*/
@SysUISingleton
-public class AuthController extends CoreStartable implements CommandQueue.Callbacks,
+public class AuthController implements CoreStartable, CommandQueue.Callbacks,
AuthDialogCallback, DozeReceiver {
private static final String TAG = "AuthController";
@@ -110,6 +110,7 @@
private static final int SENSOR_PRIVACY_DELAY = 500;
private final Handler mHandler;
+ private final Context mContext;
private final Execution mExecution;
private final CommandQueue mCommandQueue;
private final StatusBarStateController mStatusBarStateController;
@@ -293,6 +294,8 @@
}
});
mUdfpsController.setAuthControllerUpdateUdfpsLocation(this::updateUdfpsLocation);
+ mUdfpsController.setUdfpsDisplayMode(new UdfpsDisplayMode(mContext, mExecution,
+ this));
mUdfpsBounds = mUdfpsProps.get(0).getLocation().getRect();
}
@@ -625,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) {
@@ -669,7 +661,7 @@
@NonNull InteractionJankMonitor jankMonitor,
@Main Handler handler,
@Background DelayableExecutor bgExecutor) {
- super(context);
+ mContext = context;
mExecution = execution;
mUserManager = userManager;
mLockPatternUtils = lockPatternUtils;
@@ -963,8 +955,6 @@
} else {
Log.w(TAG, "onBiometricError callback but dialog is gone");
}
-
- onCancelUdfps();
}
@Override
@@ -1099,8 +1089,7 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
+ public void onConfigurationChanged(Configuration newConfig) {
updateSensorLocations();
// Save the state of the current dialog (buttons showing, etc)
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/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
index 3ad2bef..4130cf5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
@@ -22,9 +22,9 @@
import com.android.systemui.animation.Interpolators
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionListener
+import com.android.systemui.shade.ShadeExpansionStateManager
import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
import com.android.systemui.util.ViewController
import java.io.PrintWriter
@@ -41,7 +41,7 @@
abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>(
view: T,
protected val statusBarStateController: StatusBarStateController,
- protected val panelExpansionStateManager: PanelExpansionStateManager,
+ protected val shadeExpansionStateManager: ShadeExpansionStateManager,
protected val dialogManager: SystemUIDialogManager,
private val dumpManager: DumpManager
) : ViewController<T>(view), Dumpable {
@@ -54,7 +54,7 @@
private var dialogAlphaAnimator: ValueAnimator? = null
private val dialogListener = SystemUIDialogManager.Listener { runDialogAlphaAnimator() }
- private val panelExpansionListener = PanelExpansionListener { event ->
+ private val shadeExpansionListener = ShadeExpansionListener { event ->
// Notification shade can be expanded but not visible (fraction: 0.0), for example
// when a heads-up notification (HUN) is showing.
notificationShadeVisible = event.expanded && event.fraction > 0f
@@ -108,13 +108,13 @@
}
override fun onViewAttached() {
- panelExpansionStateManager.addExpansionListener(panelExpansionListener)
+ shadeExpansionStateManager.addExpansionListener(shadeExpansionListener)
dialogManager.registerListener(dialogListener)
dumpManager.registerDumpable(dumpTag, this)
}
override fun onViewDetached() {
- panelExpansionStateManager.removeExpansionListener(panelExpansionListener)
+ shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener)
dialogManager.unregisterListener(dialogListener)
dumpManager.unregisterDumpable(dumpTag)
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
index 4cd40d2..e6aeb43 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
@@ -17,8 +17,8 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
/**
* Class that coordinates non-HBM animations for biometric prompt.
@@ -26,13 +26,13 @@
class UdfpsBpViewController(
view: UdfpsBpView,
statusBarStateController: StatusBarStateController,
- panelExpansionStateManager: PanelExpansionStateManager,
+ shadeExpansionStateManager: ShadeExpansionStateManager,
systemUIDialogManager: SystemUIDialogManager,
dumpManager: DumpManager
) : UdfpsAnimationViewController<UdfpsBpView>(
view,
statusBarStateController,
- panelExpansionStateManager,
+ shadeExpansionStateManager,
systemUIDialogManager,
dumpManager
) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 412dc05..b49d452 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -63,12 +63,12 @@
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.SystemUIDialogManager;
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -111,7 +111,7 @@
private final WindowManager mWindowManager;
private final DelayableExecutor mFgExecutor;
@NonNull private final Executor mBiometricExecutor;
- @NonNull private final PanelExpansionStateManager mPanelExpansionStateManager;
+ @NonNull private final ShadeExpansionStateManager mShadeExpansionStateManager;
@NonNull private final StatusBarStateController mStatusBarStateController;
@NonNull private final KeyguardStateController mKeyguardStateController;
@NonNull private final StatusBarKeyguardViewManager mKeyguardViewManager;
@@ -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;
@@ -205,7 +205,7 @@
mFgExecutor.execute(() -> UdfpsController.this.showUdfpsOverlay(
new UdfpsControllerOverlay(mContext, mFingerprintManager, mInflater,
mWindowManager, mAccessibilityManager, mStatusBarStateController,
- mPanelExpansionStateManager, mKeyguardViewManager,
+ mShadeExpansionStateManager, mKeyguardViewManager,
mKeyguardUpdateMonitor, mDialogManager, mDumpManager,
mLockscreenShadeTransitionController, mConfigurationController,
mSystemClock, mKeyguardStateController,
@@ -245,7 +245,7 @@
mAcquiredReceived = true;
final UdfpsView view = mOverlay.getOverlayView();
if (view != null) {
- view.unconfigureDisplay();
+ unconfigureDisplay(view);
}
if (acquiredGood) {
mOverlay.onAcquiredGood();
@@ -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.
@@ -582,7 +586,7 @@
@NonNull WindowManager windowManager,
@NonNull StatusBarStateController statusBarStateController,
@Main DelayableExecutor fgExecutor,
- @NonNull PanelExpansionStateManager panelExpansionStateManager,
+ @NonNull ShadeExpansionStateManager shadeExpansionStateManager,
@NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager,
@NonNull DumpManager dumpManager,
@NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -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,
@@ -615,7 +618,7 @@
mFingerprintManager = checkNotNull(fingerprintManager);
mWindowManager = windowManager;
mFgExecutor = fgExecutor;
- mPanelExpansionStateManager = panelExpansionStateManager;
+ mShadeExpansionStateManager = shadeExpansionStateManager;
mStatusBarStateController = statusBarStateController;
mKeyguardStateController = keyguardStateController;
mKeyguardViewManager = statusBarKeyguardViewManager;
@@ -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;
@@ -735,6 +737,19 @@
mOverlay = null;
mOrientationListener.disable();
+
+ }
+
+ private void unconfigureDisplay(@NonNull UdfpsView view) {
+ if (view.isDisplayConfigured()) {
+ view.unconfigureDisplay();
+
+ if (mCancelAodTimeoutAction != null) {
+ mCancelAodTimeoutAction.run();
+ mCancelAodTimeoutAction = null;
+ }
+ mIsAodInterruptActive = false;
+ }
}
/**
@@ -773,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);
@@ -800,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() {
- if (mOverlay != null && mOverlay.getOverlayView() != null) {
- onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView());
- }
+ @VisibleForTesting
+ void cancelAodInterrupt() {
if (!mIsAodInterruptActive) {
return;
}
- if (mCancelAodTimeoutAction != null) {
- mCancelAodTimeoutAction.run();
- mCancelAodTimeoutAction = null;
+ if (mOverlay != null && mOverlay.getOverlayView() != null) {
+ onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView());
}
+ mCancelAodTimeoutAction = null;
mIsAodInterruptActive = false;
}
@@ -909,15 +920,8 @@
}
}
mOnFingerDown = false;
- if (view.isDisplayConfigured()) {
- view.unconfigureDisplay();
- }
+ unconfigureDisplay(view);
- if (mCancelAodTimeoutAction != null) {
- mCancelAodTimeoutAction.run();
- 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 1c62f8a..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,36 +43,40 @@
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
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.phone.SystemUIDialogManager
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.time.SystemClock
private const val TAG = "UdfpsControllerOverlay"
+@VisibleForTesting
+const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui"
+
/**
* Keeps track of the overlay state and UI resources associated with a single FingerprintService
* request. This state can persist across configuration changes via the [show] and [hide]
* methods.
*/
@UiThread
-class UdfpsControllerOverlay(
+class UdfpsControllerOverlay @JvmOverloads constructor(
private val context: Context,
fingerprintManager: FingerprintManager,
private val inflater: LayoutInflater,
private val windowManager: WindowManager,
private val accessibilityManager: AccessibilityManager,
private val statusBarStateController: StatusBarStateController,
- private val panelExpansionStateManager: PanelExpansionStateManager,
+ private val shadeExpansionStateManager: ShadeExpansionStateManager,
private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val dialogManager: SystemUIDialogManager,
@@ -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(
@@ -192,17 +225,17 @@
},
enrollHelper ?: throw IllegalStateException("no enrollment helper"),
statusBarStateController,
- panelExpansionStateManager,
+ shadeExpansionStateManager,
dialogManager,
dumpManager,
overlayParams.scaleFactor
)
}
- BiometricOverlayConstants.REASON_AUTH_KEYGUARD -> {
+ REASON_AUTH_KEYGUARD -> {
UdfpsKeyguardViewController(
view.addUdfpsView(R.layout.udfps_keyguard_view),
statusBarStateController,
- panelExpansionStateManager,
+ shadeExpansionStateManager,
statusBarKeyguardViewManager,
keyguardUpdateMonitor,
dumpManager,
@@ -216,22 +249,22 @@
activityLaunchAnimator
)
}
- BiometricOverlayConstants.REASON_AUTH_BP -> {
+ REASON_AUTH_BP -> {
// note: empty controller, currently shows no visual affordance
UdfpsBpViewController(
view.addUdfpsView(R.layout.udfps_bp_view),
statusBarStateController,
- panelExpansionStateManager,
+ shadeExpansionStateManager,
dialogManager,
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,
- panelExpansionStateManager,
+ shadeExpansionStateManager,
dialogManager,
dumpManager
)
@@ -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/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
index 0b7bdde..e01273f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
@@ -22,8 +22,8 @@
import com.android.systemui.R;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.phone.SystemUIDialogManager;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
/**
* Class that coordinates non-HBM animations during enrollment.
@@ -54,11 +54,11 @@
@NonNull UdfpsEnrollView view,
@NonNull UdfpsEnrollHelper enrollHelper,
@NonNull StatusBarStateController statusBarStateController,
- @NonNull PanelExpansionStateManager panelExpansionStateManager,
+ @NonNull ShadeExpansionStateManager shadeExpansionStateManager,
@NonNull SystemUIDialogManager systemUIDialogManager,
@NonNull DumpManager dumpManager,
float scaleFactor) {
- super(view, statusBarStateController, panelExpansionStateManager, systemUIDialogManager,
+ super(view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager,
dumpManager);
mEnrollProgressBarRadius = (int) (scaleFactor * getContext().getResources().getInteger(
R.integer.config_udfpsEnrollProgressBar));
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt
index 98205cf..7c23278 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt
@@ -17,8 +17,8 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
/**
* Class that coordinates non-HBM animations for non keyguard, enrollment or biometric prompt
@@ -29,13 +29,13 @@
class UdfpsFpmOtherViewController(
view: UdfpsFpmOtherView,
statusBarStateController: StatusBarStateController,
- panelExpansionStateManager: PanelExpansionStateManager,
+ shadeExpansionStateManager: ShadeExpansionStateManager,
systemUIDialogManager: SystemUIDialogManager,
dumpManager: DumpManager
) : UdfpsAnimationViewController<UdfpsFpmOtherView>(
view,
statusBarStateController,
- panelExpansionStateManager,
+ shadeExpansionStateManager,
systemUIDialogManager,
dumpManager
) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
index 24b8933..4d7f89d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
@@ -31,6 +31,9 @@
import com.android.systemui.animation.Interpolators;
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;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
@@ -38,9 +41,6 @@
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.SystemUIDialogManager;
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.time.SystemClock;
@@ -88,7 +88,7 @@
protected UdfpsKeyguardViewController(
@NonNull UdfpsKeyguardView view,
@NonNull StatusBarStateController statusBarStateController,
- @NonNull PanelExpansionStateManager panelExpansionStateManager,
+ @NonNull ShadeExpansionStateManager shadeExpansionStateManager,
@NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager,
@NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
@NonNull DumpManager dumpManager,
@@ -100,7 +100,7 @@
@NonNull SystemUIDialogManager systemUIDialogManager,
@NonNull UdfpsController udfpsController,
@NonNull ActivityLaunchAnimator activityLaunchAnimator) {
- super(view, statusBarStateController, panelExpansionStateManager, systemUIDialogManager,
+ super(view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager,
dumpManager);
mKeyguardViewManager = statusBarKeyguardViewManager;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -153,7 +153,7 @@
mQsExpansion = mKeyguardViewManager.getQsExpansion();
updateGenericBouncerVisibility();
mConfigurationController.addCallback(mConfigurationListener);
- getPanelExpansionStateManager().addExpansionListener(mPanelExpansionListener);
+ getShadeExpansionStateManager().addExpansionListener(mShadeExpansionListener);
updateScaleFactor();
mView.updatePadding();
updateAlpha();
@@ -174,7 +174,7 @@
mKeyguardViewManager.removeAlternateAuthInterceptor(mAlternateAuthInterceptor);
mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false);
mConfigurationController.removeCallback(mConfigurationListener);
- getPanelExpansionStateManager().removeExpansionListener(mPanelExpansionListener);
+ getShadeExpansionStateManager().removeExpansionListener(mShadeExpansionListener);
if (mLockScreenShadeTransitionController.getUdfpsKeyguardViewController() == this) {
mLockScreenShadeTransitionController.setUdfpsKeyguardViewController(null);
}
@@ -219,7 +219,7 @@
mView.animateInUdfpsBouncer(null);
}
- if (mKeyguardViewManager.isOccluded()) {
+ if (mKeyguardStateController.isOccluded()) {
mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true);
}
@@ -502,9 +502,9 @@
}
};
- private final PanelExpansionListener mPanelExpansionListener = new PanelExpansionListener() {
+ private final ShadeExpansionListener mShadeExpansionListener = new ShadeExpansionListener() {
@Override
- public void onPanelExpansionChanged(PanelExpansionChangeEvent event) {
+ public void onPanelExpansionChanged(ShadeExpansionChangeEvent event) {
float fraction = event.getFraction();
mPanelExpansionFraction =
mKeyguardViewManager.isBouncerInTransit() ? BouncerPanelExpansionCalculator
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
index 96af42b..d99625a 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
@@ -17,9 +17,9 @@
package com.android.systemui.bluetooth
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.BluetoothLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import javax.inject.Inject
/** Helper class for logging bluetooth events. */
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java
index 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/broadcast/BroadcastDispatcherStartable.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcherStartable.kt
index d7b263a..c536e81 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcherStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcherStartable.kt
@@ -16,16 +16,14 @@
package com.android.systemui.broadcast
-import android.content.Context
import com.android.systemui.CoreStartable
import javax.inject.Inject
class BroadcastDispatcherStartable @Inject constructor(
- context: Context,
val broadcastDispatcher: BroadcastDispatcher
-) : CoreStartable(context) {
+) : CoreStartable {
override fun start() {
broadcastDispatcher.initialize()
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
index 5b3a982..d27708f 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
@@ -20,11 +20,11 @@
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogMessage
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogMessage
import com.android.systemui.log.dagger.BroadcastDispatcherLog
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
index d53e56f..500f280 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
@@ -18,6 +18,7 @@
import static com.android.systemui.classifier.Classifier.BACK_GESTURE;
import static com.android.systemui.classifier.Classifier.GENERIC;
+import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR;
import static com.android.systemui.classifier.FalsingManagerProxy.FALSING_SUCCESS;
import static com.android.systemui.classifier.FalsingModule.BRIGHT_LINE_GESTURE_CLASSIFERS;
@@ -220,6 +221,11 @@
return r;
}).collect(Collectors.toList());
+ // check for false tap if it is a seekbar interaction
+ if (interactionType == MEDIA_SEEKBAR) {
+ localResult[0] &= isFalseTap(LOW_PENALTY);
+ }
+
logDebug("False Gesture (type: " + interactionType + "): " + localResult[0]);
return localResult[0];
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/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
index f526277..82e5704 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
@@ -31,16 +31,19 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.util.DeviceConfigProxy;
import javax.inject.Inject;
+import javax.inject.Provider;
/**
* ClipboardListener brings up a clipboard overlay when something is copied to the clipboard.
*/
@SysUISingleton
-public class ClipboardListener extends CoreStartable
- implements ClipboardManager.OnPrimaryClipChangedListener {
+public class ClipboardListener implements
+ CoreStartable, ClipboardManager.OnPrimaryClipChangedListener {
private static final String TAG = "ClipboardListener";
@VisibleForTesting
@@ -49,21 +52,32 @@
static final String EXTRA_SUPPRESS_OVERLAY =
"com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY";
+ private final Context mContext;
private final DeviceConfigProxy mDeviceConfig;
- private final ClipboardOverlayControllerFactory mOverlayFactory;
+ private final Provider<ClipboardOverlayController> mOverlayProvider;
+ private final ClipboardOverlayControllerLegacyFactory mOverlayFactory;
private final ClipboardManager mClipboardManager;
private final UiEventLogger mUiEventLogger;
- private ClipboardOverlayController mClipboardOverlayController;
+ private final FeatureFlags mFeatureFlags;
+ private boolean mUsingNewOverlay;
+ private ClipboardOverlay mClipboardOverlay;
@Inject
public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy,
- ClipboardOverlayControllerFactory overlayFactory, ClipboardManager clipboardManager,
- UiEventLogger uiEventLogger) {
- super(context);
+ Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
+ ClipboardOverlayControllerLegacyFactory overlayFactory,
+ ClipboardManager clipboardManager,
+ UiEventLogger uiEventLogger,
+ FeatureFlags featureFlags) {
+ mContext = context;
mDeviceConfig = deviceConfigProxy;
+ mOverlayProvider = clipboardOverlayControllerProvider;
mOverlayFactory = overlayFactory;
mClipboardManager = clipboardManager;
mUiEventLogger = uiEventLogger;
+ mFeatureFlags = featureFlags;
+
+ mUsingNewOverlay = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR);
}
@Override
@@ -88,16 +102,22 @@
return;
}
- if (mClipboardOverlayController == null) {
- mClipboardOverlayController = mOverlayFactory.create(mContext);
+ boolean enabled = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR);
+ if (mClipboardOverlay == null || enabled != mUsingNewOverlay) {
+ mUsingNewOverlay = enabled;
+ if (enabled) {
+ mClipboardOverlay = mOverlayProvider.get();
+ } else {
+ mClipboardOverlay = mOverlayFactory.create(mContext);
+ }
mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource);
} else {
mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
}
- mClipboardOverlayController.setClipData(clipData, clipSource);
- mClipboardOverlayController.setOnSessionCompleteListener(() -> {
+ mClipboardOverlay.setClipData(clipData, clipSource);
+ mClipboardOverlay.setOnSessionCompleteListener(() -> {
// Session is complete, free memory until it's needed again.
- mClipboardOverlayController = null;
+ mClipboardOverlay = null;
});
}
@@ -119,4 +139,10 @@
private static boolean isEmulator() {
return SystemProperties.getBoolean("ro.boot.qemu", false);
}
+
+ interface ClipboardOverlay {
+ void setClipData(ClipData clipData, String clipSource);
+
+ void setOnSessionCompleteListener(Runnable runnable);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index 7e499eb..9f338d1 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -17,7 +17,6 @@
package com.android.systemui.clipboardoverlay;
import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
@@ -37,11 +36,6 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.annotation.MainThread;
-import android.app.ICompatCameraControlCallback;
import android.app.RemoteAction;
import android.content.BroadcastReceiver;
import android.content.ClipData;
@@ -52,14 +46,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
-import android.content.res.Configuration;
-import android.content.res.Resources;
import android.graphics.Bitmap;
-import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.graphics.drawable.Icon;
import android.hardware.display.DisplayManager;
import android.hardware.input.InputManager;
import android.net.Uri;
@@ -67,57 +54,37 @@
import android.os.Looper;
import android.provider.DeviceConfig;
import android.text.TextUtils;
-import android.util.DisplayMetrics;
import android.util.Log;
-import android.util.MathUtils;
import android.util.Size;
-import android.util.TypedValue;
import android.view.Display;
-import android.view.DisplayCutout;
-import android.view.Gravity;
import android.view.InputEvent;
import android.view.InputEventReceiver;
import android.view.InputMonitor;
-import android.view.LayoutInflater;
import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewRootImpl;
-import android.view.ViewTreeObserver;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-import android.view.animation.LinearInterpolator;
-import android.view.animation.PathInterpolator;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextLinks;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
import androidx.annotation.NonNull;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import com.android.internal.logging.UiEventLogger;
-import com.android.internal.policy.PhoneWindow;
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.broadcast.BroadcastSender;
-import com.android.systemui.screenshot.DraggableConstraintLayout;
-import com.android.systemui.screenshot.FloatingWindowUtil;
-import com.android.systemui.screenshot.OverlayActionChip;
+import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext;
import com.android.systemui.screenshot.TimeoutHandler;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Optional;
+
+import javax.inject.Inject;
/**
* Controls state and UI for the overlay that appears when something is added to the clipboard
*/
-public class ClipboardOverlayController {
+public class ClipboardOverlayController implements ClipboardListener.ClipboardOverlay {
private static final String TAG = "ClipboardOverlayCtrlr";
/** Constants for screenshot/copy deconflicting */
@@ -126,36 +93,22 @@
public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY";
private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000;
- private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
- private static final int FONT_SEARCH_STEP_PX = 4;
private final Context mContext;
private final ClipboardLogger mClipboardLogger;
private final BroadcastDispatcher mBroadcastDispatcher;
private final DisplayManager mDisplayManager;
- private final DisplayMetrics mDisplayMetrics;
- private final WindowManager mWindowManager;
- private final WindowManager.LayoutParams mWindowLayoutParams;
- private final PhoneWindow mWindow;
+ private final ClipboardOverlayWindow mWindow;
private final TimeoutHandler mTimeoutHandler;
- private final AccessibilityManager mAccessibilityManager;
private final TextClassifier mTextClassifier;
- private final DraggableConstraintLayout mView;
- private final View mClipboardPreview;
- private final ImageView mImagePreview;
- private final TextView mTextPreview;
- private final TextView mHiddenPreview;
- private final View mPreviewBorder;
- private final OverlayActionChip mEditChip;
- private final OverlayActionChip mShareChip;
- private final OverlayActionChip mRemoteCopyChip;
- private final View mActionContainerBackground;
- private final View mDismissButton;
- private final LinearLayout mActionContainer;
- private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>();
+ private final ClipboardOverlayView mView;
private Runnable mOnSessionCompleteListener;
+ private Runnable mOnRemoteCopyTapped;
+ private Runnable mOnShareTapped;
+ private Runnable mOnEditTapped;
+ private Runnable mOnPreviewTapped;
private InputMonitor mInputMonitor;
private InputEventReceiver mInputEventReceiver;
@@ -163,14 +116,66 @@
private BroadcastReceiver mCloseDialogsReceiver;
private BroadcastReceiver mScreenshotReceiver;
- private boolean mBlockAttach = false;
private Animator mExitAnimator;
private Animator mEnterAnimator;
- private final int mOrientation;
- private boolean mKeyboardVisible;
+ private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks =
+ new ClipboardOverlayView.ClipboardOverlayCallbacks() {
+ @Override
+ public void onInteraction() {
+ mTimeoutHandler.resetTimeout();
+ }
- public ClipboardOverlayController(Context context,
+ @Override
+ public void onSwipeDismissInitiated(Animator animator) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
+ mExitAnimator = animator;
+ }
+
+ @Override
+ public void onDismissComplete() {
+ hideImmediate();
+ }
+
+ @Override
+ public void onPreviewTapped() {
+ if (mOnPreviewTapped != null) {
+ mOnPreviewTapped.run();
+ }
+ }
+
+ @Override
+ public void onShareButtonTapped() {
+ if (mOnShareTapped != null) {
+ mOnShareTapped.run();
+ }
+ }
+
+ @Override
+ public void onEditButtonTapped() {
+ if (mOnEditTapped != null) {
+ mOnEditTapped.run();
+ }
+ }
+
+ @Override
+ public void onRemoteCopyButtonTapped() {
+ if (mOnRemoteCopyTapped != null) {
+ mOnRemoteCopyTapped.run();
+ }
+ }
+
+ @Override
+ public void onDismissButtonTapped() {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
+ animateOut();
+ }
+ };
+
+ @Inject
+ public ClipboardOverlayController(@OverlayWindowContext Context context,
+ ClipboardOverlayView clipboardOverlayView,
+ ClipboardOverlayWindow clipboardOverlayWindow,
BroadcastDispatcher broadcastDispatcher,
BroadcastSender broadcastSender,
TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) {
@@ -181,121 +186,26 @@
mClipboardLogger = new ClipboardLogger(uiEventLogger);
- mAccessibilityManager = AccessibilityManager.getInstance(mContext);
+ mView = clipboardOverlayView;
+ mWindow = clipboardOverlayWindow;
+ mWindow.init(mView::setInsets, () -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
+ hideImmediate();
+ });
+
mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class))
.getTextClassifier();
- mWindowManager = mContext.getSystemService(WindowManager.class);
-
- mDisplayMetrics = new DisplayMetrics();
- mContext.getDisplay().getRealMetrics(mDisplayMetrics);
-
mTimeoutHandler = timeoutHandler;
mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
- // Setup the window that we are going to use
- mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
- mWindowLayoutParams.setTitle("ClipboardOverlay");
+ mView.setCallbacks(mClipboardCallbacks);
- mWindow = FloatingWindowUtil.getFloatingWindow(mContext);
- mWindow.setWindowManager(mWindowManager, null, null);
- setWindowFocusable(false);
-
- mView = (DraggableConstraintLayout)
- LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay, null);
- mActionContainerBackground =
- requireNonNull(mView.findViewById(R.id.actions_container_background));
- mActionContainer = requireNonNull(mView.findViewById(R.id.actions));
- mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview));
- mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview));
- mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview));
- mHiddenPreview = requireNonNull(mView.findViewById(R.id.hidden_preview));
- mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border));
- mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip));
- mShareChip = requireNonNull(mView.findViewById(R.id.share_chip));
- mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip));
- mEditChip.setAlpha(1);
- mShareChip.setAlpha(1);
- mRemoteCopyChip.setAlpha(1);
- mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button));
-
- mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share));
- mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() {
- @Override
- public void onInteraction() {
- mTimeoutHandler.resetTimeout();
- }
-
- @Override
- public void onSwipeDismissInitiated(Animator animator) {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
- mExitAnimator = animator;
- }
-
- @Override
- public void onDismissComplete() {
- hideImmediate();
- }
- });
-
- mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
- int availableHeight = mTextPreview.getHeight()
- - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom());
- mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight());
- return true;
- });
-
- mDismissButton.setOnClickListener(view -> {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
- animateOut();
- });
-
- mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
- mRemoteCopyChip.setIcon(
- Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true);
- mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
- mOrientation = mContext.getResources().getConfiguration().orientation;
-
- attachWindow();
- withWindowAttached(() -> {
+ mWindow.withWindowAttached(() -> {
mWindow.setContentView(mView);
- WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
- mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime());
- updateInsets(insets);
- mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(
- new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- WindowInsets insets =
- mWindowManager.getCurrentWindowMetrics().getWindowInsets();
- boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime());
- if (keyboardVisible != mKeyboardVisible) {
- mKeyboardVisible = keyboardVisible;
- updateInsets(insets);
- }
- }
- });
- mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
- new ViewRootImpl.ActivityConfigCallback() {
- @Override
- public void onConfigurationChanged(Configuration overrideConfig,
- int newDisplayId) {
- if (mContext.getResources().getConfiguration().orientation
- != mOrientation) {
- mClipboardLogger.logSessionComplete(
- CLIPBOARD_OVERLAY_DISMISSED_OTHER);
- hideImmediate();
- }
- }
-
- @Override
- public void requestCompatCameraControl(
- boolean showControl, boolean transformationApplied,
- ICompatCameraControlCallback callback) {
- Log.w(TAG, "unexpected requestCompatCameraControl call");
- }
- });
+ mView.setInsets(mWindow.getWindowInsets(),
+ mContext.getResources().getConfiguration().orientation);
});
mTimeoutHandler.setOnTimeoutRunnable(() -> {
@@ -336,21 +246,19 @@
broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
}
- void setClipData(ClipData clipData, String clipSource) {
+ @Override // ClipboardListener.ClipboardOverlay
+ public void setClipData(ClipData clipData, String clipSource) {
if (mExitAnimator != null && mExitAnimator.isRunning()) {
mExitAnimator.cancel();
}
reset();
- String accessibilityAnnouncement;
+ String accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null
&& clipData.getDescription().getExtras()
.getBoolean(ClipDescription.EXTRA_IS_SENSITIVE);
if (clipData == null || clipData.getItemCount() == 0) {
- showTextPreview(
- mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
- mTextPreview);
- accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
+ mView.showDefaultTextPreview();
} else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
ClipData.Item item = clipData.getItemAt(0);
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
@@ -360,53 +268,47 @@
}
}
if (isSensitive) {
- showEditableText(
- mContext.getResources().getString(R.string.clipboard_asterisks), true);
+ showEditableText(mContext.getString(R.string.clipboard_asterisks), true);
} else {
showEditableText(item.getText(), false);
}
- showShareChip(clipData);
+ mOnShareTapped = () -> shareContent(clipData);
+ mView.showShareChip();
accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied);
} else if (clipData.getItemAt(0).getUri() != null) {
if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) {
- showShareChip(clipData);
+ mOnShareTapped = () -> shareContent(clipData);
+ mView.showShareChip();
accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied);
- } else {
- accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
}
} else {
- showTextPreview(
- mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
- mTextPreview);
- accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
+ mView.showDefaultTextPreview();
}
+ maybeShowRemoteCopy(clipData);
+ animateIn();
+ mView.announceForAccessibility(accessibilityAnnouncement);
+ mTimeoutHandler.resetTimeout();
+ }
+
+ private void maybeShowRemoteCopy(ClipData clipData) {
Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext);
// Only show remote copy if it's available.
PackageManager packageManager = mContext.getPackageManager();
if (packageManager.resolveActivity(
remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) {
- mRemoteCopyChip.setContentDescription(
- mContext.getString(R.string.clipboard_send_nearby_description));
- mRemoteCopyChip.setVisibility(View.VISIBLE);
- mRemoteCopyChip.setOnClickListener((v) -> {
+ mView.setRemoteCopyVisibility(true);
+ mOnRemoteCopyTapped = () -> {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED);
mContext.startActivity(remoteCopyIntent);
animateOut();
- });
- mActionContainerBackground.setVisibility(View.VISIBLE);
+ };
} else {
- mRemoteCopyChip.setVisibility(View.GONE);
+ mView.setRemoteCopyVisibility(false);
}
- withWindowAttached(() -> {
- if (mEnterAnimator == null || !mEnterAnimator.isRunning()) {
- mView.post(this::animateIn);
- }
- mView.announceForAccessibility(accessibilityAnnouncement);
- });
- mTimeoutHandler.resetTimeout();
}
- void setOnSessionCompleteListener(Runnable runnable) {
+ @Override // ClipboardListener.ClipboardOverlay
+ public void setOnSessionCompleteListener(Runnable runnable) {
mOnSessionCompleteListener = runnable;
}
@@ -418,72 +320,29 @@
actions.addAll(classification.getActions());
}
mView.post(() -> {
- resetActionChips();
- if (actions.size() > 0) {
- mActionContainerBackground.setVisibility(View.VISIBLE);
- for (RemoteAction action : actions) {
- Intent targetIntent = action.getActionIntent().getIntent();
- ComponentName component = targetIntent.getComponent();
- if (component != null && !TextUtils.equals(source,
- component.getPackageName())) {
- OverlayActionChip chip = constructActionChip(action);
- mActionContainer.addView(chip);
- mActionChips.add(chip);
- break; // only show at most one action chip
- }
- }
- }
+ Optional<RemoteAction> action = actions.stream().filter(remoteAction -> {
+ ComponentName component = remoteAction.getActionIntent().getIntent().getComponent();
+ return component != null && !TextUtils.equals(source, component.getPackageName());
+ }).findFirst();
+ mView.resetActionChips();
+ action.ifPresent(remoteAction -> mView.setActionChip(remoteAction, () -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
+ animateOut();
+ }));
});
}
- private void showShareChip(ClipData clip) {
- mShareChip.setVisibility(View.VISIBLE);
- mActionContainerBackground.setVisibility(View.VISIBLE);
- mShareChip.setOnClickListener((v) -> shareContent(clip));
- }
-
- private OverlayActionChip constructActionChip(RemoteAction action) {
- OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate(
- R.layout.overlay_action_chip, mActionContainer, false);
- chip.setText(action.getTitle());
- chip.setContentDescription(action.getTitle());
- chip.setIcon(action.getIcon(), false);
- chip.setPendingIntent(action.getActionIntent(), () -> {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
- animateOut();
- });
- chip.setAlpha(1);
- return chip;
- }
-
private void monitorOutsideTouches() {
InputManager inputManager = mContext.getSystemService(InputManager.class);
mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0);
- mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(),
- Looper.getMainLooper()) {
+ mInputEventReceiver = new InputEventReceiver(
+ mInputMonitor.getInputChannel(), Looper.getMainLooper()) {
@Override
public void onInputEvent(InputEvent event) {
if (event instanceof MotionEvent) {
MotionEvent motionEvent = (MotionEvent) event;
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
- Region touchRegion = new Region();
-
- final Rect tmpRect = new Rect();
- mPreviewBorder.getBoundsOnScreen(tmpRect);
- tmpRect.inset(
- (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
- (int) FloatingWindowUtil.dpToPx(mDisplayMetrics,
- -SWIPE_PADDING_DP));
- touchRegion.op(tmpRect, Region.Op.UNION);
- mActionContainerBackground.getBoundsOnScreen(tmpRect);
- tmpRect.inset(
- (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
- (int) FloatingWindowUtil.dpToPx(mDisplayMetrics,
- -SWIPE_PADDING_DP));
- touchRegion.op(tmpRect, Region.Op.UNION);
- mDismissButton.getBoundsOnScreen(tmpRect);
- touchRegion.op(tmpRect, Region.Op.UNION);
- if (!touchRegion.contains(
+ if (!mView.isInTouchRegion(
(int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
animateOut();
@@ -513,95 +372,27 @@
animateOut();
}
- private void showSinglePreview(View v) {
- mTextPreview.setVisibility(View.GONE);
- mImagePreview.setVisibility(View.GONE);
- mHiddenPreview.setVisibility(View.GONE);
- v.setVisibility(View.VISIBLE);
- }
-
- private void showTextPreview(CharSequence text, TextView textView) {
- showSinglePreview(textView);
- final CharSequence truncatedText = text.subSequence(0, Math.min(500, text.length()));
- textView.setText(truncatedText);
- updateTextSize(truncatedText, textView);
-
- textView.addOnLayoutChangeListener(
- (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
- if (right - left != oldRight - oldLeft) {
- updateTextSize(truncatedText, textView);
- }
- });
- mEditChip.setVisibility(View.GONE);
- }
-
- private void updateTextSize(CharSequence text, TextView textView) {
- Paint paint = new Paint(textView.getPaint());
- Resources res = textView.getResources();
- float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font);
- float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font);
- if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) {
- // If the text is a single word and would fit within the TextView at the min font size,
- // find the biggest font size that will fit.
- float fontSizePx = minFontSize;
- while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize
- && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) {
- fontSizePx += FONT_SEARCH_STEP_PX;
- }
- // Need to turn off autosizing, otherwise setTextSize is a no-op.
- textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
- // It's possible to hit the max font size and not fill the width, so centering
- // horizontally looks better in this case.
- textView.setGravity(Gravity.CENTER);
- textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx);
- } else {
- // Otherwise just stick with autosize.
- textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize,
- (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX);
- textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
- }
- }
-
- private static boolean fitsInView(CharSequence text, TextView textView, Paint paint,
- float fontSizePx) {
- paint.setTextSize(fontSizePx);
- float size = paint.measureText(text.toString());
- float availableWidth = textView.getWidth() - textView.getPaddingLeft()
- - textView.getPaddingRight();
- return size < availableWidth;
- }
-
- private static boolean isOneWord(CharSequence text) {
- return text.toString().split("\\s+", 2).length == 1;
- }
-
private void showEditableText(CharSequence text, boolean hidden) {
- TextView textView = hidden ? mHiddenPreview : mTextPreview;
- showTextPreview(text, textView);
- View.OnClickListener listener = v -> editText();
- setAccessibilityActionToEdit(textView);
+ mView.showTextPreview(text, hidden);
+ mView.setEditAccessibilityAction(true);
+ mOnPreviewTapped = this::editText;
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
- mEditChip.setVisibility(View.VISIBLE);
- mActionContainerBackground.setVisibility(View.VISIBLE);
- mEditChip.setContentDescription(
- mContext.getString(R.string.clipboard_edit_text_description));
- mEditChip.setOnClickListener(listener);
+ mOnEditTapped = this::editText;
+ mView.showEditChip(mContext.getString(R.string.clipboard_edit_text_description));
}
- textView.setOnClickListener(listener);
}
private boolean tryShowEditableImage(Uri uri, boolean isSensitive) {
- View.OnClickListener listener = v -> editImage(uri);
+ Runnable listener = () -> editImage(uri);
ContentResolver resolver = mContext.getContentResolver();
String mimeType = resolver.getType(uri);
boolean isEditableImage = mimeType != null && mimeType.startsWith("image");
if (isSensitive) {
- mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden));
- showSinglePreview(mHiddenPreview);
+ mView.showImagePreview(null);
if (isEditableImage) {
- mHiddenPreview.setOnClickListener(listener);
- setAccessibilityActionToEdit(mHiddenPreview);
+ mOnPreviewTapped = listener;
+ mView.setEditAccessibilityAction(true);
}
} else if (isEditableImage) { // if the MIMEtype is image, try to load
try {
@@ -609,44 +400,36 @@
// The width of the view is capped, height maintains aspect ratio, so allow it to be
// taller if needed.
Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null);
- showSinglePreview(mImagePreview);
- mImagePreview.setImageBitmap(thumbnail);
- mImagePreview.setOnClickListener(listener);
- setAccessibilityActionToEdit(mImagePreview);
+ mView.showImagePreview(thumbnail);
+ mView.setEditAccessibilityAction(true);
+ mOnPreviewTapped = listener;
} catch (IOException e) {
Log.e(TAG, "Thumbnail loading failed", e);
- showTextPreview(
- mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
- mTextPreview);
+ mView.showDefaultTextPreview();
isEditableImage = false;
}
} else {
- showTextPreview(
- mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
- mTextPreview);
+ mView.showDefaultTextPreview();
}
if (isEditableImage && DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
- mEditChip.setVisibility(View.VISIBLE);
- mActionContainerBackground.setVisibility(View.VISIBLE);
- mEditChip.setOnClickListener(listener);
- mEditChip.setContentDescription(
- mContext.getString(R.string.clipboard_edit_image_description));
+ mView.showEditChip(mContext.getString(R.string.clipboard_edit_image_description));
}
return isEditableImage;
}
- private void setAccessibilityActionToEdit(View view) {
- ViewCompat.replaceAccessibilityAction(view,
- AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
- mContext.getString(R.string.clipboard_edit), null);
- }
-
private void animateIn() {
- if (mAccessibilityManager.isEnabled()) {
- mDismissButton.setVisibility(View.VISIBLE);
+ if (mEnterAnimator != null && mEnterAnimator.isRunning()) {
+ return;
}
- mEnterAnimator = getEnterAnimation();
+ mEnterAnimator = mView.getEnterAnimation();
+ mEnterAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mTimeoutHandler.resetTimeout();
+ }
+ });
mEnterAnimator.start();
}
@@ -654,7 +437,7 @@
if (mExitAnimator != null && mExitAnimator.isRunning()) {
return;
}
- Animator anim = getExitAnimation();
+ Animator anim = mView.getExitAnimation();
anim.addListener(new AnimatorListenerAdapter() {
private boolean mCancelled;
@@ -676,122 +459,11 @@
anim.start();
}
- private Animator getEnterAnimation() {
- TimeInterpolator linearInterpolator = new LinearInterpolator();
- TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f);
- AnimatorSet enterAnim = new AnimatorSet();
-
- ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
- rootAnim.setInterpolator(linearInterpolator);
- rootAnim.setDuration(66);
- rootAnim.addUpdateListener(animation -> {
- mView.setAlpha(animation.getAnimatedFraction());
- });
-
- ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
- scaleAnim.setInterpolator(scaleInterpolator);
- scaleAnim.setDuration(333);
- scaleAnim.addUpdateListener(animation -> {
- float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
- mClipboardPreview.setScaleX(previewScale);
- mClipboardPreview.setScaleY(previewScale);
- mPreviewBorder.setScaleX(previewScale);
- mPreviewBorder.setScaleY(previewScale);
-
- float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
- mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
- mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
- float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction());
- float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
- mActionContainer.setScaleX(actionsScaleX);
- mActionContainer.setScaleY(actionsScaleY);
- mActionContainerBackground.setScaleX(actionsScaleX);
- mActionContainerBackground.setScaleY(actionsScaleY);
- });
-
- ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
- alphaAnim.setInterpolator(linearInterpolator);
- alphaAnim.setDuration(283);
- alphaAnim.addUpdateListener(animation -> {
- float alpha = animation.getAnimatedFraction();
- mClipboardPreview.setAlpha(alpha);
- mPreviewBorder.setAlpha(alpha);
- mDismissButton.setAlpha(alpha);
- mActionContainer.setAlpha(alpha);
- });
-
- mActionContainer.setAlpha(0);
- mPreviewBorder.setAlpha(0);
- mClipboardPreview.setAlpha(0);
- enterAnim.play(rootAnim).with(scaleAnim);
- enterAnim.play(alphaAnim).after(50).after(rootAnim);
-
- enterAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- super.onAnimationEnd(animation);
- mView.setAlpha(1);
- mTimeoutHandler.resetTimeout();
- }
- });
- return enterAnim;
- }
-
- private Animator getExitAnimation() {
- TimeInterpolator linearInterpolator = new LinearInterpolator();
- TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f);
- AnimatorSet exitAnim = new AnimatorSet();
-
- ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
- rootAnim.setInterpolator(linearInterpolator);
- rootAnim.setDuration(100);
- rootAnim.addUpdateListener(anim -> mView.setAlpha(1 - anim.getAnimatedFraction()));
-
- ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
- scaleAnim.setInterpolator(scaleInterpolator);
- scaleAnim.setDuration(250);
- scaleAnim.addUpdateListener(animation -> {
- float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
- mClipboardPreview.setScaleX(previewScale);
- mClipboardPreview.setScaleY(previewScale);
- mPreviewBorder.setScaleX(previewScale);
- mPreviewBorder.setScaleY(previewScale);
-
- float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
- mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
- mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
- float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction());
- float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
- mActionContainer.setScaleX(actionScaleX);
- mActionContainer.setScaleY(actionScaleY);
- mActionContainerBackground.setScaleX(actionScaleX);
- mActionContainerBackground.setScaleY(actionScaleY);
- });
-
- ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
- alphaAnim.setInterpolator(linearInterpolator);
- alphaAnim.setDuration(166);
- alphaAnim.addUpdateListener(animation -> {
- float alpha = 1 - animation.getAnimatedFraction();
- mClipboardPreview.setAlpha(alpha);
- mPreviewBorder.setAlpha(alpha);
- mDismissButton.setAlpha(alpha);
- mActionContainer.setAlpha(alpha);
- });
-
- exitAnim.play(alphaAnim).with(scaleAnim);
- exitAnim.play(rootAnim).after(150).after(alphaAnim);
- return exitAnim;
- }
-
- private void hideImmediate() {
+ void hideImmediate() {
// Note this may be called multiple times if multiple dismissal events happen at the same
// time.
mTimeoutHandler.cancelTimeout();
- final View decorView = mWindow.peekDecorView();
- if (decorView != null && decorView.isAttachedToWindow()) {
- mWindowManager.removeViewImmediate(decorView);
- }
+ mWindow.remove();
if (mCloseDialogsReceiver != null) {
mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver);
mCloseDialogsReceiver = null;
@@ -813,129 +485,20 @@
}
}
- private void resetActionChips() {
- for (OverlayActionChip chip : mActionChips) {
- mActionContainer.removeView(chip);
- }
- mActionChips.clear();
- }
-
private void reset() {
- mView.setTranslationX(0);
- mView.setAlpha(0);
- mActionContainerBackground.setVisibility(View.GONE);
- mShareChip.setVisibility(View.GONE);
- mEditChip.setVisibility(View.GONE);
- mRemoteCopyChip.setVisibility(View.GONE);
- resetActionChips();
+ mOnRemoteCopyTapped = null;
+ mOnShareTapped = null;
+ mOnEditTapped = null;
+ mOnPreviewTapped = null;
+ mView.reset();
mTimeoutHandler.cancelTimeout();
mClipboardLogger.reset();
}
- @MainThread
- private void attachWindow() {
- View decorView = mWindow.getDecorView();
- if (decorView.isAttachedToWindow() || mBlockAttach) {
- return;
- }
- mBlockAttach = true;
- mWindowManager.addView(decorView, mWindowLayoutParams);
- decorView.requestApplyInsets();
- mView.requestApplyInsets();
- decorView.getViewTreeObserver().addOnWindowAttachListener(
- new ViewTreeObserver.OnWindowAttachListener() {
- @Override
- public void onWindowAttached() {
- mBlockAttach = false;
- }
-
- @Override
- public void onWindowDetached() {
- }
- }
- );
- }
-
- private void withWindowAttached(Runnable action) {
- View decorView = mWindow.getDecorView();
- if (decorView.isAttachedToWindow()) {
- action.run();
- } else {
- decorView.getViewTreeObserver().addOnWindowAttachListener(
- new ViewTreeObserver.OnWindowAttachListener() {
- @Override
- public void onWindowAttached() {
- mBlockAttach = false;
- decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
- action.run();
- }
-
- @Override
- public void onWindowDetached() {
- }
- });
- }
- }
-
- private void updateInsets(WindowInsets insets) {
- int orientation = mContext.getResources().getConfiguration().orientation;
- FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams();
- if (p == null) {
- return;
- }
- DisplayCutout cutout = insets.getDisplayCutout();
- Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
- Insets imeInsets = insets.getInsets(WindowInsets.Type.ime());
- if (cutout == null) {
- p.setMargins(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom));
- } else {
- Insets waterfall = cutout.getWaterfallInsets();
- if (orientation == ORIENTATION_PORTRAIT) {
- p.setMargins(
- waterfall.left,
- Math.max(cutout.getSafeInsetTop(), waterfall.top),
- waterfall.right,
- Math.max(imeInsets.bottom,
- Math.max(cutout.getSafeInsetBottom(),
- Math.max(navBarInsets.bottom, waterfall.bottom))));
- } else {
- p.setMargins(
- waterfall.left,
- waterfall.top,
- waterfall.right,
- Math.max(imeInsets.bottom,
- Math.max(navBarInsets.bottom, waterfall.bottom)));
- }
- }
- mView.setLayoutParams(p);
- mView.requestLayout();
- }
-
private Display getDefaultDisplay() {
return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
}
- /**
- * Updates the window focusability. If the window is already showing, then it updates the
- * window immediately, otherwise the layout params will be applied when the window is next
- * shown.
- */
- private void setWindowFocusable(boolean focusable) {
- int flags = mWindowLayoutParams.flags;
- if (focusable) {
- mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
- } else {
- mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
- }
- if (mWindowLayoutParams.flags == flags) {
- return;
- }
- final View decorView = mWindow.peekDecorView();
- if (decorView != null && decorView.isAttachedToWindow()) {
- mWindowManager.updateViewLayout(decorView, mWindowLayoutParams);
- }
- }
-
static class ClipboardLogger {
private final UiEventLogger mUiEventLogger;
private boolean mGuarded = false;
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java
new file mode 100644
index 0000000..3a040829
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java
@@ -0,0 +1,963 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.clipboardoverlay;
+
+import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
+
+import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS;
+import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISSED_OTHER;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EDIT_TAPPED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
+
+import static java.util.Objects.requireNonNull;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.MainThread;
+import android.app.ICompatCameraControlCallback;
+import android.app.RemoteAction;
+import android.content.BroadcastReceiver;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.Icon;
+import android.hardware.display.DisplayManager;
+import android.hardware.input.InputManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Looper;
+import android.provider.DeviceConfig;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.Size;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.DisplayCutout;
+import android.view.Gravity;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.InputMonitor;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.PathInterpolator;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassificationManager;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.policy.PhoneWindow;
+import com.android.systemui.R;
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.screenshot.DraggableConstraintLayout;
+import com.android.systemui.screenshot.FloatingWindowUtil;
+import com.android.systemui.screenshot.OverlayActionChip;
+import com.android.systemui.screenshot.TimeoutHandler;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Controls state and UI for the overlay that appears when something is added to the clipboard
+ */
+public class ClipboardOverlayControllerLegacy implements ClipboardListener.ClipboardOverlay {
+ private static final String TAG = "ClipboardOverlayCtrlr";
+ private static final String REMOTE_COPY_ACTION = "android.intent.action.REMOTE_COPY";
+
+ /** Constants for screenshot/copy deconflicting */
+ public static final String SCREENSHOT_ACTION = "com.android.systemui.SCREENSHOT";
+ public static final String SELF_PERMISSION = "com.android.systemui.permission.SELF";
+ public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY";
+
+ private static final String EXTRA_EDIT_SOURCE_CLIPBOARD = "edit_source_clipboard";
+
+ private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000;
+ private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
+ private static final int FONT_SEARCH_STEP_PX = 4;
+
+ private final Context mContext;
+ private final ClipboardLogger mClipboardLogger;
+ private final BroadcastDispatcher mBroadcastDispatcher;
+ private final DisplayManager mDisplayManager;
+ private final DisplayMetrics mDisplayMetrics;
+ private final WindowManager mWindowManager;
+ private final WindowManager.LayoutParams mWindowLayoutParams;
+ private final PhoneWindow mWindow;
+ private final TimeoutHandler mTimeoutHandler;
+ private final AccessibilityManager mAccessibilityManager;
+ private final TextClassifier mTextClassifier;
+
+ private final DraggableConstraintLayout mView;
+ private final View mClipboardPreview;
+ private final ImageView mImagePreview;
+ private final TextView mTextPreview;
+ private final TextView mHiddenPreview;
+ private final View mPreviewBorder;
+ private final OverlayActionChip mEditChip;
+ private final OverlayActionChip mShareChip;
+ private final OverlayActionChip mRemoteCopyChip;
+ private final View mActionContainerBackground;
+ private final View mDismissButton;
+ private final LinearLayout mActionContainer;
+ private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>();
+
+ private Runnable mOnSessionCompleteListener;
+
+ private InputMonitor mInputMonitor;
+ private InputEventReceiver mInputEventReceiver;
+
+ private BroadcastReceiver mCloseDialogsReceiver;
+ private BroadcastReceiver mScreenshotReceiver;
+
+ private boolean mBlockAttach = false;
+ private Animator mExitAnimator;
+ private Animator mEnterAnimator;
+ private final int mOrientation;
+ private boolean mKeyboardVisible;
+
+
+ public ClipboardOverlayControllerLegacy(Context context,
+ BroadcastDispatcher broadcastDispatcher,
+ BroadcastSender broadcastSender,
+ TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) {
+ mBroadcastDispatcher = broadcastDispatcher;
+ mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
+ final Context displayContext = context.createDisplayContext(getDefaultDisplay());
+ mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null);
+
+ mClipboardLogger = new ClipboardLogger(uiEventLogger);
+
+ mAccessibilityManager = AccessibilityManager.getInstance(mContext);
+ mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class))
+ .getTextClassifier();
+
+ mWindowManager = mContext.getSystemService(WindowManager.class);
+
+ mDisplayMetrics = new DisplayMetrics();
+ mContext.getDisplay().getRealMetrics(mDisplayMetrics);
+
+ mTimeoutHandler = timeoutHandler;
+ mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
+
+ // Setup the window that we are going to use
+ mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
+ mWindowLayoutParams.setTitle("ClipboardOverlay");
+
+ mWindow = FloatingWindowUtil.getFloatingWindow(mContext);
+ mWindow.setWindowManager(mWindowManager, null, null);
+
+ setWindowFocusable(false);
+
+ mView = (DraggableConstraintLayout)
+ LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay_legacy, null);
+ mActionContainerBackground =
+ requireNonNull(mView.findViewById(R.id.actions_container_background));
+ mActionContainer = requireNonNull(mView.findViewById(R.id.actions));
+ mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview));
+ mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview));
+ mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview));
+ mHiddenPreview = requireNonNull(mView.findViewById(R.id.hidden_preview));
+ mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border));
+ mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip));
+ mShareChip = requireNonNull(mView.findViewById(R.id.share_chip));
+ mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip));
+ mEditChip.setAlpha(1);
+ mShareChip.setAlpha(1);
+ mRemoteCopyChip.setAlpha(1);
+ mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button));
+
+ mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share));
+ mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() {
+ @Override
+ public void onInteraction() {
+ mTimeoutHandler.resetTimeout();
+ }
+
+ @Override
+ public void onSwipeDismissInitiated(Animator animator) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
+ mExitAnimator = animator;
+ }
+
+ @Override
+ public void onDismissComplete() {
+ hideImmediate();
+ }
+ });
+
+ mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
+ int availableHeight = mTextPreview.getHeight()
+ - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom());
+ mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight());
+ return true;
+ });
+
+ mDismissButton.setOnClickListener(view -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
+ animateOut();
+ });
+
+ mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
+ mRemoteCopyChip.setIcon(
+ Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true);
+ mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
+ mOrientation = mContext.getResources().getConfiguration().orientation;
+
+ attachWindow();
+ withWindowAttached(() -> {
+ mWindow.setContentView(mView);
+ WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+ mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime());
+ updateInsets(insets);
+ mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ WindowInsets insets =
+ mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+ boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime());
+ if (keyboardVisible != mKeyboardVisible) {
+ mKeyboardVisible = keyboardVisible;
+ updateInsets(insets);
+ }
+ }
+ });
+ mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
+ new ViewRootImpl.ActivityConfigCallback() {
+ @Override
+ public void onConfigurationChanged(Configuration overrideConfig,
+ int newDisplayId) {
+ if (mContext.getResources().getConfiguration().orientation
+ != mOrientation) {
+ mClipboardLogger.logSessionComplete(
+ CLIPBOARD_OVERLAY_DISMISSED_OTHER);
+ hideImmediate();
+ }
+ }
+
+ @Override
+ public void requestCompatCameraControl(
+ boolean showControl, boolean transformationApplied,
+ ICompatCameraControlCallback callback) {
+ Log.w(TAG, "unexpected requestCompatCameraControl call");
+ }
+ });
+ });
+
+ mTimeoutHandler.setOnTimeoutRunnable(() -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TIMED_OUT);
+ animateOut();
+ });
+
+ mCloseDialogsReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
+ animateOut();
+ }
+ }
+ };
+
+ mBroadcastDispatcher.registerReceiver(mCloseDialogsReceiver,
+ new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS));
+ mScreenshotReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (SCREENSHOT_ACTION.equals(intent.getAction())) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
+ animateOut();
+ }
+ }
+ };
+
+ mBroadcastDispatcher.registerReceiver(mScreenshotReceiver,
+ new IntentFilter(SCREENSHOT_ACTION), null, null, Context.RECEIVER_EXPORTED,
+ SELF_PERMISSION);
+ monitorOutsideTouches();
+
+ Intent copyIntent = new Intent(COPY_OVERLAY_ACTION);
+ // Set package name so the system knows it's safe
+ copyIntent.setPackage(mContext.getPackageName());
+ broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
+ }
+
+ @Override // ClipboardListener.ClipboardOverlay
+ public void setClipData(ClipData clipData, String clipSource) {
+ if (mExitAnimator != null && mExitAnimator.isRunning()) {
+ mExitAnimator.cancel();
+ }
+ reset();
+ String accessibilityAnnouncement;
+
+ boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null
+ && clipData.getDescription().getExtras()
+ .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE);
+ if (clipData == null || clipData.getItemCount() == 0) {
+ showTextPreview(
+ mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
+ mTextPreview);
+ accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
+ } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
+ ClipData.Item item = clipData.getItemAt(0);
+ if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+ CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
+ if (item.getTextLinks() != null) {
+ AsyncTask.execute(() -> classifyText(clipData.getItemAt(0), clipSource));
+ }
+ }
+ if (isSensitive) {
+ showEditableText(
+ mContext.getResources().getString(R.string.clipboard_asterisks), true);
+ } else {
+ showEditableText(item.getText(), false);
+ }
+ showShareChip(clipData);
+ accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied);
+ } else if (clipData.getItemAt(0).getUri() != null) {
+ if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) {
+ showShareChip(clipData);
+ accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied);
+ } else {
+ accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
+ }
+ } else {
+ showTextPreview(
+ mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
+ mTextPreview);
+ accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
+ }
+ Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext);
+ // Only show remote copy if it's available.
+ PackageManager packageManager = mContext.getPackageManager();
+ if (packageManager.resolveActivity(
+ remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) {
+ mRemoteCopyChip.setContentDescription(
+ mContext.getString(R.string.clipboard_send_nearby_description));
+ mRemoteCopyChip.setVisibility(View.VISIBLE);
+ mRemoteCopyChip.setOnClickListener((v) -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED);
+ mContext.startActivity(remoteCopyIntent);
+ animateOut();
+ });
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ } else {
+ mRemoteCopyChip.setVisibility(View.GONE);
+ }
+ withWindowAttached(() -> {
+ if (mEnterAnimator == null || !mEnterAnimator.isRunning()) {
+ mView.post(this::animateIn);
+ }
+ mView.announceForAccessibility(accessibilityAnnouncement);
+ });
+ mTimeoutHandler.resetTimeout();
+ }
+
+ @Override // ClipboardListener.ClipboardOverlay
+ public void setOnSessionCompleteListener(Runnable runnable) {
+ mOnSessionCompleteListener = runnable;
+ }
+
+ private void classifyText(ClipData.Item item, String source) {
+ ArrayList<RemoteAction> actions = new ArrayList<>();
+ for (TextLinks.TextLink link : item.getTextLinks().getLinks()) {
+ TextClassification classification = mTextClassifier.classifyText(
+ item.getText(), link.getStart(), link.getEnd(), null);
+ actions.addAll(classification.getActions());
+ }
+ mView.post(() -> {
+ resetActionChips();
+ if (actions.size() > 0) {
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ for (RemoteAction action : actions) {
+ Intent targetIntent = action.getActionIntent().getIntent();
+ ComponentName component = targetIntent.getComponent();
+ if (component != null && !TextUtils.equals(source,
+ component.getPackageName())) {
+ OverlayActionChip chip = constructActionChip(action);
+ mActionContainer.addView(chip);
+ mActionChips.add(chip);
+ break; // only show at most one action chip
+ }
+ }
+ }
+ });
+ }
+
+ private void showShareChip(ClipData clip) {
+ mShareChip.setVisibility(View.VISIBLE);
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ mShareChip.setOnClickListener((v) -> shareContent(clip));
+ }
+
+ private OverlayActionChip constructActionChip(RemoteAction action) {
+ OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate(
+ R.layout.overlay_action_chip, mActionContainer, false);
+ chip.setText(action.getTitle());
+ chip.setContentDescription(action.getTitle());
+ chip.setIcon(action.getIcon(), false);
+ chip.setPendingIntent(action.getActionIntent(), () -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
+ animateOut();
+ });
+ chip.setAlpha(1);
+ return chip;
+ }
+
+ private void monitorOutsideTouches() {
+ InputManager inputManager = mContext.getSystemService(InputManager.class);
+ mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0);
+ mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(),
+ Looper.getMainLooper()) {
+ @Override
+ public void onInputEvent(InputEvent event) {
+ if (event instanceof MotionEvent) {
+ MotionEvent motionEvent = (MotionEvent) event;
+ if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ Region touchRegion = new Region();
+
+ final Rect tmpRect = new Rect();
+ mPreviewBorder.getBoundsOnScreen(tmpRect);
+ tmpRect.inset(
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics,
+ -SWIPE_PADDING_DP));
+ touchRegion.op(tmpRect, Region.Op.UNION);
+ mActionContainerBackground.getBoundsOnScreen(tmpRect);
+ tmpRect.inset(
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics,
+ -SWIPE_PADDING_DP));
+ touchRegion.op(tmpRect, Region.Op.UNION);
+ mDismissButton.getBoundsOnScreen(tmpRect);
+ touchRegion.op(tmpRect, Region.Op.UNION);
+ if (!touchRegion.contains(
+ (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
+ animateOut();
+ }
+ }
+ }
+ finishInputEvent(event, true /* handled */);
+ }
+ };
+ }
+
+ private void editImage(Uri uri) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
+ mContext.startActivity(IntentCreator.getImageEditIntent(uri, mContext));
+ animateOut();
+ }
+
+ private void editText() {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
+ mContext.startActivity(IntentCreator.getTextEditorIntent(mContext));
+ animateOut();
+ }
+
+ private void shareContent(ClipData clip) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SHARE_TAPPED);
+ mContext.startActivity(IntentCreator.getShareIntent(clip, mContext));
+ animateOut();
+ }
+
+ private void showSinglePreview(View v) {
+ mTextPreview.setVisibility(View.GONE);
+ mImagePreview.setVisibility(View.GONE);
+ mHiddenPreview.setVisibility(View.GONE);
+ v.setVisibility(View.VISIBLE);
+ }
+
+ private void showTextPreview(CharSequence text, TextView textView) {
+ showSinglePreview(textView);
+ final CharSequence truncatedText = text.subSequence(0, Math.min(500, text.length()));
+ textView.setText(truncatedText);
+ updateTextSize(truncatedText, textView);
+
+ textView.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ if (right - left != oldRight - oldLeft) {
+ updateTextSize(truncatedText, textView);
+ }
+ });
+ mEditChip.setVisibility(View.GONE);
+ }
+
+ private void updateTextSize(CharSequence text, TextView textView) {
+ Paint paint = new Paint(textView.getPaint());
+ Resources res = textView.getResources();
+ float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font);
+ float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font);
+ if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) {
+ // If the text is a single word and would fit within the TextView at the min font size,
+ // find the biggest font size that will fit.
+ float fontSizePx = minFontSize;
+ while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize
+ && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) {
+ fontSizePx += FONT_SEARCH_STEP_PX;
+ }
+ // Need to turn off autosizing, otherwise setTextSize is a no-op.
+ textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
+ // It's possible to hit the max font size and not fill the width, so centering
+ // horizontally looks better in this case.
+ textView.setGravity(Gravity.CENTER);
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx);
+ } else {
+ // Otherwise just stick with autosize.
+ textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize,
+ (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX);
+ textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
+ }
+ }
+
+ private static boolean fitsInView(CharSequence text, TextView textView, Paint paint,
+ float fontSizePx) {
+ paint.setTextSize(fontSizePx);
+ float size = paint.measureText(text.toString());
+ float availableWidth = textView.getWidth() - textView.getPaddingLeft()
+ - textView.getPaddingRight();
+ return size < availableWidth;
+ }
+
+ private static boolean isOneWord(CharSequence text) {
+ return text.toString().split("\\s+", 2).length == 1;
+ }
+
+ private void showEditableText(CharSequence text, boolean hidden) {
+ TextView textView = hidden ? mHiddenPreview : mTextPreview;
+ showTextPreview(text, textView);
+ View.OnClickListener listener = v -> editText();
+ setAccessibilityActionToEdit(textView);
+ if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+ CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
+ mEditChip.setVisibility(View.VISIBLE);
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ mEditChip.setContentDescription(
+ mContext.getString(R.string.clipboard_edit_text_description));
+ mEditChip.setOnClickListener(listener);
+ }
+ textView.setOnClickListener(listener);
+ }
+
+ private boolean tryShowEditableImage(Uri uri, boolean isSensitive) {
+ View.OnClickListener listener = v -> editImage(uri);
+ ContentResolver resolver = mContext.getContentResolver();
+ String mimeType = resolver.getType(uri);
+ boolean isEditableImage = mimeType != null && mimeType.startsWith("image");
+ if (isSensitive) {
+ mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden));
+ showSinglePreview(mHiddenPreview);
+ if (isEditableImage) {
+ mHiddenPreview.setOnClickListener(listener);
+ setAccessibilityActionToEdit(mHiddenPreview);
+ }
+ } else if (isEditableImage) { // if the MIMEtype is image, try to load
+ try {
+ int size = mContext.getResources().getDimensionPixelSize(R.dimen.overlay_x_scale);
+ // The width of the view is capped, height maintains aspect ratio, so allow it to be
+ // taller if needed.
+ Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null);
+ showSinglePreview(mImagePreview);
+ mImagePreview.setImageBitmap(thumbnail);
+ mImagePreview.setOnClickListener(listener);
+ setAccessibilityActionToEdit(mImagePreview);
+ } catch (IOException e) {
+ Log.e(TAG, "Thumbnail loading failed", e);
+ showTextPreview(
+ mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
+ mTextPreview);
+ isEditableImage = false;
+ }
+ } else {
+ showTextPreview(
+ mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
+ mTextPreview);
+ }
+ if (isEditableImage && DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
+ mEditChip.setVisibility(View.VISIBLE);
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ mEditChip.setOnClickListener(listener);
+ mEditChip.setContentDescription(
+ mContext.getString(R.string.clipboard_edit_image_description));
+ }
+ return isEditableImage;
+ }
+
+ private void setAccessibilityActionToEdit(View view) {
+ ViewCompat.replaceAccessibilityAction(view,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ mContext.getString(R.string.clipboard_edit), null);
+ }
+
+ private void animateIn() {
+ if (mAccessibilityManager.isEnabled()) {
+ mDismissButton.setVisibility(View.VISIBLE);
+ }
+ mEnterAnimator = getEnterAnimation();
+ mEnterAnimator.start();
+ }
+
+ private void animateOut() {
+ if (mExitAnimator != null && mExitAnimator.isRunning()) {
+ return;
+ }
+ Animator anim = getExitAnimation();
+ anim.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (!mCancelled) {
+ hideImmediate();
+ }
+ }
+ });
+ mExitAnimator = anim;
+ anim.start();
+ }
+
+ private Animator getEnterAnimation() {
+ TimeInterpolator linearInterpolator = new LinearInterpolator();
+ TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f);
+ AnimatorSet enterAnim = new AnimatorSet();
+
+ ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
+ rootAnim.setInterpolator(linearInterpolator);
+ rootAnim.setDuration(66);
+ rootAnim.addUpdateListener(animation -> {
+ mView.setAlpha(animation.getAnimatedFraction());
+ });
+
+ ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
+ scaleAnim.setInterpolator(scaleInterpolator);
+ scaleAnim.setDuration(333);
+ scaleAnim.addUpdateListener(animation -> {
+ float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
+ mClipboardPreview.setScaleX(previewScale);
+ mClipboardPreview.setScaleY(previewScale);
+ mPreviewBorder.setScaleX(previewScale);
+ mPreviewBorder.setScaleY(previewScale);
+
+ float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
+ mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
+ mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
+ float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction());
+ float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
+ mActionContainer.setScaleX(actionsScaleX);
+ mActionContainer.setScaleY(actionsScaleY);
+ mActionContainerBackground.setScaleX(actionsScaleX);
+ mActionContainerBackground.setScaleY(actionsScaleY);
+ });
+
+ ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
+ alphaAnim.setInterpolator(linearInterpolator);
+ alphaAnim.setDuration(283);
+ alphaAnim.addUpdateListener(animation -> {
+ float alpha = animation.getAnimatedFraction();
+ mClipboardPreview.setAlpha(alpha);
+ mPreviewBorder.setAlpha(alpha);
+ mDismissButton.setAlpha(alpha);
+ mActionContainer.setAlpha(alpha);
+ });
+
+ mActionContainer.setAlpha(0);
+ mPreviewBorder.setAlpha(0);
+ mClipboardPreview.setAlpha(0);
+ enterAnim.play(rootAnim).with(scaleAnim);
+ enterAnim.play(alphaAnim).after(50).after(rootAnim);
+
+ enterAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mView.setAlpha(1);
+ mTimeoutHandler.resetTimeout();
+ }
+ });
+ return enterAnim;
+ }
+
+ private Animator getExitAnimation() {
+ TimeInterpolator linearInterpolator = new LinearInterpolator();
+ TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f);
+ AnimatorSet exitAnim = new AnimatorSet();
+
+ ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
+ rootAnim.setInterpolator(linearInterpolator);
+ rootAnim.setDuration(100);
+ rootAnim.addUpdateListener(anim -> mView.setAlpha(1 - anim.getAnimatedFraction()));
+
+ ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
+ scaleAnim.setInterpolator(scaleInterpolator);
+ scaleAnim.setDuration(250);
+ scaleAnim.addUpdateListener(animation -> {
+ float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
+ mClipboardPreview.setScaleX(previewScale);
+ mClipboardPreview.setScaleY(previewScale);
+ mPreviewBorder.setScaleX(previewScale);
+ mPreviewBorder.setScaleY(previewScale);
+
+ float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
+ mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
+ mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
+ float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction());
+ float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
+ mActionContainer.setScaleX(actionScaleX);
+ mActionContainer.setScaleY(actionScaleY);
+ mActionContainerBackground.setScaleX(actionScaleX);
+ mActionContainerBackground.setScaleY(actionScaleY);
+ });
+
+ ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
+ alphaAnim.setInterpolator(linearInterpolator);
+ alphaAnim.setDuration(166);
+ alphaAnim.addUpdateListener(animation -> {
+ float alpha = 1 - animation.getAnimatedFraction();
+ mClipboardPreview.setAlpha(alpha);
+ mPreviewBorder.setAlpha(alpha);
+ mDismissButton.setAlpha(alpha);
+ mActionContainer.setAlpha(alpha);
+ });
+
+ exitAnim.play(alphaAnim).with(scaleAnim);
+ exitAnim.play(rootAnim).after(150).after(alphaAnim);
+ return exitAnim;
+ }
+
+ private void hideImmediate() {
+ // Note this may be called multiple times if multiple dismissal events happen at the same
+ // time.
+ mTimeoutHandler.cancelTimeout();
+ final View decorView = mWindow.peekDecorView();
+ if (decorView != null && decorView.isAttachedToWindow()) {
+ mWindowManager.removeViewImmediate(decorView);
+ }
+ if (mCloseDialogsReceiver != null) {
+ mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver);
+ mCloseDialogsReceiver = null;
+ }
+ if (mScreenshotReceiver != null) {
+ mBroadcastDispatcher.unregisterReceiver(mScreenshotReceiver);
+ mScreenshotReceiver = null;
+ }
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+ if (mInputMonitor != null) {
+ mInputMonitor.dispose();
+ mInputMonitor = null;
+ }
+ if (mOnSessionCompleteListener != null) {
+ mOnSessionCompleteListener.run();
+ }
+ }
+
+ private void resetActionChips() {
+ for (OverlayActionChip chip : mActionChips) {
+ mActionContainer.removeView(chip);
+ }
+ mActionChips.clear();
+ }
+
+ private void reset() {
+ mView.setTranslationX(0);
+ mView.setAlpha(0);
+ mActionContainerBackground.setVisibility(View.GONE);
+ mShareChip.setVisibility(View.GONE);
+ mEditChip.setVisibility(View.GONE);
+ mRemoteCopyChip.setVisibility(View.GONE);
+ resetActionChips();
+ mTimeoutHandler.cancelTimeout();
+ mClipboardLogger.reset();
+ }
+
+ @MainThread
+ private void attachWindow() {
+ View decorView = mWindow.getDecorView();
+ if (decorView.isAttachedToWindow() || mBlockAttach) {
+ return;
+ }
+ mBlockAttach = true;
+ mWindowManager.addView(decorView, mWindowLayoutParams);
+ decorView.requestApplyInsets();
+ mView.requestApplyInsets();
+ decorView.getViewTreeObserver().addOnWindowAttachListener(
+ new ViewTreeObserver.OnWindowAttachListener() {
+ @Override
+ public void onWindowAttached() {
+ mBlockAttach = false;
+ }
+
+ @Override
+ public void onWindowDetached() {
+ }
+ }
+ );
+ }
+
+ private void withWindowAttached(Runnable action) {
+ View decorView = mWindow.getDecorView();
+ if (decorView.isAttachedToWindow()) {
+ action.run();
+ } else {
+ decorView.getViewTreeObserver().addOnWindowAttachListener(
+ new ViewTreeObserver.OnWindowAttachListener() {
+ @Override
+ public void onWindowAttached() {
+ mBlockAttach = false;
+ decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
+ action.run();
+ }
+
+ @Override
+ public void onWindowDetached() {
+ }
+ });
+ }
+ }
+
+ private void updateInsets(WindowInsets insets) {
+ int orientation = mContext.getResources().getConfiguration().orientation;
+ FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams();
+ if (p == null) {
+ return;
+ }
+ DisplayCutout cutout = insets.getDisplayCutout();
+ Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
+ Insets imeInsets = insets.getInsets(WindowInsets.Type.ime());
+ if (cutout == null) {
+ p.setMargins(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom));
+ } else {
+ Insets waterfall = cutout.getWaterfallInsets();
+ if (orientation == ORIENTATION_PORTRAIT) {
+ p.setMargins(
+ waterfall.left,
+ Math.max(cutout.getSafeInsetTop(), waterfall.top),
+ waterfall.right,
+ Math.max(imeInsets.bottom,
+ Math.max(cutout.getSafeInsetBottom(),
+ Math.max(navBarInsets.bottom, waterfall.bottom))));
+ } else {
+ p.setMargins(
+ waterfall.left,
+ waterfall.top,
+ waterfall.right,
+ Math.max(imeInsets.bottom,
+ Math.max(navBarInsets.bottom, waterfall.bottom)));
+ }
+ }
+ mView.setLayoutParams(p);
+ mView.requestLayout();
+ }
+
+ private Display getDefaultDisplay() {
+ return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
+ }
+
+ /**
+ * Updates the window focusability. If the window is already showing, then it updates the
+ * window immediately, otherwise the layout params will be applied when the window is next
+ * shown.
+ */
+ private void setWindowFocusable(boolean focusable) {
+ int flags = mWindowLayoutParams.flags;
+ if (focusable) {
+ mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ } else {
+ mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ }
+ if (mWindowLayoutParams.flags == flags) {
+ return;
+ }
+ final View decorView = mWindow.peekDecorView();
+ if (decorView != null && decorView.isAttachedToWindow()) {
+ mWindowManager.updateViewLayout(decorView, mWindowLayoutParams);
+ }
+ }
+
+ static class ClipboardLogger {
+ private final UiEventLogger mUiEventLogger;
+ private boolean mGuarded = false;
+
+ ClipboardLogger(UiEventLogger uiEventLogger) {
+ mUiEventLogger = uiEventLogger;
+ }
+
+ void logSessionComplete(@NonNull UiEventLogger.UiEventEnum event) {
+ if (!mGuarded) {
+ mGuarded = true;
+ mUiEventLogger.log(event);
+ }
+ }
+
+ void reset() {
+ mGuarded = false;
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerFactory.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java
similarity index 76%
rename from packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerFactory.java
rename to packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java
index 8b0b2a5..0d989a7 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java
@@ -27,17 +27,17 @@
import javax.inject.Inject;
/**
- * A factory that churns out ClipboardOverlayControllers on demand.
+ * A factory that churns out ClipboardOverlayControllerLegacys on demand.
*/
@SysUISingleton
-public class ClipboardOverlayControllerFactory {
+public class ClipboardOverlayControllerLegacyFactory {
private final UiEventLogger mUiEventLogger;
private final BroadcastDispatcher mBroadcastDispatcher;
private final BroadcastSender mBroadcastSender;
@Inject
- public ClipboardOverlayControllerFactory(BroadcastDispatcher broadcastDispatcher,
+ public ClipboardOverlayControllerLegacyFactory(BroadcastDispatcher broadcastDispatcher,
BroadcastSender broadcastSender, UiEventLogger uiEventLogger) {
this.mBroadcastDispatcher = broadcastDispatcher;
this.mBroadcastSender = broadcastSender;
@@ -45,10 +45,10 @@
}
/**
- * One new ClipboardOverlayController, coming right up!
+ * One new ClipboardOverlayControllerLegacy, coming right up!
*/
- public ClipboardOverlayController create(Context context) {
- return new ClipboardOverlayController(context, mBroadcastDispatcher, mBroadcastSender,
+ public ClipboardOverlayControllerLegacy create(Context context) {
+ return new ClipboardOverlayControllerLegacy(context, mBroadcastDispatcher, mBroadcastSender,
new TimeoutHandler(context), mUiEventLogger);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
new file mode 100644
index 0000000..2d33157
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
@@ -0,0 +1,482 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.clipboardoverlay;
+
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
+import static java.util.Objects.requireNonNull;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.app.RemoteAction;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.Icon;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.MathUtils;
+import android.util.TypedValue;
+import android.view.DisplayCutout;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.PathInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
+import com.android.systemui.R;
+import com.android.systemui.screenshot.DraggableConstraintLayout;
+import com.android.systemui.screenshot.FloatingWindowUtil;
+import com.android.systemui.screenshot.OverlayActionChip;
+
+import java.util.ArrayList;
+
+/**
+ * Handles the visual elements and animations for the clipboard overlay.
+ */
+public class ClipboardOverlayView extends DraggableConstraintLayout {
+
+ interface ClipboardOverlayCallbacks extends SwipeDismissCallbacks {
+ void onDismissButtonTapped();
+
+ void onRemoteCopyButtonTapped();
+
+ void onEditButtonTapped();
+
+ void onShareButtonTapped();
+
+ void onPreviewTapped();
+ }
+
+ private static final String TAG = "ClipboardView";
+
+ private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
+ private static final int FONT_SEARCH_STEP_PX = 4;
+
+ private final DisplayMetrics mDisplayMetrics;
+ private final AccessibilityManager mAccessibilityManager;
+ private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>();
+
+ private View mClipboardPreview;
+ private ImageView mImagePreview;
+ private TextView mTextPreview;
+ private TextView mHiddenPreview;
+ private View mPreviewBorder;
+ private OverlayActionChip mEditChip;
+ private OverlayActionChip mShareChip;
+ private OverlayActionChip mRemoteCopyChip;
+ private View mActionContainerBackground;
+ private View mDismissButton;
+ private LinearLayout mActionContainer;
+
+ public ClipboardOverlayView(Context context) {
+ this(context, null);
+ }
+
+ public ClipboardOverlayView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ClipboardOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mDisplayMetrics = new DisplayMetrics();
+ mContext.getDisplay().getRealMetrics(mDisplayMetrics);
+ mAccessibilityManager = AccessibilityManager.getInstance(mContext);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ mActionContainerBackground =
+ requireNonNull(findViewById(R.id.actions_container_background));
+ mActionContainer = requireNonNull(findViewById(R.id.actions));
+ mClipboardPreview = requireNonNull(findViewById(R.id.clipboard_preview));
+ mImagePreview = requireNonNull(findViewById(R.id.image_preview));
+ mTextPreview = requireNonNull(findViewById(R.id.text_preview));
+ mHiddenPreview = requireNonNull(findViewById(R.id.hidden_preview));
+ mPreviewBorder = requireNonNull(findViewById(R.id.preview_border));
+ mEditChip = requireNonNull(findViewById(R.id.edit_chip));
+ mShareChip = requireNonNull(findViewById(R.id.share_chip));
+ mRemoteCopyChip = requireNonNull(findViewById(R.id.remote_copy_chip));
+ mDismissButton = requireNonNull(findViewById(R.id.dismiss_button));
+
+ mEditChip.setAlpha(1);
+ mShareChip.setAlpha(1);
+ mRemoteCopyChip.setAlpha(1);
+ mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share));
+
+ mEditChip.setIcon(
+ Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
+ mRemoteCopyChip.setIcon(
+ Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true);
+ mShareChip.setIcon(
+ Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
+
+ mRemoteCopyChip.setContentDescription(
+ mContext.getString(R.string.clipboard_send_nearby_description));
+
+ mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
+ int availableHeight = mTextPreview.getHeight()
+ - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom());
+ mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight());
+ return true;
+ });
+ super.onFinishInflate();
+ }
+
+ @Override
+ public void setCallbacks(SwipeDismissCallbacks callbacks) {
+ super.setCallbacks(callbacks);
+ ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks;
+ mEditChip.setOnClickListener(v -> clipboardCallbacks.onEditButtonTapped());
+ mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped());
+ mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped());
+ mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped());
+ mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped());
+ }
+
+ void setEditAccessibilityAction(boolean editable) {
+ if (editable) {
+ ViewCompat.replaceAccessibilityAction(mClipboardPreview,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ mContext.getString(R.string.clipboard_edit), null);
+ } else {
+ ViewCompat.replaceAccessibilityAction(mClipboardPreview,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ null, null);
+ }
+ }
+
+ void setInsets(WindowInsets insets, int orientation) {
+ FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) getLayoutParams();
+ if (p == null) {
+ return;
+ }
+ Rect margins = computeMargins(insets, orientation);
+ p.setMargins(margins.left, margins.top, margins.right, margins.bottom);
+ setLayoutParams(p);
+ requestLayout();
+ }
+
+ boolean isInTouchRegion(int x, int y) {
+ Region touchRegion = new Region();
+ final Rect tmpRect = new Rect();
+
+ mPreviewBorder.getBoundsOnScreen(tmpRect);
+ tmpRect.inset(
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
+ touchRegion.op(tmpRect, Region.Op.UNION);
+
+ mActionContainerBackground.getBoundsOnScreen(tmpRect);
+ tmpRect.inset(
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
+ touchRegion.op(tmpRect, Region.Op.UNION);
+
+ mDismissButton.getBoundsOnScreen(tmpRect);
+ touchRegion.op(tmpRect, Region.Op.UNION);
+
+ return touchRegion.contains(x, y);
+ }
+
+ void setRemoteCopyVisibility(boolean visible) {
+ if (visible) {
+ mRemoteCopyChip.setVisibility(View.VISIBLE);
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ } else {
+ mRemoteCopyChip.setVisibility(View.GONE);
+ }
+ }
+
+ void showDefaultTextPreview() {
+ String copied = mContext.getString(R.string.clipboard_overlay_text_copied);
+ showTextPreview(copied, false);
+ }
+
+ void showTextPreview(CharSequence text, boolean hidden) {
+ TextView textView = hidden ? mHiddenPreview : mTextPreview;
+ showSinglePreview(textView);
+ textView.setText(text.subSequence(0, Math.min(500, text.length())));
+ updateTextSize(text, textView);
+ textView.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ if (right - left != oldRight - oldLeft) {
+ updateTextSize(text, textView);
+ }
+ });
+ mEditChip.setVisibility(View.GONE);
+ }
+
+ void showImagePreview(@Nullable Bitmap thumbnail) {
+ if (thumbnail == null) {
+ mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden));
+ showSinglePreview(mHiddenPreview);
+ } else {
+ mImagePreview.setImageBitmap(thumbnail);
+ showSinglePreview(mImagePreview);
+ }
+ }
+
+ void showEditChip(String contentDescription) {
+ mEditChip.setVisibility(View.VISIBLE);
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ mEditChip.setContentDescription(contentDescription);
+ }
+
+ void showShareChip() {
+ mShareChip.setVisibility(View.VISIBLE);
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ }
+
+ void reset() {
+ setTranslationX(0);
+ setAlpha(0);
+ mActionContainerBackground.setVisibility(View.GONE);
+ mDismissButton.setVisibility(View.GONE);
+ mShareChip.setVisibility(View.GONE);
+ mEditChip.setVisibility(View.GONE);
+ mRemoteCopyChip.setVisibility(View.GONE);
+ setEditAccessibilityAction(false);
+ resetActionChips();
+ }
+
+ void resetActionChips() {
+ for (OverlayActionChip chip : mActionChips) {
+ mActionContainer.removeView(chip);
+ }
+ mActionChips.clear();
+ }
+
+ Animator getEnterAnimation() {
+ if (mAccessibilityManager.isEnabled()) {
+ mDismissButton.setVisibility(View.VISIBLE);
+ }
+ TimeInterpolator linearInterpolator = new LinearInterpolator();
+ TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f);
+ AnimatorSet enterAnim = new AnimatorSet();
+
+ ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
+ rootAnim.setInterpolator(linearInterpolator);
+ rootAnim.setDuration(66);
+ rootAnim.addUpdateListener(animation -> {
+ setAlpha(animation.getAnimatedFraction());
+ });
+
+ ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
+ scaleAnim.setInterpolator(scaleInterpolator);
+ scaleAnim.setDuration(333);
+ scaleAnim.addUpdateListener(animation -> {
+ float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
+ mClipboardPreview.setScaleX(previewScale);
+ mClipboardPreview.setScaleY(previewScale);
+ mPreviewBorder.setScaleX(previewScale);
+ mPreviewBorder.setScaleY(previewScale);
+
+ float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
+ mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
+ mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
+ float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction());
+ float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
+ mActionContainer.setScaleX(actionsScaleX);
+ mActionContainer.setScaleY(actionsScaleY);
+ mActionContainerBackground.setScaleX(actionsScaleX);
+ mActionContainerBackground.setScaleY(actionsScaleY);
+ });
+
+ ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
+ alphaAnim.setInterpolator(linearInterpolator);
+ alphaAnim.setDuration(283);
+ alphaAnim.addUpdateListener(animation -> {
+ float alpha = animation.getAnimatedFraction();
+ mClipboardPreview.setAlpha(alpha);
+ mPreviewBorder.setAlpha(alpha);
+ mDismissButton.setAlpha(alpha);
+ mActionContainer.setAlpha(alpha);
+ });
+
+ mActionContainer.setAlpha(0);
+ mPreviewBorder.setAlpha(0);
+ mClipboardPreview.setAlpha(0);
+ enterAnim.play(rootAnim).with(scaleAnim);
+ enterAnim.play(alphaAnim).after(50).after(rootAnim);
+
+ enterAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ setAlpha(1);
+ }
+ });
+ return enterAnim;
+ }
+
+ Animator getExitAnimation() {
+ TimeInterpolator linearInterpolator = new LinearInterpolator();
+ TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f);
+ AnimatorSet exitAnim = new AnimatorSet();
+
+ ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
+ rootAnim.setInterpolator(linearInterpolator);
+ rootAnim.setDuration(100);
+ rootAnim.addUpdateListener(anim -> setAlpha(1 - anim.getAnimatedFraction()));
+
+ ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
+ scaleAnim.setInterpolator(scaleInterpolator);
+ scaleAnim.setDuration(250);
+ scaleAnim.addUpdateListener(animation -> {
+ float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
+ mClipboardPreview.setScaleX(previewScale);
+ mClipboardPreview.setScaleY(previewScale);
+ mPreviewBorder.setScaleX(previewScale);
+ mPreviewBorder.setScaleY(previewScale);
+
+ float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
+ mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
+ mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
+ float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction());
+ float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
+ mActionContainer.setScaleX(actionScaleX);
+ mActionContainer.setScaleY(actionScaleY);
+ mActionContainerBackground.setScaleX(actionScaleX);
+ mActionContainerBackground.setScaleY(actionScaleY);
+ });
+
+ ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
+ alphaAnim.setInterpolator(linearInterpolator);
+ alphaAnim.setDuration(166);
+ alphaAnim.addUpdateListener(animation -> {
+ float alpha = 1 - animation.getAnimatedFraction();
+ mClipboardPreview.setAlpha(alpha);
+ mPreviewBorder.setAlpha(alpha);
+ mDismissButton.setAlpha(alpha);
+ mActionContainer.setAlpha(alpha);
+ });
+
+ exitAnim.play(alphaAnim).with(scaleAnim);
+ exitAnim.play(rootAnim).after(150).after(alphaAnim);
+ return exitAnim;
+ }
+
+ void setActionChip(RemoteAction action, Runnable onFinish) {
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ OverlayActionChip chip = constructActionChip(action, onFinish);
+ mActionContainer.addView(chip);
+ mActionChips.add(chip);
+ }
+
+ private void showSinglePreview(View v) {
+ mTextPreview.setVisibility(View.GONE);
+ mImagePreview.setVisibility(View.GONE);
+ mHiddenPreview.setVisibility(View.GONE);
+ v.setVisibility(View.VISIBLE);
+ }
+
+ private OverlayActionChip constructActionChip(RemoteAction action, Runnable onFinish) {
+ OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate(
+ R.layout.overlay_action_chip, mActionContainer, false);
+ chip.setText(action.getTitle());
+ chip.setContentDescription(action.getTitle());
+ chip.setIcon(action.getIcon(), false);
+ chip.setPendingIntent(action.getActionIntent(), onFinish);
+ chip.setAlpha(1);
+ return chip;
+ }
+
+ private static void updateTextSize(CharSequence text, TextView textView) {
+ Paint paint = new Paint(textView.getPaint());
+ Resources res = textView.getResources();
+ float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font);
+ float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font);
+ if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) {
+ // If the text is a single word and would fit within the TextView at the min font size,
+ // find the biggest font size that will fit.
+ float fontSizePx = minFontSize;
+ while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize
+ && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) {
+ fontSizePx += FONT_SEARCH_STEP_PX;
+ }
+ // Need to turn off autosizing, otherwise setTextSize is a no-op.
+ textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
+ // It's possible to hit the max font size and not fill the width, so centering
+ // horizontally looks better in this case.
+ textView.setGravity(Gravity.CENTER);
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx);
+ } else {
+ // Otherwise just stick with autosize.
+ textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize,
+ (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX);
+ textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
+ }
+ }
+
+ private static boolean fitsInView(CharSequence text, TextView textView, Paint paint,
+ float fontSizePx) {
+ paint.setTextSize(fontSizePx);
+ float size = paint.measureText(text.toString());
+ float availableWidth = textView.getWidth() - textView.getPaddingLeft()
+ - textView.getPaddingRight();
+ return size < availableWidth;
+ }
+
+ private static boolean isOneWord(CharSequence text) {
+ return text.toString().split("\\s+", 2).length == 1;
+ }
+
+ private static Rect computeMargins(WindowInsets insets, int orientation) {
+ DisplayCutout cutout = insets.getDisplayCutout();
+ Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
+ Insets imeInsets = insets.getInsets(WindowInsets.Type.ime());
+ if (cutout == null) {
+ return new Rect(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom));
+ } else {
+ Insets waterfall = cutout.getWaterfallInsets();
+ if (orientation == ORIENTATION_PORTRAIT) {
+ return new Rect(
+ waterfall.left,
+ Math.max(cutout.getSafeInsetTop(), waterfall.top),
+ waterfall.right,
+ Math.max(imeInsets.bottom,
+ Math.max(cutout.getSafeInsetBottom(),
+ Math.max(navBarInsets.bottom, waterfall.bottom))));
+ } else {
+ return new Rect(
+ waterfall.left,
+ waterfall.top,
+ waterfall.right,
+ Math.max(imeInsets.bottom,
+ Math.max(navBarInsets.bottom, waterfall.bottom)));
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java
new file mode 100644
index 0000000..9dac9b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.clipboardoverlay;
+
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.app.ICompatCameraControlCallback;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+
+import com.android.internal.policy.PhoneWindow;
+import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext;
+import com.android.systemui.screenshot.FloatingWindowUtil;
+
+import java.util.function.BiConsumer;
+
+import javax.inject.Inject;
+
+/**
+ * Handles attaching the window and the window insets for the clipboard overlay.
+ */
+public class ClipboardOverlayWindow extends PhoneWindow
+ implements ViewRootImpl.ActivityConfigCallback {
+ private static final String TAG = "ClipboardOverlayWindow";
+
+ private final Context mContext;
+ private final WindowManager mWindowManager;
+ private final WindowManager.LayoutParams mWindowLayoutParams;
+
+ private boolean mKeyboardVisible;
+ private final int mOrientation;
+ private BiConsumer<WindowInsets, Integer> mOnKeyboardChangeListener;
+ private Runnable mOnOrientationChangeListener;
+
+ @Inject
+ ClipboardOverlayWindow(@OverlayWindowContext Context context) {
+ super(context);
+ mContext = context;
+ mOrientation = mContext.getResources().getConfiguration().orientation;
+
+ // Setup the window that we are going to use
+ requestFeature(Window.FEATURE_NO_TITLE);
+ requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
+ setBackgroundDrawableResource(android.R.color.transparent);
+ mWindowManager = mContext.getSystemService(WindowManager.class);
+ mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
+ mWindowLayoutParams.setTitle("ClipboardOverlay");
+ setWindowManager(mWindowManager, null, null);
+ setWindowFocusable(false);
+ }
+
+ /**
+ * Set callbacks for keyboard state change and orientation change and attach the window
+ *
+ * @param onKeyboardChangeListener callback for IME visibility changes
+ * @param onOrientationChangeListener callback for device orientation changes
+ */
+ public void init(@NonNull BiConsumer<WindowInsets, Integer> onKeyboardChangeListener,
+ @NonNull Runnable onOrientationChangeListener) {
+ mOnKeyboardChangeListener = onKeyboardChangeListener;
+ mOnOrientationChangeListener = onOrientationChangeListener;
+
+ attach();
+ withWindowAttached(() -> {
+ WindowInsets currentInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+ mKeyboardVisible = currentInsets.isVisible(WindowInsets.Type.ime());
+ peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(() -> {
+ WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+ boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime());
+ if (keyboardVisible != mKeyboardVisible) {
+ mKeyboardVisible = keyboardVisible;
+ mOnKeyboardChangeListener.accept(insets, mOrientation);
+ }
+ });
+ peekDecorView().getViewRootImpl().setActivityConfigCallback(this);
+ });
+ }
+
+ @Override // ViewRootImpl.ActivityConfigCallback
+ public void onConfigurationChanged(Configuration overrideConfig, int newDisplayId) {
+ if (mContext.getResources().getConfiguration().orientation != mOrientation) {
+ mOnOrientationChangeListener.run();
+ }
+ }
+
+ @Override // ViewRootImpl.ActivityConfigCallback
+ public void requestCompatCameraControl(boolean showControl, boolean transformationApplied,
+ ICompatCameraControlCallback callback) {
+ Log.w(TAG, "unexpected requestCompatCameraControl call");
+ }
+
+ void remove() {
+ final View decorView = peekDecorView();
+ if (decorView != null && decorView.isAttachedToWindow()) {
+ mWindowManager.removeViewImmediate(decorView);
+ }
+ }
+
+ WindowInsets getWindowInsets() {
+ return mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+ }
+
+ void withWindowAttached(Runnable action) {
+ View decorView = getDecorView();
+ if (decorView.isAttachedToWindow()) {
+ action.run();
+ } else {
+ decorView.getViewTreeObserver().addOnWindowAttachListener(
+ new ViewTreeObserver.OnWindowAttachListener() {
+ @Override
+ public void onWindowAttached() {
+ decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
+ action.run();
+ }
+
+ @Override
+ public void onWindowDetached() {
+ }
+ });
+ }
+ }
+
+ @MainThread
+ private void attach() {
+ View decorView = getDecorView();
+ if (decorView.isAttachedToWindow()) {
+ return;
+ }
+ mWindowManager.addView(decorView, mWindowLayoutParams);
+ decorView.requestApplyInsets();
+ }
+
+ /**
+ * Updates the window focusability. If the window is already showing, then it updates the
+ * window immediately, otherwise the layout params will be applied when the window is next
+ * shown.
+ */
+ private void setWindowFocusable(boolean focusable) {
+ int flags = mWindowLayoutParams.flags;
+ if (focusable) {
+ mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ } else {
+ mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ }
+ if (mWindowLayoutParams.flags == flags) {
+ return;
+ }
+ final View decorView = peekDecorView();
+ if (decorView != null && decorView.isAttachedToWindow()) {
+ mWindowManager.updateViewLayout(decorView, mWindowLayoutParams);
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
new file mode 100644
index 0000000..2244813
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.clipboardoverlay.dagger;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.view.Display;
+import android.view.LayoutInflater;
+
+import com.android.systemui.R;
+import com.android.systemui.clipboardoverlay.ClipboardOverlayView;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+import dagger.Module;
+import dagger.Provides;
+
+/** Module for {@link com.android.systemui.clipboardoverlay}. */
+@Module
+public interface ClipboardOverlayModule {
+
+ /**
+ *
+ */
+ @Provides
+ @OverlayWindowContext
+ static Context provideWindowContext(DisplayManager displayManager, Context context) {
+ Display display = displayManager.getDisplay(DEFAULT_DISPLAY);
+ return context.createWindowContext(display, TYPE_SCREENSHOT, null);
+ }
+
+ /**
+ *
+ */
+ @Provides
+ static ClipboardOverlayView provideClipboardOverlayView(@OverlayWindowContext Context context) {
+ return (ClipboardOverlayView) LayoutInflater.from(context).inflate(
+ R.layout.clipboard_overlay, null);
+ }
+
+ @Qualifier
+ @Documented
+ @Retention(RUNTIME)
+ @interface OverlayWindowContext {
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt
index 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/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
index 77b6523..d3b5d0e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
@@ -21,6 +21,8 @@
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
+import android.os.RemoteException
+import android.service.dreams.IDreamManager
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
@@ -40,11 +42,13 @@
*/
class ControlsActivity @Inject constructor(
private val uiController: ControlsUiController,
- private val broadcastDispatcher: BroadcastDispatcher
+ private val broadcastDispatcher: BroadcastDispatcher,
+ private val dreamManager: IDreamManager,
) : ComponentActivity() {
private lateinit var parent: ViewGroup
private lateinit var broadcastReceiver: BroadcastReceiver
+ private var mExitToDream: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -81,17 +85,36 @@
parent = requireViewById<ViewGroup>(R.id.global_actions_controls)
parent.alpha = 0f
- uiController.show(parent, { finish() }, this)
+ uiController.show(parent, { finishOrReturnToDream() }, this)
ControlsAnimations.enterAnimation(parent).start()
}
- override fun onBackPressed() {
+ override fun onResume() {
+ super.onResume()
+ mExitToDream = intent.getBooleanExtra(ControlsUiController.EXIT_TO_DREAM, false)
+ }
+
+ fun finishOrReturnToDream() {
+ if (mExitToDream) {
+ try {
+ mExitToDream = false
+ dreamManager.dream()
+ return
+ } catch (e: RemoteException) {
+ // Fall through
+ }
+ }
finish()
}
+ override fun onBackPressed() {
+ finishOrReturnToDream()
+ }
+
override fun onStop() {
super.onStop()
+ mExitToDream = false
uiController.hide()
}
@@ -106,7 +129,8 @@
broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.getAction()
- if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+ if (action == Intent.ACTION_SCREEN_OFF ||
+ action == Intent.ACTION_DREAMING_STARTED) {
finish()
}
}
@@ -114,6 +138,7 @@
val filter = IntentFilter()
filter.addAction(Intent.ACTION_SCREEN_OFF)
+ filter.addAction(Intent.ACTION_DREAMING_STARTED)
broadcastDispatcher.registerReceiver(broadcastReceiver, filter)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
index 822f8f2..c1cfbcb 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
@@ -27,6 +27,7 @@
companion object {
public const val TAG = "ControlsUiController"
public const val EXTRA_ANIMATE = "extra_animate"
+ public const val EXIT_TO_DREAM = "extra_exit_to_dream"
}
fun show(parent: ViewGroup, onDismiss: Runnable, activityContext: Context)
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index c2dffe8..d05bd51 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -28,6 +28,7 @@
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.people.PeopleProvider;
+import com.android.systemui.statusbar.QsFrameTranslateModule;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.unfold.FoldStateLogger;
import com.android.systemui.unfold.FoldStateLoggingProvider;
@@ -63,6 +64,7 @@
@Subcomponent(modules = {
DefaultComponentBinder.class,
DependencyProvider.class,
+ QsFrameTranslateModule.class,
SystemUIBinder.class,
SystemUIModule.class,
SystemUICoreStartableModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 55eda0a..721c0ba 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -34,13 +34,14 @@
import com.android.systemui.media.RingtonePlayer
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper
import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver
-import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender
+import com.android.systemui.media.taptotransfer.sender.MediaTttSenderCoordinator
import com.android.systemui.power.PowerUI
import com.android.systemui.recents.Recents
import com.android.systemui.settings.dagger.MultiUserUtilsModule
import com.android.systemui.shortcut.ShortcutKeyDispatcher
import com.android.systemui.statusbar.notification.InstantAppNotifier
import com.android.systemui.statusbar.phone.KeyguardLiftController
+import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
import com.android.systemui.theme.ThemeOverlayController
import com.android.systemui.toast.ToastUI
import com.android.systemui.usb.StorageNotification
@@ -217,6 +218,12 @@
@ClassKey(KeyguardLiftController::class)
abstract fun bindKeyguardLiftController(sysui: KeyguardLiftController): CoreStartable
+ /** Inject into MediaTttSenderCoordinator. */
+ @Binds
+ @IntoMap
+ @ClassKey(MediaTttSenderCoordinator::class)
+ abstract fun bindMediaTttSenderCoordinator(sysui: MediaTttSenderCoordinator): CoreStartable
+
/** Inject into MediaTttChipControllerReceiver. */
@Binds
@IntoMap
@@ -225,17 +232,15 @@
sysui: MediaTttChipControllerReceiver
): CoreStartable
- /** Inject into MediaTttChipControllerSender. */
- @Binds
- @IntoMap
- @ClassKey(MediaTttChipControllerSender::class)
- abstract fun bindMediaTttChipControllerSender(
- sysui: MediaTttChipControllerSender
- ): CoreStartable
-
/** Inject into MediaTttCommandLineHelper. */
@Binds
@IntoMap
@ClassKey(MediaTttCommandLineHelper::class)
abstract fun bindMediaTttCommandLineHelper(sysui: MediaTttCommandLineHelper): CoreStartable
+
+ /** Inject into ChipbarCoordinator. */
+ @Binds
+ @IntoMap
+ @ClassKey(ChipbarCoordinator::class)
+ abstract fun bindChipbarController(sysui: ChipbarCoordinator): CoreStartable
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index d70b971..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;
@@ -33,6 +34,7 @@
import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
import com.android.systemui.biometrics.dagger.BiometricsModule;
import com.android.systemui.classifier.FalsingModule;
+import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
import com.android.systemui.controls.dagger.ControlsModule;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.demomode.dagger.DemoModeModule;
@@ -61,7 +63,6 @@
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.QsFrameTranslateModule;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder;
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
@@ -119,7 +120,9 @@
AssistModule.class,
BiometricsModule.class,
BouncerViewModule.class,
- ClockModule.class,
+ ClipboardOverlayModule.class,
+ ClockInfoModule.class,
+ ClockRegistryModule.class,
CoroutinesModule.class,
DreamModule.class,
ControlsModule.class,
@@ -133,7 +136,6 @@
PeopleModule.class,
PluginModule.class,
PrivacyModule.class,
- QsFrameTranslateModule.class,
ScreenshotModule.class,
SensorModule.class,
MultiUserUtilsModule.class,
@@ -167,12 +169,16 @@
@Binds
abstract BootCompleteCache bindBootCompleteCache(BootCompleteCacheImpl bootCompleteCache);
- /** */
+ /**
+ *
+ */
@Binds
public abstract ContextComponentHelper bindComponentHelper(
ContextComponentResolver componentHelper);
- /** */
+ /**
+ *
+ */
@Binds
public abstract NotificationRowBinder bindNotificationRowBinder(
NotificationRowBinderImpl notificationRowBinder);
@@ -211,6 +217,7 @@
abstract SystemClock bindSystemClock(SystemClockImpl systemClock);
// TODO: This should provided by the WM component
+
/** Provides Optional of BubbleManager */
@SysUISingleton
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt
index 991b54e..ded0fb7 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt
@@ -59,7 +59,7 @@
(view as? DisplayCutoutView)?.let { cutoutView ->
cutoutView.setColor(tintColor)
cutoutView.updateRotation(rotation)
- cutoutView.onDisplayChanged(displayUniqueId)
+ cutoutView.updateConfiguration(displayUniqueId)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index ec0013b..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
}
}
@@ -124,7 +120,7 @@
view.layoutParams = it
(view as? FaceScanningOverlay)?.let { overlay ->
overlay.setColor(tintColor)
- overlay.onDisplayChanged(displayUniqueId)
+ overlay.updateConfiguration(displayUniqueId)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt
index a252864..8b4aeef 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt
@@ -78,23 +78,18 @@
reloadMeasures()
}
- private fun reloadAll(newReloadToken: Int) {
- if (reloadToken == newReloadToken) {
- return
- }
- reloadToken = newReloadToken
- reloadRes()
- reloadMeasures()
- }
-
fun updateDisplayUniqueId(newDisplayUniqueId: String?, newReloadToken: Int?) {
if (displayUniqueId != newDisplayUniqueId) {
displayUniqueId = newDisplayUniqueId
newReloadToken ?.let { reloadToken = it }
reloadRes()
reloadMeasures()
- } else {
- newReloadToken?.let { reloadAll(it) }
+ } else if (newReloadToken != null) {
+ if (reloadToken == newReloadToken) {
+ return
+ }
+ reloadToken = newReloadToken
+ reloadMeasures()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 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 cc57662..18c8e01 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -19,12 +19,13 @@
import android.view.Display
import com.android.systemui.doze.DozeLog.Reason
import com.android.systemui.doze.DozeLog.reasonToString
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
import com.android.systemui.log.dagger.DozeLog
import com.android.systemui.statusbar.policy.DevicePostureController
+import com.google.errorprone.annotations.CompileTimeConstant
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -224,10 +225,14 @@
})
}
- 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/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
index ae41215..96c35d4 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
@@ -20,7 +20,6 @@
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
import android.annotation.MainThread;
-import android.app.UiModeManager;
import android.content.res.Configuration;
import android.hardware.display.AmbientDisplayConfiguration;
import android.os.Trace;
@@ -145,10 +144,9 @@
private final Service mDozeService;
private final WakeLock mWakeLock;
- private final AmbientDisplayConfiguration mConfig;
+ private final AmbientDisplayConfiguration mAmbientDisplayConfig;
private final WakefulnessLifecycle mWakefulnessLifecycle;
private final DozeHost mDozeHost;
- private final UiModeManager mUiModeManager;
private final DockManager mDockManager;
private final Part[] mParts;
@@ -156,18 +154,18 @@
private State mState = State.UNINITIALIZED;
private int mPulseReason;
private boolean mWakeLockHeldForCurrentState = false;
+ private int mUiModeType = Configuration.UI_MODE_TYPE_NORMAL;
@Inject
- public DozeMachine(@WrappedService Service service, AmbientDisplayConfiguration config,
+ public DozeMachine(@WrappedService Service service,
+ AmbientDisplayConfiguration ambientDisplayConfig,
WakeLock wakeLock, WakefulnessLifecycle wakefulnessLifecycle,
- UiModeManager uiModeManager,
DozeLog dozeLog, DockManager dockManager,
DozeHost dozeHost, Part[] parts) {
mDozeService = service;
- mConfig = config;
+ mAmbientDisplayConfig = ambientDisplayConfig;
mWakefulnessLifecycle = wakefulnessLifecycle;
mWakeLock = wakeLock;
- mUiModeManager = uiModeManager;
mDozeLog = dozeLog;
mDockManager = dockManager;
mDozeHost = dozeHost;
@@ -187,6 +185,18 @@
}
/**
+ * Notifies the {@link DozeMachine} that {@link Configuration} has changed.
+ */
+ public void onConfigurationChanged(Configuration newConfiguration) {
+ int newUiModeType = newConfiguration.uiMode & Configuration.UI_MODE_TYPE_MASK;
+ if (mUiModeType == newUiModeType) return;
+ mUiModeType = newUiModeType;
+ for (Part part : mParts) {
+ part.onUiModeTypeChanged(mUiModeType);
+ }
+ }
+
+ /**
* Requests transitioning to {@code requestedState}.
*
* This can be called during a state transition, in which case it will be queued until all
@@ -211,6 +221,14 @@
requestState(State.DOZE_REQUEST_PULSE, pulseReason);
}
+ /**
+ * @return true if {@link DozeMachine} is currently in either {@link State#UNINITIALIZED}
+ * or {@link State#FINISH}
+ */
+ public boolean isUninitializedOrFinished() {
+ return mState == State.UNINITIALIZED || mState == State.FINISH;
+ }
+
void onScreenState(int state) {
mDozeLog.traceDisplayState(state);
for (Part part : mParts) {
@@ -360,7 +378,7 @@
if (mState == State.FINISH) {
return State.FINISH;
}
- if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR
+ if (mUiModeType == Configuration.UI_MODE_TYPE_CAR
&& (requestedState.canPulse() || requestedState.staysAwake())) {
Log.i(TAG, "Doze is suppressed with all triggers disabled as car mode is active");
mDozeLog.traceCarModeStarted();
@@ -411,7 +429,7 @@
nextState = State.FINISH;
} else if (mDockManager.isDocked()) {
nextState = mDockManager.isHidden() ? State.DOZE : State.DOZE_AOD_DOCKED;
- } else if (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) {
+ } else if (mAmbientDisplayConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) {
nextState = State.DOZE_AOD;
} else {
nextState = State.DOZE;
@@ -427,6 +445,7 @@
/** Dumps the current state */
public void dump(PrintWriter pw) {
pw.print(" state="); pw.println(mState);
+ pw.print(" mUiModeType="); pw.println(mUiModeType);
pw.print(" wakeLockHeldForCurrentState="); pw.println(mWakeLockHeldForCurrentState);
pw.print(" wakeLock="); pw.println(mWakeLock);
pw.println("Parts:");
@@ -459,6 +478,19 @@
/** Sets the {@link DozeMachine} when this Part is associated with one. */
default void setDozeMachine(DozeMachine dozeMachine) {}
+
+ /**
+ * Notifies the Part about a change in {@link Configuration#uiMode}.
+ *
+ * @param newUiModeType {@link Configuration#UI_MODE_TYPE_NORMAL},
+ * {@link Configuration#UI_MODE_TYPE_DESK},
+ * {@link Configuration#UI_MODE_TYPE_CAR},
+ * {@link Configuration#UI_MODE_TYPE_TELEVISION},
+ * {@link Configuration#UI_MODE_TYPE_APPLIANCE},
+ * {@link Configuration#UI_MODE_TYPE_WATCH},
+ * or {@link Configuration#UI_MODE_TYPE_VR_HEADSET}
+ */
+ default void onUiModeTypeChanged(int newUiModeType) {}
}
/** A wrapper interface for {@link android.service.dreams.DreamService} */
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 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/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index a2eb4e3..e8d7e46 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -17,6 +17,7 @@
package com.android.systemui.doze;
import android.content.Context;
+import android.content.res.Configuration;
import android.os.PowerManager;
import android.os.SystemClock;
import android.service.dreams.DreamService;
@@ -59,6 +60,7 @@
mPluginManager.addPluginListener(this, DozeServicePlugin.class, false /* allowMultiple */);
DozeComponent dozeComponent = mDozeComponentBuilder.build(this);
mDozeMachine = dozeComponent.getDozeMachine();
+ mDozeMachine.onConfigurationChanged(getResources().getConfiguration());
}
@Override
@@ -127,6 +129,12 @@
}
@Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mDozeMachine.onConfigurationChanged(newConfig);
+ }
+
+ @Override
public void onRequestHideDoze() {
if (mDozeMachine != null) {
mDozeMachine.requestState(DozeMachine.State.DOZE);
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
index 7ed4b35..e6d9865 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
@@ -16,21 +16,13 @@
package com.android.systemui.doze;
-import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE;
-import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
-import android.app.UiModeManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
import android.hardware.display.AmbientDisplayConfiguration;
import android.os.PowerManager;
import android.os.UserHandle;
import android.text.TextUtils;
-import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.doze.dagger.DozeScope;
import com.android.systemui.statusbar.phone.BiometricUnlockController;
@@ -43,7 +35,9 @@
/**
* Handles suppressing doze on:
* 1. INITIALIZED, don't allow dozing at all when:
- * - in CAR_MODE
+ * - in CAR_MODE, in this scenario the device is asleep and won't listen for any triggers
+ * to wake up. In this state, no UI shows. Unlike other conditions, this suppression is only
+ * temporary and stops when the device exits CAR_MODE
* - device is NOT provisioned
* - there's a pending authentication
* 2. PowerSaveMode active
@@ -57,35 +51,47 @@
*/
@DozeScope
public class DozeSuppressor implements DozeMachine.Part {
- private static final String TAG = "DozeSuppressor";
private DozeMachine mMachine;
private final DozeHost mDozeHost;
private final AmbientDisplayConfiguration mConfig;
private final DozeLog mDozeLog;
- private final BroadcastDispatcher mBroadcastDispatcher;
- private final UiModeManager mUiModeManager;
private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
- private boolean mBroadcastReceiverRegistered;
+ private boolean mIsCarModeEnabled = false;
@Inject
public DozeSuppressor(
DozeHost dozeHost,
AmbientDisplayConfiguration config,
DozeLog dozeLog,
- BroadcastDispatcher broadcastDispatcher,
- UiModeManager uiModeManager,
Lazy<BiometricUnlockController> biometricUnlockControllerLazy) {
mDozeHost = dozeHost;
mConfig = config;
mDozeLog = dozeLog;
- mBroadcastDispatcher = broadcastDispatcher;
- mUiModeManager = uiModeManager;
mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
}
@Override
+ public void onUiModeTypeChanged(int newUiModeType) {
+ boolean isCarModeEnabled = newUiModeType == UI_MODE_TYPE_CAR;
+ if (mIsCarModeEnabled == isCarModeEnabled) {
+ return;
+ }
+ mIsCarModeEnabled = isCarModeEnabled;
+ // Do not handle the event if doze machine is not initialized yet.
+ // It will be handled upon initialization.
+ if (mMachine.isUninitializedOrFinished()) {
+ return;
+ }
+ if (mIsCarModeEnabled) {
+ handleCarModeStarted();
+ } else {
+ handleCarModeExited();
+ }
+ }
+
+ @Override
public void setDozeMachine(DozeMachine dozeMachine) {
mMachine = dozeMachine;
}
@@ -94,7 +100,6 @@
public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
switch (newState) {
case INITIALIZED:
- registerBroadcastReceiver();
mDozeHost.addCallback(mHostCallback);
checkShouldImmediatelyEndDoze();
checkShouldImmediatelySuspendDoze();
@@ -108,14 +113,12 @@
@Override
public void destroy() {
- unregisterBroadcastReceiver();
mDozeHost.removeCallback(mHostCallback);
}
private void checkShouldImmediatelySuspendDoze() {
- if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
- mDozeLog.traceCarModeStarted();
- mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
+ if (mIsCarModeEnabled) {
+ handleCarModeStarted();
}
}
@@ -135,7 +138,7 @@
@Override
public void dump(PrintWriter pw) {
- pw.println(" uiMode=" + mUiModeManager.getCurrentModeType());
+ pw.println(" isCarModeEnabled=" + mIsCarModeEnabled);
pw.println(" hasPendingAuth="
+ mBiometricUnlockControllerLazy.get().hasPendingAuthentication());
pw.println(" isProvisioned=" + mDozeHost.isProvisioned());
@@ -143,40 +146,18 @@
pw.println(" aodPowerSaveActive=" + mDozeHost.isPowerSaveActive());
}
- private void registerBroadcastReceiver() {
- if (mBroadcastReceiverRegistered) {
- return;
- }
- IntentFilter filter = new IntentFilter(ACTION_ENTER_CAR_MODE);
- filter.addAction(ACTION_EXIT_CAR_MODE);
- mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter);
- mBroadcastReceiverRegistered = true;
+ private void handleCarModeExited() {
+ mDozeLog.traceCarModeEnded();
+ mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)
+ ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
}
- private void unregisterBroadcastReceiver() {
- if (!mBroadcastReceiverRegistered) {
- return;
- }
- mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver);
- mBroadcastReceiverRegistered = false;
+ private void handleCarModeStarted() {
+ mDozeLog.traceCarModeStarted();
+ mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
}
- private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (ACTION_ENTER_CAR_MODE.equals(action)) {
- mDozeLog.traceCarModeStarted();
- mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
- } else if (ACTION_EXIT_CAR_MODE.equals(action)) {
- mDozeLog.traceCarModeEnded();
- mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)
- ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
- }
- }
- };
-
- private DozeHost.Callback mHostCallback = new DozeHost.Callback() {
+ private final DozeHost.Callback mHostCallback = new DozeHost.Callback() {
@Override
public void onPowerSaveChanged(boolean active) {
// handles suppression changes, while DozeMachine#transitionPolicy handles gating
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 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/DreamOverlayRegistrant.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
index 99ca3c7..d145f5c 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
@@ -40,11 +40,12 @@
* {@link DreamOverlayRegistrant} is responsible for telling system server that SystemUI should be
* the designated dream overlay component.
*/
-public class DreamOverlayRegistrant extends CoreStartable {
+public class DreamOverlayRegistrant implements CoreStartable {
private static final String TAG = "DreamOverlayRegistrant";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final IDreamManager mDreamManager;
private final ComponentName mOverlayServiceComponent;
+ private final Context mContext;
private final Resources mResources;
private boolean mCurrentRegisteredState = false;
@@ -98,7 +99,7 @@
@Inject
public DreamOverlayRegistrant(Context context, @Main Resources resources) {
- super(context);
+ mContext = context;
mResources = resources;
mDreamManager = IDreamManager.Stub.asInterface(
ServiceManager.getService(DreamService.DREAM_SERVICE));
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 696fc72..d1b7368 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -64,29 +64,26 @@
private final Executor mExecutor;
// A controller for the dream overlay container view (which contains both the status bar and the
// content area).
- private final DreamOverlayContainerViewController mDreamOverlayContainerViewController;
+ private DreamOverlayContainerViewController mDreamOverlayContainerViewController;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@Nullable
private final ComponentName mLowLightDreamComponent;
private final UiEventLogger mUiEventLogger;
+ private final WindowManager mWindowManager;
// A reference to the {@link Window} used to hold the dream overlay.
private Window mWindow;
- // True if the service has been destroyed.
- private boolean mDestroyed;
+ // True if a dream has bound to the service and dream overlay service has started.
+ private boolean mStarted = false;
- private final Complication.Host mHost = new Complication.Host() {
- @Override
- public void requestExitDream() {
- mExecutor.execute(DreamOverlayService.this::requestExit);
- }
- };
+ // True if the service has been destroyed.
+ private boolean mDestroyed = false;
+
+ private final DreamOverlayComponent mDreamOverlayComponent;
private final LifecycleRegistry mLifecycleRegistry;
- private ViewModelStore mViewModelStore = new ViewModelStore();
-
private DreamOverlayTouchMonitor mDreamOverlayTouchMonitor;
private final KeyguardUpdateMonitorCallback mKeyguardCallback =
@@ -103,7 +100,7 @@
}
};
- private DreamOverlayStateController mStateController;
+ private final DreamOverlayStateController mStateController;
@VisibleForTesting
public enum DreamOverlayEvent implements UiEventLogger.UiEventEnum {
@@ -128,6 +125,7 @@
public DreamOverlayService(
Context context,
@Main Executor executor,
+ WindowManager windowManager,
DreamOverlayComponent.Factory dreamOverlayComponentFactory,
DreamOverlayStateController stateController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -136,19 +134,19 @@
ComponentName lowLightDreamComponent) {
mContext = context;
mExecutor = executor;
+ mWindowManager = windowManager;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mLowLightDreamComponent = lowLightDreamComponent;
mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
mStateController = stateController;
mUiEventLogger = uiEventLogger;
- final DreamOverlayComponent component =
- dreamOverlayComponentFactory.create(mViewModelStore, mHost);
- mDreamOverlayContainerViewController = component.getDreamOverlayContainerViewController();
+ final ViewModelStore viewModelStore = new ViewModelStore();
+ final Complication.Host host =
+ () -> mExecutor.execute(DreamOverlayService.this::requestExit);
+ mDreamOverlayComponent = dreamOverlayComponentFactory.create(viewModelStore, host);
+ mLifecycleRegistry = mDreamOverlayComponent.getLifecycleRegistry();
setCurrentState(Lifecycle.State.CREATED);
- mLifecycleRegistry = component.getLifecycleRegistry();
- mDreamOverlayTouchMonitor = component.getDreamOverlayTouchMonitor();
- mDreamOverlayTouchMonitor.init();
}
private void setCurrentState(Lifecycle.State state) {
@@ -159,34 +157,48 @@
public void onDestroy() {
mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback);
setCurrentState(Lifecycle.State.DESTROYED);
- final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
- if (mWindow != null) {
- windowManager.removeView(mWindow.getDecorView());
- }
- mStateController.setOverlayActive(false);
- mStateController.setLowLightActive(false);
+
+ resetCurrentDreamOverlay();
+
mDestroyed = true;
super.onDestroy();
}
@Override
public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
- mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
setCurrentState(Lifecycle.State.STARTED);
- final ComponentName dreamComponent = getDreamComponent();
- mStateController.setLowLightActive(
- dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
+
mExecutor.execute(() -> {
+ mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
+
if (mDestroyed) {
// The task could still be executed after the service has been destroyed. Bail if
// that is the case.
return;
}
+
+ if (mStarted) {
+ // Reset the current dream overlay before starting a new one. This can happen
+ // when two dreams overlap (briefly, for a smoother dream transition) and both
+ // dreams are bound to the dream overlay service.
+ resetCurrentDreamOverlay();
+ }
+
+ mDreamOverlayContainerViewController =
+ mDreamOverlayComponent.getDreamOverlayContainerViewController();
+ mDreamOverlayTouchMonitor = mDreamOverlayComponent.getDreamOverlayTouchMonitor();
+ mDreamOverlayTouchMonitor.init();
+
mStateController.setShouldShowComplications(shouldShowComplications());
addOverlayWindowLocked(layoutParams);
setCurrentState(Lifecycle.State.RESUMED);
mStateController.setOverlayActive(true);
+ final ComponentName dreamComponent = getDreamComponent();
+ mStateController.setLowLightActive(
+ dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START);
+
+ mStarted = true;
});
}
@@ -222,8 +234,7 @@
removeContainerViewFromParent();
mWindow.setContentView(mDreamOverlayContainerViewController.getContainerView());
- final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
- windowManager.addView(mWindow.getDecorView(), mWindow.getAttributes());
+ mWindowManager.addView(mWindow.getDecorView(), mWindow.getAttributes());
}
private void removeContainerViewFromParent() {
@@ -238,4 +249,18 @@
Log.w(TAG, "Removing dream overlay container view parent!");
parentView.removeView(containerView);
}
+
+ private void resetCurrentDreamOverlay() {
+ if (mStarted && mWindow != null) {
+ mWindowManager.removeView(mWindow.getDecorView());
+ }
+
+ mStateController.setOverlayActive(false);
+ mStateController.setLowLightActive(false);
+
+ mDreamOverlayContainerViewController = null;
+ mDreamOverlayTouchMonitor = null;
+
+ mStarted = false;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java
index bbcab60..ee2f1af 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java
@@ -16,7 +16,6 @@
package com.android.systemui.dreams.complication;
-import android.content.Context;
import android.database.ContentObserver;
import android.os.UserHandle;
import android.provider.Settings;
@@ -37,7 +36,7 @@
* user, and pushes updates to {@link DreamOverlayStateController}.
*/
@SysUISingleton
-public class ComplicationTypesUpdater extends CoreStartable {
+public class ComplicationTypesUpdater implements CoreStartable {
private final DreamBackend mDreamBackend;
private final Executor mExecutor;
private final SecureSettings mSecureSettings;
@@ -45,13 +44,11 @@
private final DreamOverlayStateController mDreamOverlayStateController;
@Inject
- ComplicationTypesUpdater(Context context,
+ ComplicationTypesUpdater(
DreamBackend dreamBackend,
@Main Executor executor,
SecureSettings secureSettings,
DreamOverlayStateController dreamOverlayStateController) {
- super(context);
-
mDreamBackend = dreamBackend;
mExecutor = executor;
mSecureSettings = secureSettings;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java
index 675a2f4..77e1fc9 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java
@@ -19,7 +19,6 @@
import static com.android.systemui.dreams.complication.dagger.DreamClockTimeComplicationModule.DREAM_CLOCK_TIME_COMPLICATION_VIEW;
import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS;
-import android.content.Context;
import android.view.View;
import com.android.systemui.CoreStartable;
@@ -61,7 +60,7 @@
* {@link CoreStartable} responsible for registering {@link DreamClockTimeComplication} with
* SystemUI.
*/
- public static class Registrant extends CoreStartable {
+ public static class Registrant implements CoreStartable {
private final DreamOverlayStateController mDreamOverlayStateController;
private final DreamClockTimeComplication mComplication;
@@ -69,10 +68,9 @@
* Default constructor to register {@link DreamClockTimeComplication}.
*/
@Inject
- public Registrant(Context context,
+ public Registrant(
DreamOverlayStateController dreamOverlayStateController,
DreamClockTimeComplication dreamClockTimeComplication) {
- super(context);
mDreamOverlayStateController = dreamOverlayStateController;
mComplication = dreamClockTimeComplication;
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
index 821e13e..cedd850a 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
@@ -71,7 +71,7 @@
/**
* {@link CoreStartable} for registering the complication with SystemUI on startup.
*/
- public static class Registrant extends CoreStartable {
+ public static class Registrant implements CoreStartable {
private final DreamHomeControlsComplication mComplication;
private final DreamOverlayStateController mDreamOverlayStateController;
private final ControlsComponent mControlsComponent;
@@ -90,11 +90,9 @@
};
@Inject
- public Registrant(Context context, DreamHomeControlsComplication complication,
+ public Registrant(DreamHomeControlsComplication complication,
DreamOverlayStateController dreamOverlayStateController,
ControlsComponent controlsComponent) {
- super(context);
-
mComplication = complication;
mControlsComponent = controlsComponent;
mDreamOverlayStateController = dreamOverlayStateController;
@@ -212,7 +210,8 @@
final Intent intent = new Intent(mContext, ControlsActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(ControlsUiController.EXTRA_ANIMATE, true);
+ .putExtra(ControlsUiController.EXTRA_ANIMATE, true)
+ .putExtra(ControlsUiController.EXIT_TO_DREAM, true);
final ActivityLaunchAnimator.Controller controller =
v != null ? ActivityLaunchAnimator.Controller.fromView(v, null /* cujType */)
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
index 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/dreams/complication/SmartSpaceComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java
index a981f25..c3aaf0c 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java
@@ -61,7 +61,7 @@
* {@link CoreStartable} responsbile for registering {@link SmartSpaceComplication} with
* SystemUI.
*/
- public static class Registrant extends CoreStartable {
+ public static class Registrant implements CoreStartable {
private final DreamSmartspaceController mSmartSpaceController;
private final DreamOverlayStateController mDreamOverlayStateController;
private final SmartSpaceComplication mComplication;
@@ -78,11 +78,10 @@
* Default constructor for {@link SmartSpaceComplication}.
*/
@Inject
- public Registrant(Context context,
+ public Registrant(
DreamOverlayStateController dreamOverlayStateController,
SmartSpaceComplication smartSpaceComplication,
DreamSmartspaceController smartSpaceController) {
- super(context);
mDreamOverlayStateController = dreamOverlayStateController;
mComplication = smartSpaceComplication;
mSmartSpaceController = smartSpaceController;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
index f769a23..0dba4ff 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
@@ -36,11 +36,11 @@
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.KeyguardBouncer;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
import com.android.wm.shell.animation.FlingAnimationUtils;
import java.util.Optional;
@@ -154,8 +154,8 @@
private void setPanelExpansion(float expansion, float dragDownAmount) {
mCurrentExpansion = expansion;
- PanelExpansionChangeEvent event =
- new PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent event =
+ new ShadeExpansionChangeEvent(
/* fraction= */ mCurrentExpansion,
/* expanded= */ false,
/* tracking= */ true,
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
index 08ef8f3..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.log.LogBuffer
+import com.android.systemui.dump.nano.SystemUIProtoDump
+import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
+import com.google.protobuf.nano.MessageNano
+import java.io.BufferedOutputStream
+import java.io.FileDescriptor
+import java.io.FileOutputStream
import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Provider
@@ -100,7 +105,7 @@
/**
* 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,
@@ -235,6 +268,7 @@
pw.println("$ <invocation> buffers")
pw.println("$ <invocation> bugreport-critical")
pw.println("$ <invocation> bugreport-normal")
+ pw.println("$ <invocation> config")
pw.println()
pw.println("Targets can be listed:")
@@ -266,6 +300,7 @@
}
}
}
+ PROTO -> pArgs.proto = true
"-t", "--tail" -> {
pArgs.tailLength = readArgument(iterator, arg) {
it.toInt()
@@ -277,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")
}
@@ -313,13 +351,21 @@
const val PRIORITY_ARG_CRITICAL = "CRITICAL"
const val PRIORITY_ARG_HIGH = "HIGH"
const val PRIORITY_ARG_NORMAL = "NORMAL"
+ const val PROTO = "--proto"
}
}
private val PRIORITY_OPTIONS =
arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_HIGH, PRIORITY_ARG_NORMAL)
-private val COMMANDS = arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables")
+private val COMMANDS = arrayOf(
+ "bugreport-critical",
+ "bugreport-normal",
+ "buffers",
+ "dumpables",
+ "config",
+ "help"
+)
private class ParsedArgs(
val rawArgs: Array<String>,
@@ -329,6 +375,7 @@
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 cca04da..ae78089 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
@@ -18,7 +18,9 @@
import android.util.ArrayMap
import com.android.systemui.Dumpable
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.ProtoDumpable
+import com.android.systemui.dump.nano.SystemUIProtoDump
+import com.android.systemui.plugins.log.LogBuffer
import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Singleton
@@ -90,7 +92,7 @@
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/LogBufferEulogizer.kt b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
index 0eab1af..8299b13 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
@@ -19,7 +19,7 @@
import android.content.Context
import android.util.Log
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.util.io.Files
import com.android.systemui.util.time.SystemClock
import java.io.IOException
diff --git a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
index 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/statusbar/phone/panelstate/PanelStateListener.kt b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto
similarity index 61%
copy from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
copy to packages/SystemUI/src/com/android/systemui/dump/sysui.proto
index ca667dd..cd8c08a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -13,11 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+syntax = "proto3";
-package com.android.systemui.statusbar.phone.panelstate
+package com.android.systemui.dump;
-/** A listener interface to be notified of state change events for the notification panel. */
-fun interface PanelStateListener {
- /** Called when the panel's expansion state has changed. */
- fun onPanelStateChanged(@PanelState state: Int)
+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/FeatureFlagsDebugStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
index c0e3021..560dcbd 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
@@ -16,9 +16,7 @@
package com.android.systemui.flags
-import android.content.Context
import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.commandline.CommandRegistry
import dagger.Binds
@@ -30,12 +28,11 @@
class FeatureFlagsDebugStartable
@Inject
constructor(
- @Application context: Context,
dumpManager: DumpManager,
private val commandRegistry: CommandRegistry,
private val flagCommand: FlagCommand,
featureFlags: FeatureFlags
-) : CoreStartable(context) {
+) : CoreStartable {
init {
dumpManager.registerDumpable(FeatureFlagsDebug.TAG) { pw, args ->
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt
index f138f1e..e7d8cc3 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt
@@ -16,9 +16,7 @@
package com.android.systemui.flags
-import android.content.Context
import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dump.DumpManager
import dagger.Binds
import dagger.Module
@@ -28,8 +26,7 @@
class FeatureFlagsReleaseStartable
@Inject
-constructor(@Application context: Context, dumpManager: DumpManager, featureFlags: FeatureFlags) :
- CoreStartable(context) {
+constructor(dumpManager: DumpManager, featureFlags: FeatureFlags) : CoreStartable {
init {
dumpManager.registerDumpable(FeatureFlagsRelease.TAG) { pw, args ->
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 3868b6f..0000000
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ /dev/null
@@ -1,352 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.flags;
-
-import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER;
-
-import com.android.internal.annotations.Keep;
-import com.android.systemui.R;
-
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * List of {@link Flag} objects for use in SystemUI.
- *
- * Flag Ids are integers.
- * Ids must be unique. This is enforced in a unit test.
- * Ids need not be sequential. Flags can "claim" a chunk of ids for flags in related features with
- * a comment. This is purely for organizational purposes.
- *
- * On public release builds, flags will always return their default value. There is no way to
- * change their value on release builds.
- *
- * See {@link FeatureFlagsDebug} for instructions on flipping the flags via adb.
- */
-public class Flags {
- public static final UnreleasedFlag TEAMFOOD = new UnreleasedFlag(1);
-
- /***************************************/
- // 100 - notification
- public static final UnreleasedFlag NOTIFICATION_PIPELINE_DEVELOPER_LOGGING =
- new UnreleasedFlag(103);
-
- public static final UnreleasedFlag NSSL_DEBUG_LINES =
- new UnreleasedFlag(105);
-
- public static final UnreleasedFlag NSSL_DEBUG_REMOVE_ANIMATION =
- new UnreleasedFlag(106);
-
- public static final UnreleasedFlag NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE =
- new UnreleasedFlag(107);
-
- public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS =
- new ResourceBooleanFlag(108, R.bool.config_notificationToContents);
-
- public static final ReleasedFlag REMOVE_UNRANKED_NOTIFICATIONS =
- new ReleasedFlag(109);
-
- public static final UnreleasedFlag FSI_REQUIRES_KEYGUARD =
- new UnreleasedFlag(110, true);
-
- public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true);
-
- public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112,
- false);
-
- public static final UnreleasedFlag NOTIFICATION_DISMISSAL_FADE = new UnreleasedFlag(113, true);
-
- // next id: 114
-
- /***************************************/
- // 200 - keyguard/lockscreen
-
- // ** Flag retired **
- // public static final BooleanFlag KEYGUARD_LAYOUT =
- // new BooleanFlag(200, true);
-
- public static final ReleasedFlag LOCKSCREEN_ANIMATIONS =
- new ReleasedFlag(201);
-
- public static final ReleasedFlag NEW_UNLOCK_SWIPE_ANIMATION =
- new ReleasedFlag(202);
-
- public static final ResourceBooleanFlag CHARGING_RIPPLE =
- new ResourceBooleanFlag(203, R.bool.flag_charging_ripple);
-
- public static final ResourceBooleanFlag BOUNCER_USER_SWITCHER =
- new ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher);
-
- public static final ResourceBooleanFlag FACE_SCANNING_ANIM =
- new ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation);
-
- public static final UnreleasedFlag LOCKSCREEN_CUSTOM_CLOCKS = new UnreleasedFlag(207);
-
- /**
- * Flag to enable the usage of the new bouncer data source. This is a refactor of and
- * eventual replacement of KeyguardBouncer.java.
- */
- public static final UnreleasedFlag MODERN_BOUNCER = new UnreleasedFlag(208);
-
- /** Whether UserSwitcherActivity should use modern architecture. */
- public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY =
- new ReleasedFlag(209, true);
-
- /**
- * 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);
-
- // 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);
-
- // 1100 - windowing
- @Keep
- public static final SysPropBooleanFlag WM_ENABLE_SHELL_TRANSITIONS =
- new SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false);
-
- /**
- * b/170163464: animate bubbles expanded view collapse with home gesture
- */
- @Keep
- public static final SysPropBooleanFlag BUBBLES_HOME_GESTURE =
- new SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true);
-
- @Keep
- public static final DeviceConfigBooleanFlag WM_ENABLE_PARTIAL_SCREEN_SHARING =
- new DeviceConfigBooleanFlag(1102, "record_task_content",
- NAMESPACE_WINDOW_MANAGER, false, true);
-
- @Keep
- public static final SysPropBooleanFlag HIDE_NAVBAR_WINDOW =
- new SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false);
-
- @Keep
- public static final SysPropBooleanFlag WM_DESKTOP_WINDOWING =
- new SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false);
-
- @Keep
- public static final SysPropBooleanFlag WM_CAPTION_ON_SHELL =
- new SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false);
-
- @Keep
- public static final SysPropBooleanFlag FLOATING_TASKS_ENABLED =
- new SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false);
-
- @Keep
- public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES =
- new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false);
-
- @Keep
- public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_BUBBLE =
- new SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true);
- @Keep
- public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_PIP =
- new SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true);
-
- @Keep
- public static final SysPropBooleanFlag ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
- new SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false);
-
- // 1200 - predictive back
- @Keep
- public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
- 1200, "persist.wm.debug.predictive_back", true);
- @Keep
- public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK_ANIM = new SysPropBooleanFlag(
- 1201, "persist.wm.debug.predictive_back_anim", false);
- @Keep
- public static final SysPropBooleanFlag WM_ALWAYS_ENFORCE_PREDICTIVE_BACK =
- new SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false);
-
- public static final UnreleasedFlag NEW_BACK_AFFORDANCE =
- new UnreleasedFlag(1203, false /* teamfood */);
-
- // 1300 - screenshots
-
- public static final UnreleasedFlag SCREENSHOT_REQUEST_PROCESSOR = new UnreleasedFlag(1300);
- public static final UnreleasedFlag SCREENSHOT_WORK_PROFILE_POLICY = new UnreleasedFlag(1301);
-
- // 1400 - columbus
- public static final ReleasedFlag QUICK_TAP_IN_PCC = new ReleasedFlag(1400);
-
- // 1500 - chooser
- public static final UnreleasedFlag CHOOSER_UNBUNDLED = new UnreleasedFlag(1500);
-
- // Pay no attention to the reflection behind the curtain.
- // ========================== Curtain ==========================
- // | |
- // | . . . . . . . . . . . . . . . . . . . |
- private static Map<Integer, Flag<?>> sFlagMap;
- static Map<Integer, Flag<?>> collectFlags() {
- if (sFlagMap != null) {
- return sFlagMap;
- }
-
- Map<Integer, Flag<?>> flags = new HashMap<>();
- List<Field> flagFields = getFlagFields();
-
- for (Field field : flagFields) {
- try {
- Flag<?> flag = (Flag<?>) field.get(null);
- flags.put(flag.getId(), flag);
- } catch (IllegalAccessException e) {
- // no-op
- }
- }
-
- sFlagMap = flags;
-
- return sFlagMap;
- }
-
- static List<Field> getFlagFields() {
- Field[] fields = Flags.class.getFields();
- List<Field> result = new ArrayList<>();
-
- for (Field field : fields) {
- Class<?> t = field.getType();
- if (Flag.class.isAssignableFrom(t)) {
- result.add(field);
- }
- }
-
- return result;
- }
- // | . . . . . . . . . . . . . . . . . . . |
- // | |
- // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/
-
-}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
new file mode 100644
index 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/GlobalActionsComponent.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java
index 74d5bd5..9f321d8 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java
@@ -36,8 +36,7 @@
* Manages power menu plugins and communicates power menu actions to the CentralSurfaces.
*/
@SysUISingleton
-public class GlobalActionsComponent extends CoreStartable
- implements Callbacks, GlobalActionsManager {
+public class GlobalActionsComponent implements CoreStartable, Callbacks, GlobalActionsManager {
private final CommandQueue mCommandQueue;
private final ExtensionController mExtensionController;
@@ -48,11 +47,10 @@
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@Inject
- public GlobalActionsComponent(Context context, CommandQueue commandQueue,
+ public GlobalActionsComponent(CommandQueue commandQueue,
ExtensionController extensionController,
Provider<GlobalActions> globalActionsProvider,
StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
- super(context);
mCommandQueue = commandQueue;
mExtensionController = extensionController;
mGlobalActionsProvider = globalActionsProvider;
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/keyboard/KeyboardUI.java b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
index 568143c..4f1a2b3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
@@ -27,7 +27,6 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
-import android.content.res.Configuration;
import android.hardware.input.InputManager;
import android.os.Handler;
import android.os.HandlerThread;
@@ -66,7 +65,7 @@
/** */
@SysUISingleton
-public class KeyboardUI extends CoreStartable implements InputManager.OnTabletModeChangedListener {
+public class KeyboardUI implements CoreStartable, InputManager.OnTabletModeChangedListener {
private static final String TAG = "KeyboardUI";
private static final boolean DEBUG = false;
@@ -127,13 +126,12 @@
@Inject
public KeyboardUI(Context context, Provider<LocalBluetoothManager> bluetoothManagerProvider) {
- super(context);
+ mContext = context;
this.mBluetoothManagerProvider = bluetoothManagerProvider;
}
@Override
public void start() {
- mContext = super.mContext;
HandlerThread thread = new HandlerThread("Keyboard", Process.THREAD_PRIORITY_BACKGROUND);
thread.start();
mHandler = new KeyboardHandler(thread.getLooper());
@@ -141,10 +139,6 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
- }
-
- @Override
public void dump(PrintWriter pw, String[] args) {
pw.println("KeyboardUI:");
pw.println(" mEnabled=" + mEnabled);
@@ -156,7 +150,7 @@
}
@Override
- protected void onBootCompleted() {
+ public void onBootCompleted() {
mHandler.sendEmptyMessage(MSG_ON_BOOT_COMPLETED);
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index 5f96a3b..a908e94 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -666,7 +666,7 @@
return
}
- if (keyguardViewController.isShowing && !playingCannedUnlockAnimation) {
+ if (keyguardStateController.isShowing && !playingCannedUnlockAnimation) {
showOrHideSurfaceIfDismissAmountThresholdsReached()
// If the surface is visible or it's about to be, start updating its appearance to
@@ -726,7 +726,7 @@
private fun finishKeyguardExitRemoteAnimationIfReachThreshold() {
// no-op if keyguard is not showing or animation is not enabled.
if (!KeyguardService.sEnableRemoteKeyguardGoingAwayAnimation ||
- !keyguardViewController.isShowing) {
+ !keyguardStateController.isShowing) {
return
}
@@ -849,7 +849,7 @@
* animation.
*/
fun hideKeyguardViewAfterRemoteAnimation() {
- if (keyguardViewController.isShowing) {
+ if (keyguardStateController.isShowing) {
// Hide the keyguard, with no fade out since we animated it away during the unlock.
keyguardViewController.hide(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index da0b910..c8370b4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -124,6 +124,7 @@
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -134,7 +135,6 @@
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.util.DeviceConfigProxy;
@@ -186,7 +186,7 @@
* directly to the keyguard UI is posted to a {@link android.os.Handler} to ensure it is taken on the UI
* thread of the keyguard.
*/
-public class KeyguardViewMediator extends CoreStartable implements Dumpable,
+public class KeyguardViewMediator implements CoreStartable, Dumpable,
StatusBarStateController.StateListener {
private static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000;
private static final long KEYGUARD_DONE_PENDING_TIMEOUT_MS = 3000;
@@ -272,6 +272,7 @@
private boolean mShuttingDown;
private boolean mDozing;
private boolean mAnimatingScreenOff;
+ private final Context mContext;
private final FalsingCollector mFalsingCollector;
/** High level access to the power manager for WakeLocks */
@@ -400,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.
*/
@@ -793,6 +799,8 @@
KeyguardUpdateMonitor.StrongAuthTracker strongAuthTracker =
mUpdateMonitor.getStrongAuthTracker();
int strongAuth = strongAuthTracker.getStrongAuthForUser(currentUser);
+ boolean allowedNonStrongAfterIdleTimeout =
+ strongAuthTracker.isNonStrongBiometricAllowedAfterIdleTimeout(currentUser);
if (any && !strongAuthTracker.hasUserAuthenticatedSinceBoot()) {
return KeyguardSecurityView.PROMPT_REASON_RESTART;
@@ -811,6 +819,8 @@
} else if (any && (strongAuth
& STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT) != 0) {
return KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT;
+ } else if (any && !allowedNonStrongAfterIdleTimeout) {
+ return KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT;
}
return KeyguardSecurityView.PROMPT_REASON_NONE;
}
@@ -941,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 -> {
@@ -1128,7 +1137,7 @@
DreamOverlayStateController dreamOverlayStateController,
Lazy<NotificationShadeWindowController> notificationShadeWindowControllerLazy,
Lazy<ActivityLaunchAnimator> activityLaunchAnimator) {
- super(context);
+ mContext = context;
mFalsingCollector = falsingCollector;
mLockPatternUtils = lockPatternUtils;
mBroadcastDispatcher = broadcastDispatcher;
@@ -1174,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() {
@@ -1881,7 +1893,7 @@
// if the keyguard is already showing, don't bother. check flags in both files
// to account for the hiding animation which results in a delay and discrepancy
// between flags
- if (mShowing && mKeyguardViewControllerLazy.get().isShowing()) {
+ if (mShowing && mKeyguardStateController.isShowing()) {
if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing");
resetStateLocked();
return;
@@ -2972,14 +2984,14 @@
*/
public KeyguardViewController registerCentralSurfaces(CentralSurfaces centralSurfaces,
NotificationPanelViewController panelView,
- @Nullable PanelExpansionStateManager panelExpansionStateManager,
+ @Nullable ShadeExpansionStateManager shadeExpansionStateManager,
BiometricUnlockController biometricUnlockController,
View notificationContainer, KeyguardBypassController bypassController) {
mCentralSurfaces = centralSurfaces;
mKeyguardViewControllerLazy.get().registerCentralSurfaces(
centralSurfaces,
panelView,
- panelExpansionStateManager,
+ shadeExpansionStateManager,
biometricUnlockController,
notificationContainer,
bypassController);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 56f1ac4..56a1f1a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -43,6 +43,7 @@
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
+import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -72,6 +73,7 @@
FalsingModule.class,
KeyguardQuickAffordanceModule.class,
KeyguardRepositoryModule.class,
+ StartKeyguardTransitionModule.class,
})
public class KeyguardModule {
/**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 45b668e..b186ae0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -21,6 +21,7 @@
import com.android.systemui.common.shared.model.Position
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.doze.DozeHost
+import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.policy.KeyguardStateController
import javax.inject.Inject
@@ -85,6 +86,9 @@
*/
val dozeAmount: Flow<Float>
+ /** Observable for the [StatusBarState] */
+ val statusBarState: Flow<StatusBarState>
+
/**
* Returns `true` if the keyguard is showing; `false` otherwise.
*
@@ -185,6 +189,24 @@
return keyguardStateController.isShowing
}
+ override val statusBarState: Flow<StatusBarState> = conflatedCallbackFlow {
+ val callback =
+ object : StatusBarStateController.StateListener {
+ override fun onStateChanged(state: Int) {
+ trySendWithFailureLogging(statusBarStateIntToObject(state), TAG, "state")
+ }
+ }
+
+ statusBarStateController.addCallback(callback)
+ trySendWithFailureLogging(
+ statusBarStateIntToObject(statusBarStateController.getState()),
+ TAG,
+ "initial state"
+ )
+
+ awaitClose { statusBarStateController.removeCallback(callback) }
+ }
+
override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.value = animate
}
@@ -197,6 +219,15 @@
_clockPosition.value = Position(x, y)
}
+ private fun statusBarStateIntToObject(value: Int): StatusBarState {
+ return when (value) {
+ 0 -> StatusBarState.SHADE
+ 1 -> StatusBarState.KEYGUARD
+ 2 -> StatusBarState.SHADE_LOCKED
+ else -> throw IllegalArgumentException("Invalid StatusBarState value: $value")
+ }
+ }
+
companion object {
private const val TAG = "KeyguardRepositoryImpl"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
new file mode 100644
index 0000000..e8532ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.data.repository
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
+import android.annotation.FloatRange
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filter
+
+@SysUISingleton
+class KeyguardTransitionRepository @Inject constructor() {
+ /*
+ * Each transition between [KeyguardState]s will have an associated Flow.
+ * In order to collect these events, clients should call [transition].
+ */
+ private val _transitions = MutableStateFlow(TransitionStep())
+ val transitions = _transitions.asStateFlow()
+
+ /* Information about the active transition. */
+ private var currentTransitionInfo: TransitionInfo? = null
+ /*
+ * When manual control of the transition is requested, a unique [UUID] is used as the handle
+ * to permit calls to [updateTransition]
+ */
+ private var updateTransitionId: UUID? = null
+
+ /**
+ * Interactors that require information about changes between [KeyguardState]s will call this to
+ * register themselves for flowable [TransitionStep]s when that transition occurs.
+ */
+ fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> {
+ return transitions.filter { step -> step.from == from && step.to == to }
+ }
+
+ /**
+ * Begin a transition from one state to another. The [info.from] must match
+ * [currentTransitionInfo.to], or the request will be denied. This is enforced to avoid
+ * unplanned transitions.
+ */
+ fun startTransition(info: TransitionInfo): UUID? {
+ if (currentTransitionInfo != null) {
+ // Open questions:
+ // * Queue of transitions? buffer of 1?
+ // * Are transitions cancellable if a new one is triggered?
+ // * What validation does this need to do?
+ Log.wtf(TAG, "Transition still active: $currentTransitionInfo")
+ return null
+ }
+ currentTransitionInfo?.animator?.cancel()
+
+ currentTransitionInfo = info
+ info.animator?.let { animator ->
+ // An animator was provided, so use it to run the transition
+ animator.setFloatValues(0f, 1f)
+ val updateListener =
+ object : AnimatorUpdateListener {
+ override fun onAnimationUpdate(animation: ValueAnimator) {
+ emitTransition(
+ info,
+ (animation.getAnimatedValue() as Float),
+ TransitionState.RUNNING
+ )
+ }
+ }
+ val adapter =
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ Log.i(TAG, "Starting transition: $info")
+ emitTransition(info, 0f, TransitionState.STARTED)
+ }
+ override fun onAnimationCancel(animation: Animator) {
+ Log.i(TAG, "Cancelling transition: $info")
+ }
+ override fun onAnimationEnd(animation: Animator) {
+ Log.i(TAG, "Ending transition: $info")
+ emitTransition(info, 1f, TransitionState.FINISHED)
+ animator.removeListener(this)
+ animator.removeUpdateListener(updateListener)
+ }
+ }
+ animator.addListener(adapter)
+ animator.addUpdateListener(updateListener)
+ animator.start()
+ return@startTransition null
+ }
+ ?: run {
+ Log.i(TAG, "Starting transition (manual): $info")
+ emitTransition(info, 0f, TransitionState.STARTED)
+
+ // No animator, so it's manual. Provide a mechanism to callback
+ updateTransitionId = UUID.randomUUID()
+ return@startTransition updateTransitionId
+ }
+ }
+
+ /**
+ * Allows manual control of a transition. When calling [startTransition], the consumer must pass
+ * in a null animator. In return, it will get a unique [UUID] that will be validated to allow
+ * further updates.
+ *
+ * When the transition is over, TransitionState.FINISHED must be passed into the [state]
+ * parameter.
+ */
+ fun updateTransition(
+ transitionId: UUID,
+ @FloatRange(from = 0.0, to = 1.0) value: Float,
+ state: TransitionState
+ ) {
+ if (updateTransitionId != transitionId) {
+ Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
+ return
+ }
+
+ if (currentTransitionInfo == null) {
+ Log.wtf(TAG, "Attempting to update with null 'currentTransitionInfo'")
+ return
+ }
+
+ currentTransitionInfo?.let { info ->
+ if (state == TransitionState.FINISHED) {
+ updateTransitionId = null
+ Log.i(TAG, "Ending transition: $info")
+ }
+
+ emitTransition(info, value, state)
+ }
+ }
+
+ private fun emitTransition(
+ info: TransitionInfo,
+ value: Float,
+ transitionState: TransitionState
+ ) {
+ if (transitionState == TransitionState.FINISHED) {
+ currentTransitionInfo = null
+ }
+ _transitions.value = TransitionStep(info.from, info.to, value, transitionState)
+ }
+
+ companion object {
+ private const val TAG = "KeyguardTransitionRepository"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
new file mode 100644
index 0000000..4003766
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class AodLockscreenTransitionInteractor
+@Inject
+constructor(
+ @Application private val scope: CoroutineScope,
+ private val keyguardRepository: KeyguardRepository,
+ private val keyguardTransitionRepository: KeyguardTransitionRepository,
+) : TransitionInteractor("AOD<->LOCKSCREEN") {
+
+ override fun start() {
+ scope.launch {
+ keyguardRepository.isDozing.collect { isDozing ->
+ if (isDozing) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.AOD,
+ getAnimator(),
+ )
+ )
+ } else {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.AOD,
+ KeyguardState.LOCKSCREEN,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun getAnimator(): ValueAnimator {
+ return ValueAnimator().apply {
+ setInterpolator(Interpolators.LINEAR)
+ setDuration(TRANSITION_DURATION_MS)
+ }
+ }
+
+ companion object {
+ private const val TRANSITION_DURATION_MS = 500L
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
index 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/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 192919e..fc2269c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -38,7 +38,7 @@
val dozeAmount: Flow<Float> = repository.dozeAmount
/** Whether the system is in doze mode. */
val isDozing: Flow<Boolean> = repository.isDozing
- /** Whether the keyguard is showing ot not. */
+ /** Whether the keyguard is showing to not. */
val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
fun isKeyguardShowing(): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 01cd3e2..f663b0d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -19,7 +19,7 @@
import android.content.Intent
import com.android.internal.widget.LockPatternUtils
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
@@ -67,19 +67,19 @@
*
* @param configKey The configuration key corresponding to the [KeyguardQuickAffordanceModel] of
* the affordance that was clicked
- * @param animationController An optional controller for the activity-launch animation
+ * @param expandable An optional [Expandable] for the activity- or dialog-launch animation
*/
fun onQuickAffordanceClicked(
configKey: KClass<out KeyguardQuickAffordanceConfig>,
- animationController: ActivityLaunchAnimator.Controller?,
+ expandable: Expandable?,
) {
@Suppress("UNCHECKED_CAST") val config = registry.get(configKey as KClass<Nothing>)
- when (val result = config.onQuickAffordanceClicked(animationController)) {
+ when (val result = config.onQuickAffordanceClicked(expandable)) {
is KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity ->
launchQuickAffordance(
intent = result.intent,
canShowWhileLocked = result.canShowWhileLocked,
- animationController = animationController
+ expandable = expandable,
)
is KeyguardQuickAffordanceConfig.OnClickedResult.Handled -> Unit
}
@@ -104,6 +104,7 @@
KeyguardQuickAffordanceModel.Visible(
configKey = configs[index]::class,
icon = visibleState.icon,
+ toggle = visibleState.toggle,
)
} else {
KeyguardQuickAffordanceModel.Hidden
@@ -114,7 +115,7 @@
private fun launchQuickAffordance(
intent: Intent,
canShowWhileLocked: Boolean,
- animationController: ActivityLaunchAnimator.Controller?,
+ expandable: Expandable?,
) {
@LockPatternUtils.StrongAuthTracker.StrongAuthFlags
val strongAuthFlags =
@@ -130,13 +131,13 @@
activityStarter.postStartActivityDismissingKeyguard(
intent,
0 /* delay */,
- animationController
+ expandable?.activityLaunchController(),
)
} else {
activityStarter.startActivity(
intent,
true /* dismissShade */,
- animationController,
+ expandable?.activityLaunchController(),
true /* showOverLockscreenWhenLocked */,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
new file mode 100644
index 0000000..b166681
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.domain.interactor
+
+import android.util.Log
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import java.util.Set
+import javax.inject.Inject
+
+@SysUISingleton
+class KeyguardTransitionCoreStartable
+@Inject
+constructor(
+ private val interactors: Set<TransitionInteractor>,
+) : CoreStartable {
+
+ override fun start() {
+ // By listing the interactors in a when, the compiler will help enforce all classes
+ // extending the sealed class [TransitionInteractor] will be initialized.
+ interactors.forEach {
+ // `when` needs to be an expression in order for the compiler to enforce it being
+ // exhaustive
+ val ret =
+ when (it) {
+ is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
+ is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
+ }
+ it.start()
+ }
+ }
+
+ companion object {
+ private const val TAG = "KeyguardTransitionCoreStartable"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
new file mode 100644
index 0000000..7409aec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+
+/** Encapsulates business-logic related to the keyguard transitions. */
+@SysUISingleton
+class KeyguardTransitionInteractor
+@Inject
+constructor(
+ repository: KeyguardTransitionRepository,
+) {
+ /** AOD->LOCKSCREEN transition information. */
+ val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN)
+
+ /** LOCKSCREEN->AOD transition information. */
+ val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
+
+ /**
+ * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <->
+ * Lockscreen (0f).
+ */
+ val dozeAmountTransition: Flow<TransitionStep> =
+ merge(
+ aodToLockscreenTransition.map { step -> step.copy(value = 1f - step.value) },
+ lockscreenToAodTransition,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
new file mode 100644
index 0000000..3c2a12e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.util.kotlin.sample
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class LockscreenBouncerTransitionInteractor
+@Inject
+constructor(
+ @Application private val scope: CoroutineScope,
+ private val keyguardRepository: KeyguardRepository,
+ private val shadeRepository: ShadeRepository,
+ private val keyguardTransitionRepository: KeyguardTransitionRepository,
+) : TransitionInteractor("LOCKSCREEN<->BOUNCER") {
+
+ private var transitionId: UUID? = null
+
+ override fun start() {
+ scope.launch {
+ shadeRepository.shadeModel.sample(
+ combine(
+ keyguardTransitionRepository.transitions,
+ keyguardRepository.statusBarState,
+ ) { transitions, statusBarState ->
+ Pair(transitions, statusBarState)
+ }
+ ) { shadeModel, pair ->
+ val (transitions, statusBarState) = pair
+
+ val id = transitionId
+ if (id != null) {
+ // An existing `id` means a transition is started, and calls to
+ // `updateTransition` will control it until FINISHED
+ keyguardTransitionRepository.updateTransition(
+ id,
+ shadeModel.expansionAmount,
+ if (shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f) {
+ transitionId = null
+ TransitionState.FINISHED
+ } else {
+ TransitionState.RUNNING
+ }
+ )
+ } else {
+ // TODO (b/251849525): Remove statusbarstate check when that state is integrated
+ // into KeyguardTransitionRepository
+ val isOnLockscreen =
+ transitions.transitionState == TransitionState.FINISHED &&
+ transitions.to == KeyguardState.LOCKSCREEN
+ if (
+ isOnLockscreen &&
+ shadeModel.isUserDragging &&
+ statusBarState != SHADE_LOCKED
+ ) {
+ transitionId =
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ ownerName = name,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.BOUNCER,
+ animator = null,
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
new file mode 100644
index 0000000..74c542c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.CoreStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
+
+@Module
+abstract class StartKeyguardTransitionModule {
+
+ @Binds
+ @IntoMap
+ @ClassKey(KeyguardTransitionCoreStartable::class)
+ abstract fun bind(impl: KeyguardTransitionCoreStartable): CoreStartable
+
+ @Binds
+ @IntoSet
+ abstract fun lockscreenBouncer(
+ impl: LockscreenBouncerTransitionInteractor
+ ): TransitionInteractor
+
+ @Binds
+ @IntoSet
+ abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
new file mode 100644
index 0000000..a2a46d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+/**
+ * Each TransitionInteractor is responsible for determining under which conditions to notify
+ * [KeyguardTransitionRepository] to signal a transition. When (and if) the transition occurs is
+ * determined by [KeyguardTransitionRepository].
+ *
+ * [name] field should be a unique identifiable string representing this state, used primarily for
+ * logging
+ *
+ * MUST list implementing classes in dagger module [StartKeyguardTransitionModule] and also in the
+ * 'when' clause of [KeyguardTransitionCoreStartable]
+ */
+sealed class TransitionInteractor(val name: String) {
+
+ abstract fun start()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
index eb6bb92..e56b259 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
@@ -19,6 +19,7 @@
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import kotlin.reflect.KClass
/**
@@ -35,5 +36,7 @@
val configKey: KClass<out KeyguardQuickAffordanceConfig>,
/** An icon for the affordance. */
val icon: Icon,
+ /** The toggle state for the affordance. */
+ val toggle: KeyguardQuickAffordanceToggleState,
) : KeyguardQuickAffordanceModel()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index 89604f0..8384260 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -20,7 +20,7 @@
import android.content.Context
import android.content.Intent
import androidx.annotation.DrawableRes
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
@@ -61,7 +61,7 @@
}
override fun onQuickAffordanceClicked(
- animationController: ActivityLaunchAnimator.Controller?,
+ expandable: Expandable?,
): KeyguardQuickAffordanceConfig.OnClickedResult {
return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
intent =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt
index 8e1c6b7..95027d0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt
@@ -18,8 +18,9 @@
package com.android.systemui.keyguard.domain.quickaffordance
import android.content.Intent
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import kotlinx.coroutines.flow.Flow
/** Defines interface that can act as data source for a single quick affordance model. */
@@ -27,9 +28,7 @@
val state: Flow<State>
- fun onQuickAffordanceClicked(
- animationController: ActivityLaunchAnimator.Controller?
- ): OnClickedResult
+ fun onQuickAffordanceClicked(expandable: Expandable?): OnClickedResult
/**
* Encapsulates the state of a "quick affordance" in the keyguard bottom area (for example, a
@@ -44,6 +43,9 @@
data class Visible(
/** An icon for the affordance. */
val icon: Icon,
+ /** The toggle state for the affordance. */
+ val toggle: KeyguardQuickAffordanceToggleState =
+ KeyguardQuickAffordanceToggleState.NotSupported,
) : State()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
index d97deaf..502a607 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
@@ -18,7 +18,7 @@
package com.android.systemui.keyguard.domain.quickaffordance
import com.android.systemui.R
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
@@ -66,7 +66,7 @@
}
override fun onQuickAffordanceClicked(
- animationController: ActivityLaunchAnimator.Controller?,
+ expandable: Expandable?,
): KeyguardQuickAffordanceConfig.OnClickedResult {
return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
intent = controller.intent,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
index 9196b09..a24a0d6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -23,7 +23,7 @@
import android.service.quickaccesswallet.QuickAccessWalletClient
import android.util.Log
import com.android.systemui.R
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
@@ -84,11 +84,11 @@
}
override fun onQuickAffordanceClicked(
- animationController: ActivityLaunchAnimator.Controller?,
+ expandable: Expandable?,
): KeyguardQuickAffordanceConfig.OnClickedResult {
walletController.startQuickAccessUiIntent(
activityStarter,
- animationController,
+ expandable?.activityLaunchController(),
/* hasCard= */ true,
)
return KeyguardQuickAffordanceConfig.OnClickedResult.Handled
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
new file mode 100644
index 0000000..f66d5d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+/** List of all possible states to transition to/from */
+enum class KeyguardState {
+ /** For initialization only */
+ NONE,
+ /* Always-on Display. The device is in a low-power mode with a minimal UI visible */
+ AOD,
+ /*
+ * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS
+ * (Fingerprint Sensor) variations, for the user to verify their credentials
+ */
+ BOUNCER,
+ /*
+ * Device is actively displaying keyguard UI and is not in low-power mode. Device may be
+ * unlocked if SWIPE security method is used, or if face lockscreen bypass is false.
+ */
+ LOCKSCREEN,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt
new file mode 100644
index 0000000..bb95347
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+/** See [com.android.systemui.statusbar.StatusBarState] for definitions */
+enum class StatusBarState {
+ SHADE,
+ KEYGUARD,
+ SHADE_LOCKED,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt
new file mode 100644
index 0000000..bfccf3fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+import android.animation.ValueAnimator
+
+/** Tracks who is controlling the current transition, and how to run it. */
+data class TransitionInfo(
+ val ownerName: String,
+ val from: KeyguardState,
+ val to: KeyguardState,
+ val animator: ValueAnimator?, // 'null' animator signal manual control
+) {
+ override fun toString(): String =
+ "TransitionInfo(ownerName=$ownerName, from=$from, to=$to, " +
+ (if (animator != null) {
+ "animated"
+ } else {
+ "manual"
+ }) +
+ ")"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt
new file mode 100644
index 0000000..d8691c1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+/** Possible states for a running transition between [State] */
+enum class TransitionState {
+ NONE,
+ STARTED,
+ RUNNING,
+ FINISHED
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
new file mode 100644
index 0000000..688ec91
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+/** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */
+data class TransitionStep(
+ val from: KeyguardState = KeyguardState.NONE,
+ val to: KeyguardState = KeyguardState.NONE,
+ val value: Float = 0f, // constrained [0.0, 1.0]
+ val transitionState: TransitionState = TransitionState.NONE,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt
new file mode 100644
index 0000000..55d38a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.shared.quickaffordance
+
+/** Enumerates all possible toggle states for a quick affordance on the lock-screen. */
+sealed class KeyguardQuickAffordanceToggleState {
+ /** Toggling is not supported. */
+ object NotSupported : KeyguardQuickAffordanceToggleState()
+ /** The quick affordance is on. */
+ object On : KeyguardQuickAffordanceToggleState()
+ /** The quick affordance is off. */
+ object Off : KeyguardQuickAffordanceToggleState()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index 65b85c0..2c99ca5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -29,7 +29,7 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.settingslib.Utils
import com.android.systemui.R
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.animation.Interpolators
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
@@ -238,14 +238,26 @@
IconViewBinder.bind(viewModel.icon, view)
+ view.isActivated = viewModel.isActivated
view.drawable.setTint(
Utils.getColorAttrDefaultColor(
view.context,
- com.android.internal.R.attr.textColorPrimary
+ if (viewModel.isActivated) {
+ com.android.internal.R.attr.textColorPrimaryInverse
+ } else {
+ com.android.internal.R.attr.textColorPrimary
+ },
)
)
view.backgroundTintList =
- Utils.getColorAttr(view.context, com.android.internal.R.attr.colorSurface)
+ Utils.getColorAttr(
+ view.context,
+ if (viewModel.isActivated) {
+ com.android.internal.R.attr.colorAccentPrimary
+ } else {
+ com.android.internal.R.attr.colorSurface
+ }
+ )
view.isClickable = viewModel.isClickable
if (viewModel.isClickable) {
@@ -268,7 +280,7 @@
viewModel.onClicked(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = viewModel.configKey,
- animationController = ActivityLaunchAnimator.Controller.fromView(view),
+ expandable = Expandable.fromView(view),
)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index 970ee4c..535ca72 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -23,6 +23,7 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -119,10 +120,11 @@
onClicked = { parameters ->
quickAffordanceInteractor.onQuickAffordanceClicked(
configKey = parameters.configKey,
- animationController = parameters.animationController,
+ expandable = parameters.expandable,
)
},
isClickable = isClickable,
+ isActivated = toggle is KeyguardQuickAffordanceToggleState.On,
)
is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
index 0971f13..bf598ba 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -16,7 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
import kotlin.reflect.KClass
@@ -30,9 +30,10 @@
val icon: Icon = Icon.Resource(res = 0, contentDescription = null),
val onClicked: (OnClickedParameters) -> Unit = {},
val isClickable: Boolean = false,
+ val isActivated: Boolean = false,
) {
data class OnClickedParameters(
val configKey: KClass<out KeyguardQuickAffordanceConfig>,
- val animationController: ActivityLaunchAnimator.Controller?,
+ val expandable: Expandable?,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
index 5651399..f9e341c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
@@ -19,6 +19,9 @@
import android.app.ActivityManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
+
import javax.inject.Inject
@SysUISingleton
@@ -26,7 +29,7 @@
private val dumpManager: DumpManager,
private val logcatEchoTracker: LogcatEchoTracker
) {
- /* limit the size of maxPoolSize for low ram (Go) devices */
+ /* limitiometricMessageDeferralLogger the size of maxPoolSize for low ram (Go) devices */
private fun adjustMaxSize(requestedMaxSize: Int): Int {
return if (ActivityManager.isLowRamDeviceStatic()) {
minOf(requestedMaxSize, 20) /* low ram max log size*/
diff --git a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
index 8f9357a..c7e4c5e 100644
--- a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
+++ b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
@@ -21,7 +21,6 @@
import static android.app.StatusBarManager.SESSION_KEYGUARD;
import android.annotation.Nullable;
-import android.content.Context;
import android.os.RemoteException;
import android.util.Log;
@@ -48,7 +47,7 @@
* session. Can be used across processes via StatusBarManagerService#registerSessionListener
*/
@SysUISingleton
-public class SessionTracker extends CoreStartable {
+public class SessionTracker implements CoreStartable {
private static final String TAG = "SessionTracker";
private static final boolean DEBUG = false;
@@ -65,13 +64,11 @@
@Inject
public SessionTracker(
- Context context,
IStatusBarService statusBarService,
AuthController authController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
KeyguardStateController keyguardStateController
) {
- super(context);
mStatusBarManagerService = statusBarService;
mAuthController = authController;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
index 7f1ad6d..eeadf40 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
@@ -23,7 +23,7 @@
import javax.inject.Qualifier;
/**
- * A {@link com.android.systemui.log.LogBuffer} for BiometricMessages processing such as
+ * A {@link com.android.systemui.plugins.log.LogBuffer} for BiometricMessages processing such as
* {@link com.android.systemui.biometrics.FaceHelpMessageDeferral}
*/
@Qualifier
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java
index 7d1f1c2..5cca1ab 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
index 9ca0293..1d016d8 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java
index 7c5f402..c9f78bc 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
copy to packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
index ca667dd..0645236 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,10 +14,12 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone.panelstate
+package com.android.systemui.log.dagger
-/** A listener interface to be notified of state change events for the notification panel. */
-fun interface PanelStateListener {
- /** Called when the panel's expansion state has changed. */
- fun onPanelStateChanged(@PanelState state: Int)
-}
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.plugins.log.LogBuffer] for keyguard clock logs. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class KeyguardClockLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java
index 08d969b..76d20be 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 529d066..9adb855 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -22,11 +22,11 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.log.LogBuffer;
import com.android.systemui.log.LogBufferFactory;
-import com.android.systemui.log.LogcatEchoTracker;
-import com.android.systemui.log.LogcatEchoTrackerDebug;
-import com.android.systemui.log.LogcatEchoTrackerProd;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogcatEchoTracker;
+import com.android.systemui.plugins.log.LogcatEchoTrackerDebug;
+import com.android.systemui.plugins.log.LogcatEchoTrackerProd;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.util.Compile;
@@ -43,7 +43,7 @@
@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
@@ -344,4 +354,14 @@
public static LogBuffer provideKeyguardLogBuffer(LogBufferFactory factory) {
return factory.create("KeyguardLog", 250);
}
+
+ /**
+ * Provides a {@link LogBuffer} for Udfps logs.
+ */
+ @Provides
+ @SysUISingleton
+ @UdfpsLog
+ public static LogBuffer provideUdfpsLogBuffer(LogBufferFactory factory) {
+ return factory.create("UdfpsLog", 1000);
+ }
}
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 1d7ba94..af43347 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -26,7 +26,7 @@
import 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 b03655a..f4dac6e 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -26,7 +26,7 @@
import javax.inject.Qualifier;
/**
- * A {@link LogBuffer} for {@link com.android.systemui.media.MediaCarouselController}
+ * A {@link LogBuffer} for {@link com.android.systemui.media.controls.ui.MediaCarouselController}
*/
@Qualifier
@Documented
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java
index c67d8be..73690ab 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
index 53963fc..0c2cd92 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -26,7 +26,7 @@
import javax.inject.Qualifier;
/**
- * A {@link LogBuffer} for {@link com.android.systemui.media.MediaTimeoutLogger}
+ * A {@link LogBuffer} for {@link com.android.systemui.media.controls.pipeline.MediaTimeoutLogger}
*/
@Qualifier
@Documented
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
index 5c572e8..1570d43 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
index edab8c3..bf216c6 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
index 75a34fc..5b7f4bb 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -26,7 +26,7 @@
import javax.inject.Qualifier;
/**
- * A {@link LogBuffer} for {@link com.android.systemui.media.MediaViewLogger}
+ * A {@link LogBuffer} for {@link com.android.systemui.media.controls.ui.MediaViewLogger}
*/
@Qualifier
@Documented
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java
index b1c6dcf..6d91f0c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
index 20fc6ff..26af496 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
index fcc184a..61daf9c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java
index 760fbf3..a59afa0 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java
index a0b6864..6f8ea7f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java
index 8c8753a..835d349 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java
index 7259eeb..6e2bd7b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java
index e96e532..77b1bf5 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
index 557a254..9fd166b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java
index dd5010c..dd168ba 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
index bd0d298..d24bfcb 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
index b237f2d..67cdb72 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java
index f26b316..af0f7c5 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
index dd68375..4c276e2 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java
index 8671dbf..ba8b27c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/UdfpsLog.java
similarity index 61%
copy from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
copy to packages/SystemUI/src/com/android/systemui/log/dagger/UdfpsLog.java
index ca667dd..14000e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/UdfpsLog.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,10 +14,17 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone.panelstate
+package com.android.systemui.log.dagger;
-/** A listener interface to be notified of state change events for the notification panel. */
-fun interface PanelStateListener {
- /** Called when the panel's expansion state has changed. */
- fun onPanelStateChanged(@PanelState state: Int)
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface UdfpsLog {
}
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 9dd18b2..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ /dev/null
@@ -1,1113 +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.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
- private 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
- private val pageIndicator: PageIndicator
- private val visualStabilityCallback: OnReorderingAllowedListener
- private var needsReordering: Boolean = false
- private var keysNeedRemoval = mutableSetOf<String>()
- var shouldScrollToActivePlayer: Boolean = false
- private var isRtl: Boolean = false
- set(value) {
- if (value != field) {
- field = value
- mediaFrame.layoutDirection =
- if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
- mediaCarouselScrollHandler.scrollToStart()
- }
- }
- private var currentlyExpanded = true
- set(value) {
- if (field != value) {
- field = value
- for (player in MediaPlayerData.players()) {
- player.setListening(field)
- }
- }
- }
- private val configListener = object : ConfigurationController.ConfigurationListener {
- override fun onDensityOrFontScaleChanged() {
- // System font changes should only happen when UMO is offscreen or a flicker may occur
- updatePlayers(recreateMedia = true)
- inflateSettingsButton()
- }
-
- override fun onThemeChanged() {
- updatePlayers(recreateMedia = false)
- inflateSettingsButton()
- }
-
- override fun onConfigChanged(newConfig: Configuration?) {
- if (newConfig == null) return
- isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL
- }
-
- override fun onUiModeChanged() {
- updatePlayers(recreateMedia = false)
- inflateSettingsButton()
- }
- }
-
- /**
- * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility.
- * It will be called when the container is out of view.
- */
- lateinit var updateUserVisibility: () -> Unit
- lateinit var updateHostVisibility: () -> Unit
-
- private val isReorderingAllowed: Boolean
- get() = visualStabilityProvider.isReorderingAllowed
-
- init {
- dumpManager.registerDumpable(TAG, this)
- mediaFrame = inflateMediaCarousel()
- mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
- pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
- mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
- executor, this::onSwipeToDismiss, this::updatePageIndicatorLocation,
- this::closeGuts, falsingCollector, falsingManager, this::logSmartspaceImpression,
- logger)
- isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
- inflateSettingsButton()
- mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
- configurationController.addCallback(configListener)
- visualStabilityCallback = OnReorderingAllowedListener {
- if (needsReordering) {
- needsReordering = false
- reorderAllPlayers(previousVisiblePlayerKey = null)
- }
-
- keysNeedRemoval.forEach {
- removePlayer(it)
- }
- if (keysNeedRemoval.size > 0) {
- // Carousel visibility may need to be updated after late removals
- updateHostVisibility()
- }
- keysNeedRemoval.clear()
-
- // Update user visibility so that no extra impression will be logged when
- // activeMediaIndex resets to 0
- if (this::updateUserVisibility.isInitialized) {
- updateUserVisibility()
- }
-
- // Let's reset our scroll position
- mediaCarouselScrollHandler.scrollToStart()
- }
- visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback)
- mediaManager.addListener(object : MediaDataManager.Listener {
- override fun onMediaDataLoaded(
- key: String,
- oldKey: String?,
- data: MediaData,
- immediately: Boolean,
- receivedSmartspaceCardLatency: Int,
- isSsReactivated: Boolean
- ) {
- debugLogger.logMediaLoaded(key)
- if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) {
- // Log card received if a new resumable media card is added
- MediaPlayerData.getMediaPlayer(key)?.let {
- /* ktlint-disable max-line-length */
- logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
- it.mSmartspaceId,
- it.mUid,
- surfaces = intArrayOf(
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY),
- rank = MediaPlayerData.getMediaPlayerIndex(key))
- /* ktlint-disable max-line-length */
- }
- if (mediaCarouselScrollHandler.visibleToUser &&
- mediaCarouselScrollHandler.visibleMediaIndex
- == MediaPlayerData.getMediaPlayerIndex(key)) {
- logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
- }
- } else if (receivedSmartspaceCardLatency != 0) {
- // Log resume card received if resumable media card is reactivated and
- // resume card is ranked first
- MediaPlayerData.players().forEachIndexed { index, it ->
- if (it.recommendationViewHolder == null) {
- it.mSmartspaceId = SmallHash.hash(it.mUid +
- systemClock.currentTimeMillis().toInt())
- it.mIsImpressed = false
- /* ktlint-disable max-line-length */
- logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
- it.mSmartspaceId,
- it.mUid,
- surfaces = intArrayOf(
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY),
- rank = index,
- receivedLatencyMillis = receivedSmartspaceCardLatency)
- /* ktlint-disable max-line-length */
- }
- }
- // If media container area already visible to the user, log impression for
- // reactivated card.
- if (mediaCarouselScrollHandler.visibleToUser &&
- !mediaCarouselScrollHandler.qsExpanded) {
- logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
- }
- }
-
- val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
- if (canRemove && !Utils.useMediaResumption(context)) {
- // This view isn't playing, let's remove this! This happens e.g when
- // dismissing/timing out a view. We still have the data around because
- // resumption could be on, but we should save the resources and release this.
- if (isReorderingAllowed) {
- onMediaDataRemoved(key)
- } else {
- keysNeedRemoval.add(key)
- }
- } else {
- keysNeedRemoval.remove(key)
- }
- }
-
- override fun onSmartspaceMediaDataLoaded(
- key: String,
- data: SmartspaceMediaData,
- shouldPrioritize: Boolean
- ) {
- debugLogger.logRecommendationLoaded(key)
- // Log the case where the hidden media carousel with the existed inactive resume
- // media is shown by the Smartspace signal.
- if (data.isActive) {
- val hasActivatedExistedResumeMedia =
- !mediaManager.hasActiveMedia() &&
- mediaManager.hasAnyMedia() &&
- shouldPrioritize
- if (hasActivatedExistedResumeMedia) {
- // Log resume card received if resumable media card is reactivated and
- // recommendation card is valid and ranked first
- MediaPlayerData.players().forEachIndexed { index, it ->
- if (it.recommendationViewHolder == null) {
- it.mSmartspaceId = SmallHash.hash(it.mUid +
- systemClock.currentTimeMillis().toInt())
- it.mIsImpressed = false
- /* ktlint-disable max-line-length */
- logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
- it.mSmartspaceId,
- it.mUid,
- surfaces = intArrayOf(
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY),
- rank = index,
- receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt())
- /* ktlint-disable max-line-length */
- }
- }
- }
- addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
- MediaPlayerData.getMediaPlayer(key)?.let {
- /* ktlint-disable max-line-length */
- logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
- it.mSmartspaceId,
- it.mUid,
- surfaces = intArrayOf(
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY),
- rank = MediaPlayerData.getMediaPlayerIndex(key),
- receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt())
- /* ktlint-disable max-line-length */
- }
- if (mediaCarouselScrollHandler.visibleToUser &&
- mediaCarouselScrollHandler.visibleMediaIndex
- == MediaPlayerData.getMediaPlayerIndex(key)) {
- logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
- }
- } else {
- onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
- }
- }
-
- override fun onMediaDataRemoved(key: String) {
- debugLogger.logMediaRemoved(key)
- removePlayer(key)
- }
-
- override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
- debugLogger.logRecommendationRemoved(key, immediately)
- if (immediately || isReorderingAllowed) {
- removePlayer(key)
- if (!immediately) {
- // Although it wasn't requested, we were able to process the removal
- // immediately since reordering is allowed. So, notify hosts to update
- if (this@MediaCarouselController::updateHostVisibility.isInitialized) {
- updateHostVisibility()
- }
- }
- } else {
- keysNeedRemoval.add(key)
- }
- }
- })
- mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
- // The pageIndicator is not laid out yet when we get the current state update,
- // Lets make sure we have the right dimensions
- updatePageIndicatorLocation()
- }
- mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback {
- override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
- if (location == desiredLocation) {
- onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false)
- }
- }
- })
- }
-
- private fun inflateSettingsButton() {
- val settings = LayoutInflater.from(context).inflate(R.layout.media_carousel_settings_button,
- mediaFrame, false) as View
- if (this::settingsButton.isInitialized) {
- mediaFrame.removeView(settingsButton)
- }
- settingsButton = settings
- mediaFrame.addView(settingsButton)
- mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
- settingsButton.setOnClickListener {
- logger.logCarouselSettings()
- activityStarter.startActivity(settingsIntent, true /* dismissShade */)
- }
- }
-
- private fun inflateMediaCarousel(): ViewGroup {
- val mediaCarousel = LayoutInflater.from(context).inflate(R.layout.media_carousel,
- UniqueObjectHostView(context), false) as ViewGroup
- // Because this is inflated when not attached to the true view hierarchy, it resolves some
- // potential issues to force that the layout direction is defined by the locale
- // (rather than inherited from the parent, which would resolve to LTR when unattached).
- mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
- return mediaCarousel
- }
-
- private fun reorderAllPlayers(previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?) {
- mediaContent.removeAllViews()
- for (mediaPlayer in MediaPlayerData.players()) {
- mediaPlayer.mediaViewHolder?.let {
- mediaContent.addView(it.player)
- } ?: mediaPlayer.recommendationViewHolder?.let {
- mediaContent.addView(it.recommendations)
- }
- }
- mediaCarouselScrollHandler.onPlayersChanged()
-
- // Automatically scroll to the active player if needed
- if (shouldScrollToActivePlayer) {
- shouldScrollToActivePlayer = false
- val activeMediaIndex = MediaPlayerData.firstActiveMediaIndex()
- if (activeMediaIndex != -1) {
- previousVisiblePlayerKey?.let {
- val previousVisibleIndex = MediaPlayerData.playerKeys()
- .indexOfFirst { key -> it == key }
- mediaCarouselScrollHandler
- .scrollToPlayer(previousVisibleIndex, activeMediaIndex)
- } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = activeMediaIndex)
- }
- }
- }
-
- // Returns true if new player is added
- private fun addOrUpdatePlayer(
- key: String,
- oldKey: String?,
- data: MediaData,
- isSsReactivated: Boolean
- ): Boolean = traceSection("MediaCarouselController#addOrUpdatePlayer") {
- MediaPlayerData.moveIfExists(oldKey, key)
- val existingPlayer = MediaPlayerData.getMediaPlayer(key)
- val curVisibleMediaKey = MediaPlayerData.playerKeys()
- .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
- val isCurVisibleMediaPlaying = curVisibleMediaKey?.data?.isPlaying
- if (existingPlayer == null) {
- val newPlayer = mediaControlPanelFactory.get()
- newPlayer.attachPlayer(MediaViewHolder.create(
- LayoutInflater.from(context), mediaContent))
- newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
- val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT)
- newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
- newPlayer.bindPlayer(data, key)
- newPlayer.setListening(currentlyExpanded)
- MediaPlayerData.addMediaPlayer(
- key, data, newPlayer, systemClock, isSsReactivated, debugLogger
- )
- updatePlayerToState(newPlayer, noAnimation = true)
- if (data.active) {
- reorderAllPlayers(curVisibleMediaKey)
- } else {
- needsReordering = true
- }
- } else {
- existingPlayer.bindPlayer(data, key)
- MediaPlayerData.addMediaPlayer(
- key, data, existingPlayer, systemClock, isSsReactivated, debugLogger
- )
- // Check the playing status of both current visible and new media players
- // To make sure we scroll to the active playing media card.
- if (isReorderingAllowed ||
- shouldScrollToActivePlayer &&
- data.isPlaying == true &&
- isCurVisibleMediaPlaying == false
- ) {
- reorderAllPlayers(curVisibleMediaKey)
- } else {
- needsReordering = true
- }
- }
- updatePageIndicator()
- mediaCarouselScrollHandler.onPlayersChanged()
- mediaFrame.requiresRemeasuring = true
- // Check postcondition: mediaContent should have the same number of children as there are
- // elements in mediaPlayers.
- if (MediaPlayerData.players().size != mediaContent.childCount) {
- Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync")
- }
- return existingPlayer == null
- }
-
- private fun addSmartspaceMediaRecommendations(
- key: String,
- data: SmartspaceMediaData,
- shouldPrioritize: Boolean
- ) = traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
- if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
- if (MediaPlayerData.getMediaPlayer(key) != null) {
- Log.w(TAG, "Skip adding smartspace target in carousel")
- return
- }
-
- val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey()
- existingSmartspaceMediaKey?.let {
- val removedPlayer = MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey)
- removedPlayer?.run { debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey) }
- }
-
- val newRecs = mediaControlPanelFactory.get()
- newRecs.attachRecommendation(
- RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent))
- newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
- val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT)
- newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp)
- newRecs.bindRecommendation(data)
- val curVisibleMediaKey = MediaPlayerData.playerKeys()
- .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
- MediaPlayerData.addMediaRecommendation(
- key, data, newRecs, shouldPrioritize, systemClock, debugLogger
- )
- updatePlayerToState(newRecs, noAnimation = true)
- reorderAllPlayers(curVisibleMediaKey)
- updatePageIndicator()
- mediaFrame.requiresRemeasuring = true
- // Check postcondition: mediaContent should have the same number of children as there are
- // elements in mediaPlayers.
- if (MediaPlayerData.players().size != mediaContent.childCount) {
- Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync")
- }
- }
-
- fun removePlayer(
- key: String,
- dismissMediaData: Boolean = true,
- dismissRecommendation: Boolean = true
- ) {
- if (key == MediaPlayerData.smartspaceMediaKey()) {
- MediaPlayerData.smartspaceMediaData?.let {
- logger.logRecommendationRemoved(it.packageName, it.instanceId)
- }
- }
- val removed = MediaPlayerData.removeMediaPlayer(key)
- removed?.apply {
- mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
- mediaContent.removeView(removed.mediaViewHolder?.player)
- mediaContent.removeView(removed.recommendationViewHolder?.recommendations)
- removed.onDestroy()
- mediaCarouselScrollHandler.onPlayersChanged()
- updatePageIndicator()
-
- if (dismissMediaData) {
- // Inform the media manager of a potentially late dismissal
- mediaManager.dismissMediaData(key, delay = 0L)
- }
- if (dismissRecommendation) {
- // Inform the media manager of a potentially late dismissal
- mediaManager.dismissSmartspaceRecommendation(key, delay = 0L)
- }
- }
- }
-
- private fun updatePlayers(recreateMedia: Boolean) {
- pageIndicator.tintList = ColorStateList.valueOf(
- context.getColor(R.color.media_paging_indicator)
- )
-
- MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) ->
- if (isSsMediaRec) {
- val smartspaceMediaData = MediaPlayerData.smartspaceMediaData
- removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
- smartspaceMediaData?.let {
- addSmartspaceMediaRecommendations(
- it.targetId, it, MediaPlayerData.shouldPrioritizeSs)
- }
- } else {
- val isSsReactivated = MediaPlayerData.isSsReactivated(key)
- if (recreateMedia) {
- removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
- }
- addOrUpdatePlayer(
- key = key, oldKey = null, data = data, isSsReactivated = isSsReactivated)
- }
- }
- }
-
- private fun updatePageIndicator() {
- val numPages = mediaContent.getChildCount()
- pageIndicator.setNumPages(numPages)
- if (numPages == 1) {
- pageIndicator.setLocation(0f)
- }
- updatePageIndicatorAlpha()
- }
-
- /**
- * Set a new interpolated state for all players. This is a state that is usually controlled
- * by a finger movement where the user drags from one state to the next.
- *
- * @param startLocation the start location of our state or -1 if this is directly set
- * @param endLocation the ending location of our state.
- * @param progress the progress of the transition between startLocation and endlocation. If
- * this is not a guided transformation, this will be 1.0f
- * @param immediately should this state be applied immediately, canceling all animations?
- */
- fun setCurrentState(
- @MediaLocation startLocation: Int,
- @MediaLocation endLocation: Int,
- progress: Float,
- immediately: Boolean
- ) {
- if (startLocation != currentStartLocation ||
- endLocation != currentEndLocation ||
- progress != currentTransitionProgress ||
- immediately
- ) {
- currentStartLocation = startLocation
- currentEndLocation = endLocation
- currentTransitionProgress = progress
- for (mediaPlayer in MediaPlayerData.players()) {
- updatePlayerToState(mediaPlayer, immediately)
- }
- maybeResetSettingsCog()
- updatePageIndicatorAlpha()
- }
- }
-
- private 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
- val endAlpha = if (endIsVisible) 1.0f else 0.0f
- 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()
- }
- }
-
- private fun maybeResetSettingsCog() {
- val hostStates = mediaHostStatesManager.mediaHostStates
- val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia
- ?: true
- val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia
- ?: endShowsActive
- if (currentlyShowingOnlyActive != endShowsActive ||
- ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
- startShowsActive != endShowsActive)) {
- // Whenever we're transitioning from between differing states or the endstate differs
- // we reset the translation
- currentlyShowingOnlyActive = endShowsActive
- mediaCarouselScrollHandler.resetTranslation(animate = true)
- }
- }
-
- private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) {
- mediaPlayer.mediaViewController.setCurrentState(
- startLocation = currentStartLocation,
- endLocation = currentEndLocation,
- transitionProgress = currentTransitionProgress,
- applyImmediately = noAnimation)
- }
-
- /**
- * The desired location of this view has changed. We should remeasure the view to match
- * the new bounds and kick off bounds animations if necessary.
- * If an animation is happening, an animation is kicked of externally, which sets a new
- * current state until we reach the targetState.
- *
- * @param desiredLocation the location we're going to
- * @param desiredHostState the target state we're transitioning to
- * @param animate should this be animated
- */
- fun onDesiredLocationChanged(
- desiredLocation: Int,
- desiredHostState: MediaHostState?,
- animate: Boolean,
- duration: Long = 200,
- startDelay: Long = 0
- ) = traceSection("MediaCarouselController#onDesiredLocationChanged") {
- desiredHostState?.let {
- if (this.desiredLocation != desiredLocation) {
- // Only log an event when location changes
- logger.logCarouselPosition(desiredLocation)
- }
-
- // This is a hosting view, let's remeasure our players
- this.desiredLocation = desiredLocation
- this.desiredHostState = it
- currentlyExpanded = it.expansion > 0
-
- val shouldCloseGuts = !currentlyExpanded &&
- !mediaManager.hasActiveMediaOrRecommendation() &&
- desiredHostState.showsOnlyActiveMedia
-
- for (mediaPlayer in MediaPlayerData.players()) {
- if (animate) {
- mediaPlayer.mediaViewController.animatePendingStateChange(
- duration = duration,
- delay = startDelay)
- }
- if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) {
- mediaPlayer.closeGuts(!animate)
- }
-
- mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
- }
- mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
- mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
- val nowVisible = it.visible
- if (nowVisible != playersVisible) {
- playersVisible = nowVisible
- if (nowVisible) {
- mediaCarouselScrollHandler.resetTranslation()
- }
- }
- updateCarouselSize()
- }
- }
-
- fun closeGuts(immediate: Boolean = true) {
- MediaPlayerData.players().forEach {
- it.closeGuts(immediate)
- }
- }
-
- /**
- * Update the size of the carousel, remeasuring it if necessary.
- */
- private fun updateCarouselSize() {
- val width = desiredHostState?.measurementInput?.width ?: 0
- val height = desiredHostState?.measurementInput?.height ?: 0
- if (width != carouselMeasureWidth && width != 0 ||
- height != carouselMeasureHeight && height != 0) {
- carouselMeasureWidth = width
- carouselMeasureHeight = height
- val playerWidthPlusPadding = carouselMeasureWidth +
- context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
- // Let's remeasure the carousel
- val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
- val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
- mediaCarousel.measure(widthSpec, heightSpec)
- mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight)
- // Update the padding after layout; view widths are used in RTL to calculate scrollX
- mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
- }
- }
-
- /**
- * Log the user impression for media card at visibleMediaIndex.
- */
- fun logSmartspaceImpression(qsExpanded: Boolean) {
- val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex
- if (MediaPlayerData.players().size > visibleMediaIndex) {
- val mediaControlPanel = MediaPlayerData.players().elementAt(visibleMediaIndex)
- val hasActiveMediaOrRecommendationCard =
- MediaPlayerData.hasActiveMediaOrRecommendationCard()
- if (!hasActiveMediaOrRecommendationCard && !qsExpanded) {
- // Skip logging if on LS or QQS, and there is no active media card
- return
- }
- logSmartspaceCardReported(800, // SMARTSPACE_CARD_SEEN
- mediaControlPanel.mSmartspaceId,
- mediaControlPanel.mUid,
- intArrayOf(mediaControlPanel.surfaceForSmartspaceLogging))
- mediaControlPanel.mIsImpressed = true
- }
- }
-
- @JvmOverloads
- /**
- * Log Smartspace events
- *
- * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN)
- * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new
- * instanceId
- * @param uid uid for the application that media comes from
- * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when
- * the event happened
- * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1
- * for tapping on card but not on any media item, 0 for first media item, 1 for second, etc.
- * @param interactedSubcardCardinality how many media items were shown to the user when there
- * is user interaction
- * @param rank the rank for media card in the media carousel, starting from 0
- * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency
- * between headphone connection to sysUI displays media recommendation card
- * @param isSwipeToDismiss whether is to log swipe-to-dismiss event
- *
- */
- fun logSmartspaceCardReported(
- eventId: Int,
- instanceId: Int,
- uid: Int,
- surfaces: IntArray,
- interactedSubcardRank: Int = 0,
- interactedSubcardCardinality: Int = 0,
- rank: Int = mediaCarouselScrollHandler.visibleMediaIndex,
- receivedLatencyMillis: Int = 0,
- isSwipeToDismiss: Boolean = false
- ) {
- if (MediaPlayerData.players().size <= rank) {
- return
- }
-
- val mediaControlKey = MediaPlayerData.playerKeys().elementAt(rank)
- // Only log media resume card when Smartspace data is available
- if (!mediaControlKey.isSsMediaRec &&
- !mediaManager.smartspaceMediaData.isActive &&
- MediaPlayerData.smartspaceMediaData == null) {
- return
- }
-
- val cardinality = mediaContent.getChildCount()
- surfaces.forEach { surface ->
- /* ktlint-disable max-line-length */
- SysUiStatsLog.write(SysUiStatsLog.SMARTSPACE_CARD_REPORTED,
- eventId,
- instanceId,
- // Deprecated, replaced with AiAi feature type so we don't need to create logging
- // card type for each new feature.
- SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD,
- surface,
- // Use -1 as rank value to indicate user swipe to dismiss the card
- if (isSwipeToDismiss) -1 else rank,
- cardinality,
- if (mediaControlKey.isSsMediaRec)
- 15 // MEDIA_RECOMMENDATION
- else if (mediaControlKey.isSsReactivated)
- 43 // MEDIA_RESUME_SS_ACTIVATED
- else
- 31, // MEDIA_RESUME
- uid,
- interactedSubcardRank,
- interactedSubcardCardinality,
- receivedLatencyMillis,
- null, // Media cards cannot have subcards.
- null // Media cards don't have dimensions today.
- )
- /* ktlint-disable max-line-length */
- if (DEBUG) {
- Log.d(TAG, "Log Smartspace card event id: $eventId instance id: $instanceId" +
- " surface: $surface rank: $rank cardinality: $cardinality " +
- "isRecommendationCard: ${mediaControlKey.isSsMediaRec} " +
- "isSsReactivated: ${mediaControlKey.isSsReactivated}" +
- "uid: $uid " +
- "interactedSubcardRank: $interactedSubcardRank " +
- "interactedSubcardCardinality: $interactedSubcardCardinality " +
- "received_latency_millis: $receivedLatencyMillis")
- }
- }
- }
-
- private fun onSwipeToDismiss() {
- MediaPlayerData.players().forEachIndexed {
- index, it ->
- if (it.mIsImpressed) {
- logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT,
- it.mSmartspaceId,
- it.mUid,
- intArrayOf(it.surfaceForSmartspaceLogging),
- rank = index,
- isSwipeToDismiss = true)
- // Reset card impressed state when swipe to dismissed
- it.mIsImpressed = false
- }
- }
- logger.logSwipeDismiss()
- mediaManager.onSwipeToDismiss()
- }
-
- fun getCurrentVisibleMediaContentIntent(): PendingIntent? {
- return MediaPlayerData.playerKeys()
- .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)?.data?.clickIntent
- }
-
- override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.apply {
- println("keysNeedRemoval: $keysNeedRemoval")
- println("dataKeys: ${MediaPlayerData.dataKeys()}")
- println("playerSortKeys: ${MediaPlayerData.playerKeys()}")
- println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}")
- println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}")
- println("current size: $currentCarouselWidth x $currentCarouselHeight")
- println("location: $desiredLocation")
- println("state: ${desiredHostState?.expansion}, " +
- "only active ${desiredHostState?.showsOnlyActiveMedia}")
- }
- }
-}
-
-@VisibleForTesting
-internal object MediaPlayerData {
- private val EMPTY = MediaData(
- userId = -1,
- initialized = false,
- app = null,
- appIcon = null,
- artist = null,
- song = null,
- artwork = null,
- actions = emptyList(),
- actionsToShowInCompact = emptyList(),
- packageName = "INVALID",
- token = null,
- clickIntent = null,
- device = null,
- active = true,
- resumeAction = null,
- instanceId = InstanceId.fakeInstanceId(-1),
- appUid = -1)
- // Whether should prioritize Smartspace card.
- internal var shouldPrioritizeSs: Boolean = false
- private set
- internal var smartspaceMediaData: SmartspaceMediaData? = null
- private set
-
- data class MediaSortKey(
- val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation.
- val data: MediaData,
- val updateTime: Long = 0,
- val isSsReactivated: Boolean = false
- )
-
- private val comparator = compareByDescending<MediaSortKey> {
- it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL }
- .thenByDescending {
- it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL }
- .thenByDescending { it.data.active }
- .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec }
- .thenByDescending { !it.data.resumption }
- .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
- .thenByDescending { it.data.lastActive }
- .thenByDescending { it.updateTime }
- .thenByDescending { it.data.notificationKey }
-
- private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
- private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
-
- fun addMediaPlayer(
- key: String,
- data: MediaData,
- player: MediaControlPanel,
- clock: SystemClock,
- isSsReactivated: Boolean,
- debugLogger: MediaCarouselControllerLogger? = null
- ) {
- val removedPlayer = removeMediaPlayer(key)
- if (removedPlayer != null && removedPlayer != player) {
- debugLogger?.logPotentialMemoryLeak(key)
- }
- val sortKey = MediaSortKey(isSsMediaRec = false,
- data, clock.currentTimeMillis(), isSsReactivated = isSsReactivated)
- mediaData.put(key, sortKey)
- mediaPlayers.put(sortKey, player)
- }
-
- fun addMediaRecommendation(
- key: String,
- data: SmartspaceMediaData,
- player: MediaControlPanel,
- shouldPrioritize: Boolean,
- clock: SystemClock,
- debugLogger: MediaCarouselControllerLogger? = null
- ) {
- shouldPrioritizeSs = shouldPrioritize
- val removedPlayer = removeMediaPlayer(key)
- if (removedPlayer != null && removedPlayer != player) {
- debugLogger?.logPotentialMemoryLeak(key)
- }
- val sortKey = MediaSortKey(isSsMediaRec = true,
- EMPTY.copy(isPlaying = false), clock.currentTimeMillis(), isSsReactivated = true)
- mediaData.put(key, sortKey)
- mediaPlayers.put(sortKey, player)
- smartspaceMediaData = data
- }
-
- fun moveIfExists(
- oldKey: String?,
- newKey: String,
- debugLogger: MediaCarouselControllerLogger? = null
- ) {
- if (oldKey == null || oldKey == newKey) {
- return
- }
-
- mediaData.remove(oldKey)?.let {
- val removedPlayer = removeMediaPlayer(newKey)
- removedPlayer?.run { debugLogger?.logPotentialMemoryLeak(newKey) }
- mediaData.put(newKey, it)
- }
- }
-
- fun getMediaPlayer(key: String): MediaControlPanel? {
- return mediaData.get(key)?.let { mediaPlayers.get(it) }
- }
-
- fun getMediaPlayerIndex(key: String): Int {
- val sortKey = mediaData.get(key)
- mediaPlayers.entries.forEachIndexed { index, e ->
- if (e.key == sortKey) {
- return index
- }
- }
- return -1
- }
-
- fun removeMediaPlayer(key: String) = mediaData.remove(key)?.let {
- if (it.isSsMediaRec) {
- smartspaceMediaData = null
- }
- mediaPlayers.remove(it)
- }
-
- fun mediaData() = mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) }
-
- fun dataKeys() = mediaData.keys
-
- fun players() = mediaPlayers.values
-
- fun playerKeys() = mediaPlayers.keys
-
- /** Returns the index of the first non-timeout media. */
- fun firstActiveMediaIndex(): Int {
- mediaPlayers.entries.forEachIndexed { index, e ->
- if (!e.key.isSsMediaRec && e.key.data.active) {
- return index
- }
- }
- return -1
- }
-
- /** Returns the existing Smartspace target id. */
- fun smartspaceMediaKey(): String? {
- mediaData.entries.forEach { e ->
- if (e.value.isSsMediaRec) {
- return e.key
- }
- }
- return null
- }
-
- @VisibleForTesting
- fun clear() {
- mediaData.clear()
- mediaPlayers.clear()
- }
-
- /* Returns true if there is active media player card or recommendation card */
- fun hasActiveMediaOrRecommendationCard(): Boolean {
- if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) {
- return true
- }
- if (firstActiveMediaIndex() != -1) {
- return true
- }
- return false
- }
-
- fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
deleted file mode 100644
index b1018f9..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.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.dagger.MediaCarouselControllerLog
-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/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
deleted file mode 100644
index bffb0fd..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ /dev/null
@@ -1,357 +0,0 @@
-package com.android.systemui.media
-
-import android.graphics.Rect
-import android.util.ArraySet
-import android.view.View
-import android.view.View.OnAttachStateChangeListener
-import com.android.systemui.util.animation.DisappearParameters
-import com.android.systemui.util.animation.MeasurementInput
-import com.android.systemui.util.animation.MeasurementOutput
-import com.android.systemui.util.animation.UniqueObjectHostView
-import java.util.Objects
-import javax.inject.Inject
-
-class MediaHost constructor(
- private val state: MediaHostStateHolder,
- private val mediaHierarchyManager: MediaHierarchyManager,
- private val mediaDataManager: MediaDataManager,
- private val mediaHostStatesManager: MediaHostStatesManager
-) : MediaHostState by state {
- lateinit var hostView: UniqueObjectHostView
- var location: Int = -1
- private set
- private var visibleChangedListeners: ArraySet<(Boolean) -> Unit> = ArraySet()
-
- private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
-
- private var inited: Boolean = false
-
- /**
- * 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
- */
- val currentBounds: Rect = Rect()
- get() {
- hostView.getLocationOnScreen(tmpLocationOnScreen)
- var left = tmpLocationOnScreen[0] + hostView.paddingLeft
- var top = tmpLocationOnScreen[1] + hostView.paddingTop
- var right = tmpLocationOnScreen[0] + hostView.width - hostView.paddingRight
- var bottom = tmpLocationOnScreen[1] + hostView.height - hostView.paddingBottom
- // Handle cases when the width or height is 0 but it has padding. In those cases
- // the above could return negative widths, which is wrong
- if (right < left) {
- left = 0
- right = 0
- }
- if (bottom < top) {
- bottom = 0
- top = 0
- }
- field.set(left, top, right, bottom)
- return field
- }
-
- /**
- * Set the clipping that this host should use, based on its parent's bounds.
- *
- * Use [Rect.set].
- */
- val currentClipping = Rect()
-
- private val listener = object : MediaDataManager.Listener {
- override fun onMediaDataLoaded(
- key: String,
- oldKey: String?,
- data: MediaData,
- immediately: Boolean,
- receivedSmartspaceCardLatency: Int,
- isSsReactivated: Boolean
- ) {
- if (immediately) {
- updateViewVisibility()
- }
- }
-
- 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) {
- updateViewVisibility()
- }
- }
- }
-
- fun addVisibilityChangeListener(listener: (Boolean) -> Unit) {
- visibleChangedListeners.add(listener)
- }
-
- fun removeVisibilityChangeListener(listener: (Boolean) -> Unit) {
- visibleChangedListeners.remove(listener)
- }
-
- /**
- * 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) {
- return
- }
- inited = true
-
- this.location = location
- hostView = mediaHierarchyManager.register(this)
- // 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()
- }
-
- 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)
- }
- // 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)
- }
-
- updateViewVisibility()
- }
-
- private fun setListeningToMediaData(listen: Boolean) {
- if (listen != listeningToMediaData) {
- listeningToMediaData = listen
- if (listen) {
- mediaDataManager.addListener(listener)
- } else {
- mediaDataManager.removeListener(listener)
- }
- }
- }
-
- /**
- * Updates this host's state based on the current media data's status, and invokes listeners if
- * the visibility has changed
- */
- fun updateViewVisibility() {
- 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)
- }
- }
- }
-
- class MediaHostStateHolder @Inject constructor() : MediaHostState {
- override var measurementInput: MeasurementInput? = null
- set(value) {
- if (value?.equals(field) != true) {
- field = value
- changedListener?.invoke()
- }
- }
-
- override var expansion: Float = 0.0f
- set(value) {
- if (!value.equals(field)) {
- field = value
- changedListener?.invoke()
- }
- }
-
- override var showsOnlyActiveMedia: Boolean = false
- set(value) {
- if (!value.equals(field)) {
- field = value
- changedListener?.invoke()
- }
- }
-
- override var visible: Boolean = true
- set(value) {
- if (field == value) {
- return
- }
- field = value
- changedListener?.invoke()
- }
-
- override var falsingProtectionNeeded: Boolean = false
- set(value) {
- if (field == value) {
- return
- }
- field = value
- changedListener?.invoke()
- }
-
- override var disappearParameters: DisappearParameters = DisappearParameters()
- set(value) {
- val newHash = value.hashCode()
- if (lastDisappearHash.equals(newHash)) {
- return
- }
- field = value
- lastDisappearHash = newHash
- changedListener?.invoke()
- }
-
- private var lastDisappearHash = disappearParameters.hashCode()
-
- /**
- * 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
- */
- override fun copy(): MediaHostState {
- val mediaHostState = MediaHostStateHolder()
- mediaHostState.expansion = expansion
- mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
- mediaHostState.measurementInput = measurementInput?.copy()
- mediaHostState.visible = visible
- mediaHostState.disappearParameters = disappearParameters.deepCopy()
- mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
- return mediaHostState
- }
-
- override fun equals(other: Any?): Boolean {
- if (!(other is MediaHostState)) {
- return false
- }
- if (!Objects.equals(measurementInput, other.measurementInput)) {
- return false
- }
- if (expansion != other.expansion) {
- return false
- }
- if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
- return false
- }
- if (visible != other.visible) {
- return false
- }
- if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
- return false
- }
- if (!disappearParameters.equals(other.disappearParameters)) {
- return false
- }
- return true
- }
-
- override fun hashCode(): Int {
- var result = measurementInput?.hashCode() ?: 0
- result = 31 * result + expansion.hashCode()
- result = 31 * result + falsingProtectionNeeded.hashCode()
- result = 31 * result + showsOnlyActiveMedia.hashCode()
- result = 31 * result + if (visible) 1 else 2
- result = 31 * result + disappearParameters.hashCode()
- return result
- }
- }
-}
-
-/**
- * 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 {
-
- companion object {
- const val EXPANDED: Float = 1.0f
- const val COLLAPSED: Float = 0.0f
- }
-
- /**
- * 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).
- */
- var expansion: Float
-
- /**
- * 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.
- */
- val visible: Boolean
-
- /**
- * 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
- */
- var disappearParameters: DisappearParameters
-
- /**
- * Get a copy of this view state, deepcopying all appropriate members
- */
- fun copy(): MediaHostState
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/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 d9c58c0..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.media.session.PlaybackState
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.dagger.MediaTimeoutListenerLog
-import javax.inject.Inject
-
-private const val TAG = "MediaTimeout"
-
-/**
- * A buffered log for [MediaTimeoutListener] events
- */
-@SysUISingleton
-class MediaTimeoutLogger @Inject constructor(
- @MediaTimeoutListenerLog private val buffer: LogBuffer
-) {
- fun logReuseListener(key: String) = buffer.log(
- TAG,
- LogLevel.DEBUG,
- {
- str1 = key
- },
- {
- "reuse listener: $str1"
- }
- )
-
- fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) = buffer.log(
- TAG,
- LogLevel.DEBUG,
- {
- str1 = oldKey
- str2 = newKey
- bool1 = hadListener
- },
- {
- "migrate from $str1 to $str2, had listener? $bool1"
- }
- )
-
- fun logUpdateListener(key: String, wasPlaying: Boolean) = buffer.log(
- TAG,
- LogLevel.DEBUG,
- {
- str1 = key
- bool1 = wasPlaying
- },
- {
- "updating $str1, was playing? $bool1"
- }
- )
-
- fun logDelayedUpdate(key: String) = buffer.log(
- TAG,
- LogLevel.DEBUG,
- {
- str1 = key
- },
- {
- "deliver delayed playback state for $str1"
- }
- )
-
- fun logSessionDestroyed(key: String) = buffer.log(
- TAG,
- LogLevel.DEBUG,
- {
- str1 = key
- },
- {
- "session destroyed $str1"
- }
- )
-
- fun logPlaybackState(key: String, state: PlaybackState?) = buffer.log(
- TAG,
- LogLevel.VERBOSE,
- {
- str1 = key
- str2 = state?.toString()
- },
- {
- "state update: key=$str1 state=$str2"
- }
- )
-
- fun logStateCallback(key: String) = buffer.log(
- TAG,
- LogLevel.VERBOSE,
- {
- str1 = key
- },
- {
- "dispatching state update for $key"
- }
- )
-
- fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) = buffer.log(
- TAG,
- LogLevel.DEBUG,
- {
- str1 = key
- bool1 = playing
- bool2 = resumption
- },
- {
- "schedule timeout $str1, playing=$bool1 resumption=$bool2"
- }
- )
-
- fun logCancelIgnored(key: String) = buffer.log(
- TAG,
- LogLevel.DEBUG,
- {
- str1 = key
- },
- {
- "cancellation already exists for $str1"
- }
- )
-
- fun logTimeout(key: String) = buffer.log(
- TAG,
- LogLevel.DEBUG,
- {
- str1 = key
- },
- {
- "execute timeout for $str1"
- }
- )
-
- fun logTimeoutCancelled(key: String, reason: String) = buffer.log(
- TAG,
- LogLevel.VERBOSE,
- {
- str1 = key
- str2 = reason
- },
- {
- "media timeout cancelled for $str1, reason: $str2"
- }
- )
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
deleted file mode 100644
index ac59175..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
+++ /dev/null
@@ -1,547 +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.constraintlayout.widget.ConstraintSet
-import com.android.systemui.R
-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
- }
-
- /**
- * A listener when the current dimensions of the player change
- */
- lateinit var sizeChangedListener: () -> Unit
- private var firstRefresh: Boolean = true
- 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
- }
- }
- }
-
- /**
- * 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.
- */
- private 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
- 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) {
- // 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)
- }
- } else {
- result = null
- }
- 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 73868189..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.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.dagger.MediaViewLog
-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 52ac4e0..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
+++ /dev/null
@@ -1,110 +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
- )
- }
-}
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 41f7354..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.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.dagger.MediaBrowserLog
-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/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
index 0b9b32b..2a8168b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
@@ -51,9 +51,10 @@
* {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}.
*/
@SysUISingleton
-public class RingtonePlayer extends CoreStartable {
+public class RingtonePlayer implements CoreStartable {
private static final String TAG = "RingtonePlayer";
private static final boolean LOGD = false;
+ private final Context mContext;
// TODO: support Uri switching under same IBinder
@@ -64,7 +65,7 @@
@Inject
public RingtonePlayer(Context context) {
- super(context);
+ mContext = context;
}
@Override
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 17ebfec..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
@@ -33,7 +33,6 @@
import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.util.concurrency.RepeatableExecutor
import javax.inject.Inject
@@ -43,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
}
/**
@@ -60,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) {
@@ -74,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,
) {
@@ -87,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
@@ -101,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. */
@@ -122,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
@@ -145,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.
@@ -160,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.
@@ -217,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()
}
@@ -232,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() {
@@ -267,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()
@@ -333,11 +348,7 @@
}
override fun onStopTrackingTouch(bar: SeekBar) {
- // in addition to the normal functionality of both functions.
- // isFalseTouch returns true if there is a real/false tap since it is not a move.
- // isFalseTap returns true if there is a real/false move since it is not a tap.
- if (falsingManager.isFalseTouch(MEDIA_SEEKBAR) &&
- falsingManager.isFalseTap(LOW_PENALTY)) {
+ if (falsingManager.isFalseTouch(MEDIA_SEEKBAR)) {
viewModel.onSeekFalse()
}
viewModel.onSeek(bar.progress.toLong())
@@ -358,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
@@ -390,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.
*/
@@ -403,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 5096a797..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
}
}
@@ -1130,14 +1256,21 @@
fun onNotificationRemoved(key: String) {
Assert.isMainThread()
val removed = mediaEntries.remove(key)
- if (useMediaResumption && removed?.resumeAction != null && removed?.isLocalSession()) {
+ if (useMediaResumption && removed?.resumeAction != null && removed.isLocalSession()) {
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 66%
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 ef49fd3..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_MEDIUM,
- 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))
@@ -289,7 +271,10 @@
return false
}
}
- if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) {
+ if (motionEvent.action == MotionEvent.ACTION_MOVE) {
+ // cancel on going animation if there is any.
+ PhysicsAnimator.getInstance(this).cancel()
+ } else if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) {
// It's an up and the fling didn't take it above
val relativePos = scrollView.relativeScrollX % playerWidthPlusPadding
val scrollXAmount: Int
@@ -303,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
@@ -321,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
}
}
@@ -335,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
@@ -348,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()) {
@@ -370,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())) {
@@ -388,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
}
@@ -399,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
}
@@ -421,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
@@ -440,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()
@@ -482,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()
@@ -526,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)
@@ -547,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
@@ -558,9 +548,7 @@
}
}
- /**
- * Reset the MediaScrollView to the start.
- */
+ /** Reset the MediaScrollView to the start. */
fun scrollToStart() {
scrollView.relativeScrollX = 0
}
@@ -578,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 759795f..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();
@@ -1441,7 +1457,7 @@
}
// Automatically scroll to the active player once the media is loaded.
- mMediaCarouselController.setShouldScrollToActivePlayer(true);
+ mMediaCarouselController.setShouldScrollToKey(true);
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/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/controls/ui/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
new file mode 100644
index 0000000..455b7de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
@@ -0,0 +1,383 @@
+/*
+ * 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
+import com.android.systemui.util.animation.UniqueObjectHostView
+import java.util.Objects
+import javax.inject.Inject
+
+class MediaHost
+constructor(
+ private val state: MediaHostStateHolder,
+ private val mediaHierarchyManager: MediaHierarchyManager,
+ private val mediaDataManager: MediaDataManager,
+ private val mediaHostStatesManager: MediaHostStatesManager
+) : MediaHostState by state {
+ lateinit var hostView: UniqueObjectHostView
+ var location: Int = -1
+ private set
+ private var visibleChangedListeners: ArraySet<(Boolean) -> Unit> = ArraySet()
+
+ private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
+
+ private var inited: Boolean = false
+
+ /** 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 */
+ val currentBounds: Rect = Rect()
+ get() {
+ hostView.getLocationOnScreen(tmpLocationOnScreen)
+ var left = tmpLocationOnScreen[0] + hostView.paddingLeft
+ var top = tmpLocationOnScreen[1] + hostView.paddingTop
+ var right = tmpLocationOnScreen[0] + hostView.width - hostView.paddingRight
+ var bottom = tmpLocationOnScreen[1] + hostView.height - hostView.paddingBottom
+ // Handle cases when the width or height is 0 but it has padding. In those cases
+ // the above could return negative widths, which is wrong
+ if (right < left) {
+ left = 0
+ right = 0
+ }
+ if (bottom < top) {
+ bottom = 0
+ top = 0
+ }
+ field.set(left, top, right, bottom)
+ return field
+ }
+
+ /**
+ * Set the clipping that this host should use, based on its parent's bounds.
+ *
+ * Use [Rect.set].
+ */
+ val currentClipping = Rect()
+
+ private val listener =
+ object : MediaDataManager.Listener {
+ override fun onMediaDataLoaded(
+ key: String,
+ oldKey: String?,
+ data: MediaData,
+ immediately: Boolean,
+ receivedSmartspaceCardLatency: Int,
+ isSsReactivated: Boolean
+ ) {
+ if (immediately) {
+ updateViewVisibility()
+ }
+ }
+
+ 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) {
+ updateViewVisibility()
+ }
+ }
+ }
+
+ fun addVisibilityChangeListener(listener: (Boolean) -> Unit) {
+ visibleChangedListeners.add(listener)
+ }
+
+ fun removeVisibilityChangeListener(listener: (Boolean) -> Unit) {
+ visibleChangedListeners.remove(listener)
+ }
+
+ /**
+ * 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) {
+ return
+ }
+ inited = true
+
+ this.location = location
+ hostView = mediaHierarchyManager.register(this)
+ // 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()
+ }
+
+ 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
+ )
+ }
+ // 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) }
+
+ updateViewVisibility()
+ }
+
+ private fun setListeningToMediaData(listen: Boolean) {
+ if (listen != listeningToMediaData) {
+ listeningToMediaData = listen
+ if (listen) {
+ mediaDataManager.addListener(listener)
+ } else {
+ mediaDataManager.removeListener(listener)
+ }
+ }
+ }
+
+ /**
+ * Updates this host's state based on the current media data's status, and invokes listeners if
+ * the visibility has changed
+ */
+ fun updateViewVisibility() {
+ 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) }
+ }
+ }
+
+ class MediaHostStateHolder @Inject constructor() : MediaHostState {
+ override var measurementInput: MeasurementInput? = null
+ set(value) {
+ if (value?.equals(field) != true) {
+ field = value
+ changedListener?.invoke()
+ }
+ }
+
+ override var expansion: Float = 0.0f
+ set(value) {
+ if (!value.equals(field)) {
+ field = value
+ changedListener?.invoke()
+ }
+ }
+
+ override var squishFraction: Float = 1.0f
+ set(value) {
+ if (!value.equals(field)) {
+ field = value
+ changedListener?.invoke()
+ }
+ }
+
+ override var showsOnlyActiveMedia: Boolean = false
+ set(value) {
+ if (!value.equals(field)) {
+ field = value
+ changedListener?.invoke()
+ }
+ }
+
+ override var visible: Boolean = true
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ changedListener?.invoke()
+ }
+
+ override var falsingProtectionNeeded: Boolean = false
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ changedListener?.invoke()
+ }
+
+ override var disappearParameters: DisappearParameters = DisappearParameters()
+ set(value) {
+ val newHash = value.hashCode()
+ if (lastDisappearHash.equals(newHash)) {
+ return
+ }
+ field = value
+ lastDisappearHash = newHash
+ changedListener?.invoke()
+ }
+
+ private var lastDisappearHash = disappearParameters.hashCode()
+
+ /** 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 */
+ override fun copy(): MediaHostState {
+ val mediaHostState = MediaHostStateHolder()
+ mediaHostState.expansion = expansion
+ mediaHostState.squishFraction = squishFraction
+ mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
+ mediaHostState.measurementInput = measurementInput?.copy()
+ mediaHostState.visible = visible
+ mediaHostState.disappearParameters = disappearParameters.deepCopy()
+ mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
+ return mediaHostState
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (!(other is MediaHostState)) {
+ return false
+ }
+ if (!Objects.equals(measurementInput, other.measurementInput)) {
+ return false
+ }
+ if (expansion != other.expansion) {
+ return false
+ }
+ if (squishFraction != other.squishFraction) {
+ return false
+ }
+ if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
+ return false
+ }
+ if (visible != other.visible) {
+ return false
+ }
+ if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
+ return false
+ }
+ if (!disappearParameters.equals(other.disappearParameters)) {
+ return false
+ }
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = measurementInput?.hashCode() ?: 0
+ result = 31 * result + expansion.hashCode()
+ result = 31 * result + squishFraction.hashCode()
+ result = 31 * result + falsingProtectionNeeded.hashCode()
+ result = 31 * result + showsOnlyActiveMedia.hashCode()
+ result = 31 * result + if (visible) 1 else 2
+ result = 31 * result + disappearParameters.hashCode()
+ return result
+ }
+ }
+}
+
+/**
+ * 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 {
+
+ companion object {
+ const val EXPANDED: Float = 1.0f
+ const val COLLAPSED: Float = 0.0f
+ }
+
+ /**
+ * 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).
+ */
+ var expansion: Float
+
+ /** Fraction of the height animation. */
+ var squishFraction: Float
+
+ /** 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. */
+ val visible: Boolean
+
+ /** 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
+ */
+ var disappearParameters: DisappearParameters
+
+ /** 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 66c036c..3e5d337 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -17,24 +17,22 @@
package com.android.systemui.media.dagger;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.log.LogBuffer;
import com.android.systemui.log.dagger.MediaTttReceiverLogBuffer;
import com.android.systemui.log.dagger.MediaTttSenderLogBuffer;
-import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.MediaFlags;
-import com.android.systemui.media.MediaHierarchyManager;
-import com.android.systemui.media.MediaHost;
-import com.android.systemui.media.MediaHostStatesManager;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
+import com.android.systemui.media.controls.ui.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHostStatesManager;
+import com.android.systemui.media.controls.util.MediaFlags;
import com.android.systemui.media.dream.dagger.MediaComplicationComponent;
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper;
import com.android.systemui.media.taptotransfer.MediaTttFlags;
import com.android.systemui.media.taptotransfer.common.MediaTttLogger;
-import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver;
import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger;
-import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender;
import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger;
+import com.android.systemui.plugins.log.LogBuffer;
import java.util.Optional;
@@ -94,30 +92,6 @@
return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager);
}
- /** */
- @Provides
- @SysUISingleton
- static Optional<MediaTttChipControllerSender> providesMediaTttChipControllerSender(
- MediaTttFlags mediaTttFlags,
- Lazy<MediaTttChipControllerSender> controllerSenderLazy) {
- if (!mediaTttFlags.isMediaTttEnabled()) {
- return Optional.empty();
- }
- return Optional.of(controllerSenderLazy.get());
- }
-
- /** */
- @Provides
- @SysUISingleton
- static Optional<MediaTttChipControllerReceiver> providesMediaTttChipControllerReceiver(
- MediaTttFlags mediaTttFlags,
- Lazy<MediaTttChipControllerReceiver> controllerReceiverLazy) {
- if (!mediaTttFlags.isMediaTttEnabled()) {
- return Optional.empty();
- }
- return Optional.of(controllerReceiverLazy.get());
- }
-
@Provides
@SysUISingleton
@MediaTttSenderLogger
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 53b4d43..20e8ae6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
@@ -18,7 +18,6 @@
import static com.android.systemui.flags.Flags.DREAM_MEDIA_COMPLICATION;
-import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -28,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;
@@ -38,7 +37,7 @@
* {@link MediaDreamSentinel} is responsible for tracking media state and registering/unregistering
* the media complication as appropriate
*/
-public class MediaDreamSentinel extends CoreStartable {
+public class MediaDreamSentinel implements CoreStartable {
private static final String TAG = "MediaDreamSentinel";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -113,11 +112,10 @@
private final FeatureFlags mFeatureFlags;
@Inject
- public MediaDreamSentinel(Context context, MediaDataManager mediaDataManager,
+ public MediaDreamSentinel(MediaDataManager mediaDataManager,
DreamOverlayStateController dreamOverlayStateController,
DreamMediaEntryComplication mediaEntryComplication,
FeatureFlags featureFlags) {
- super(context);
mMediaDataManager = mediaDataManager;
mDreamOverlayStateController = dreamOverlayStateController;
mMediaEntryComplication = mediaEntryComplication;
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/muteawait/MediaMuteAwaitLogger.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
index 78f4e01..5ace3ea 100644
--- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
@@ -1,9 +1,9 @@
package com.android.systemui.media.muteawait
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.MediaMuteAwaitLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import javax.inject.Inject
/** Log messages for [MediaMuteAwaitConnectionManager]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
index 46b2cc14..78408fc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
@@ -1,9 +1,9 @@
package com.android.systemui.media.nearby
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.NearbyMediaDevicesLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import javax.inject.Inject
/** Log messages for [NearbyMediaDevicesManager]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java b/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java
index d60172a..0ba5f28 100644
--- a/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java
@@ -40,7 +40,7 @@
* documented at {@link #handleTaskStackChanged} apply.
*/
@SysUISingleton
-public class HomeSoundEffectController extends CoreStartable {
+public class HomeSoundEffectController implements CoreStartable {
private static final String TAG = "HomeSoundEffectController";
private final AudioManager mAudioManager;
@@ -65,7 +65,6 @@
TaskStackChangeListeners taskStackChangeListeners,
ActivityManagerWrapper activityManagerWrapper,
PackageManager packageManager) {
- super(context);
mAudioManager = audioManager;
mTaskStackChangeListeners = taskStackChangeListeners;
mActivityManagerWrapper = activityManagerWrapper;
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
index f5caefb..a4a96806 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
@@ -43,7 +43,7 @@
private val commandRegistry: CommandRegistry,
private val context: Context,
@Main private val mainExecutor: Executor
-) : CoreStartable(context) {
+) : CoreStartable {
/** All commands for the sender device. */
inner class SenderCommand : Command {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/README.md b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/README.md
index 6379960..b5a0483 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/README.md
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/README.md
@@ -41,3 +41,5 @@
## Testing
If you want to test out the tap-to-transfer chip without using the `@SystemApi`s, you can use adb
commands instead. Refer to `MediaTttCommandLineHelper` for information about adb commands.
+
+TODO(b/245610654): Update this page once the chipbar migration is complete.
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
index b565f3c..120f7d6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
@@ -16,8 +16,8 @@
package com.android.systemui.media.taptotransfer.common
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.temporarydisplay.TemporaryViewLogger
/**
@@ -43,6 +43,21 @@
)
}
+ /**
+ * Logs an error in trying to update to [displayState].
+ *
+ * [displayState] is either a [android.app.StatusBarManager.MediaTransferSenderState] or
+ * a [android.app.StatusBarManager.MediaTransferReceiverState].
+ */
+ fun logStateChangeError(displayState: Int) {
+ buffer.log(
+ tag,
+ LogLevel.ERROR,
+ { int1 = displayState },
+ { "Cannot display state=$int1; aborting" }
+ )
+ }
+
/** Logs that we couldn't find information for [packageName]. */
fun logPackageNotFound(packageName: String) {
buffer.log(
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
index 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/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 1461293..dc794e6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -25,7 +25,6 @@
import android.media.MediaRoute2Info
import android.os.Handler
import android.os.PowerManager
-import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
@@ -36,6 +35,7 @@
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+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
@@ -52,6 +52,8 @@
* A controller to display and hide the Media Tap-To-Transfer chip on the **receiving** device.
*
* This chip is shown when a user is transferring media to/from a sending device and this device.
+ *
+ * TODO(b/245610654): Re-name this to be MediaTttReceiverCoordinator.
*/
@SysUISingleton
class MediaTttChipControllerReceiver @Inject constructor(
@@ -64,6 +66,7 @@
configurationController: ConfigurationController,
powerManager: PowerManager,
@Main private val mainHandler: Handler,
+ private val mediaTttFlags: MediaTttFlags,
private val uiEventLogger: MediaTttReceiverUiEventLogger,
private val viewUtil: ViewUtil,
) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger>(
@@ -112,13 +115,13 @@
logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName)
if (chipState == null) {
- Log.e(RECEIVER_TAG, "Unhandled MediaTransferReceiverState $displayState")
+ logger.logStateChangeError(displayState)
return
}
uiEventLogger.logReceiverStateChange(chipState)
if (chipState == ChipStateReceiver.FAR_FROM_SENDER) {
- removeView(removalReason = ChipStateReceiver.FAR_FROM_SENDER::class.simpleName!!)
+ removeView(removalReason = ChipStateReceiver.FAR_FROM_SENDER.name)
return
}
if (appIcon == null) {
@@ -138,7 +141,9 @@
}
override fun start() {
- commandQueue.addCallback(commandQueueCallbacks)
+ if (mediaTttFlags.isMediaTttEnabled()) {
+ commandQueue.addCallback(commandQueueCallbacks)
+ }
}
override fun updateView(newInfo: ChipReceiverInfo, currentView: ViewGroup) {
@@ -230,5 +235,3 @@
) : TemporaryViewInfo {
override fun getTimeoutMs() = DEFAULT_TIMEOUT_MILLIS
}
-
-private const val RECEIVER_TAG = "MediaTapToTransferRcvr"
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index aae973d..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,14 +18,11 @@
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
/**
@@ -36,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.
*/
@@ -44,6 +42,7 @@
val uiEvent: UiEventLogger.UiEventEnum,
@StringRes val stringResId: Int?,
val transferStatus: TransferStatus,
+ val endItem: SenderEndItem?,
val timeout: Long = DEFAULT_TIMEOUT_MILLIS
) {
/**
@@ -56,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,
),
/**
@@ -69,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,
),
/**
@@ -80,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
),
@@ -92,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
),
@@ -103,36 +106,13 @@
MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED,
R.string.media_transfer_playing_different_device,
transferStatus = TransferStatus.SUCCEEDED,
- ) {
- override fun undoClickListener(
- controllerSender: MediaTttChipControllerSender,
- 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 ast o why the UI hasn't changed yet. So, we immediately change the UI
- // here.
- controllerSender.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.
@@ -142,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(
- controllerSender: MediaTttChipControllerSender,
- 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.
- controllerSender.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(
@@ -179,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. */
@@ -187,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. */
@@ -195,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 controllerSender passed as a parameter in case we want to display a new chip state
- * 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(
- controllerSender: MediaTttChipControllerSender,
- routeInfo: MediaRoute2Info,
- undoCallback: IUndoMediaTransferCallback?,
- uiEventLogger: MediaTttSenderUiEventLogger,
- falsingManager: FalsingManager,
- ): View.OnClickListener? = null
-
companion object {
/**
* Returns the sender state enum associated with the given [displayState] from
@@ -251,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/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
deleted file mode 100644
index 5d63145..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ /dev/null
@@ -1,247 +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.taptotransfer.sender
-
-import android.app.StatusBarManager
-import android.content.Context
-import android.graphics.Rect
-import android.media.MediaRoute2Info
-import android.os.PowerManager
-import android.util.Log
-import android.view.Gravity
-import android.view.MotionEvent
-import android.view.View
-import android.view.ViewGroup
-import android.view.WindowManager
-import android.view.accessibility.AccessibilityManager
-import android.widget.TextView
-import com.android.internal.statusbar.IUndoMediaTransferCallback
-import com.android.internal.widget.CachingIconView
-import com.android.systemui.Gefingerpoken
-import com.android.systemui.R
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.animation.ViewHierarchyAnimator
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.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.plugins.FalsingManager
-import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.temporarydisplay.TemporaryDisplayRemovalReason
-import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.view.ViewUtil
-import javax.inject.Inject
-
-/**
- * A controller to display and hide the Media Tap-To-Transfer chip on the **sending** device. This
- * chip is shown when a user is transferring media to/from this device and a receiver device.
- */
-@SysUISingleton
-open class MediaTttChipControllerSender @Inject constructor(
- private val commandQueue: CommandQueue,
- context: Context,
- @MediaTttSenderLogger logger: MediaTttLogger,
- windowManager: WindowManager,
- @Main mainExecutor: DelayableExecutor,
- 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>(
- context,
- logger,
- windowManager,
- mainExecutor,
- accessibilityManager,
- configurationController,
- powerManager,
- R.layout.media_ttt_chip,
- MediaTttUtils.WINDOW_TITLE,
- MediaTttUtils.WAKE_REASON,
-) {
-
- private lateinit var parent: MediaTttChipRootView
-
- override val windowLayoutParams = commonWindowLayoutParams.apply {
- gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
- }
-
- private val commandQueueCallbacks = object : CommandQueue.Callbacks {
- override fun updateMediaTapToTransferSenderDisplay(
- @StatusBarManager.MediaTransferSenderState displayState: Int,
- routeInfo: MediaRoute2Info,
- undoCallback: IUndoMediaTransferCallback?
- ) {
- this@MediaTttChipControllerSender.updateMediaTapToTransferSenderDisplay(
- displayState, routeInfo, undoCallback
- )
- }
- }
-
- private fun updateMediaTapToTransferSenderDisplay(
- @StatusBarManager.MediaTransferSenderState displayState: Int,
- routeInfo: MediaRoute2Info,
- undoCallback: IUndoMediaTransferCallback?
- ) {
- val chipState: ChipStateSender? = ChipStateSender.getSenderStateFromId(displayState)
- val stateName = chipState?.name ?: "Invalid"
- logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName)
-
- if (chipState == null) {
- Log.e(SENDER_TAG, "Unhandled MediaTransferSenderState $displayState")
- return
- }
- uiEventLogger.logSenderStateChange(chipState)
-
- if (chipState == ChipStateSender.FAR_FROM_RECEIVER) {
- removeView(removalReason = ChipStateSender.FAR_FROM_RECEIVER.name)
- } else {
- displayView(ChipSenderInfo(chipState, routeInfo, undoCallback))
- }
- }
-
- override fun start() {
- commandQueue.addCallback(commandQueueCallbacks)
- }
-
- override fun updateView(
- newInfo: ChipSenderInfo,
- currentView: ViewGroup
- ) {
- val chipState = newInfo.state
-
- // Detect falsing touches on the chip.
- parent = currentView.requireViewById(R.id.media_ttt_sender_chip)
- parent.touchHandler = object : Gefingerpoken {
- override fun onTouchEvent(ev: MotionEvent?): Boolean {
- falsingCollector.onTouchEvent(ev)
- return false
- }
- }
-
- // 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
-
- // Text
- val otherDeviceName = newInfo.routeInfo.name.toString()
- val chipText = chipState.getChipTextString(context, otherDeviceName)
- currentView.requireViewById<TextView>(R.id.text).text = chipText
-
- // Loading
- currentView.requireViewById<View>(R.id.loading).visibility =
- (chipState.transferStatus == TransferStatus.IN_PROGRESS).visibleIfTrue()
-
- // Undo
- val undoView = currentView.requireViewById<View>(R.id.undo)
- val undoClickListener = chipState.undoClickListener(
- this,
- newInfo.routeInfo,
- newInfo.undoCallback,
- uiEventLogger,
- falsingManager,
- )
- undoView.setOnClickListener(undoClickListener)
- undoView.visibility = (undoClickListener != null).visibleIfTrue()
-
- // Failure
- currentView.requireViewById<View>(R.id.failure_icon).visibility =
- (chipState.transferStatus == TransferStatus.FAILED).visibleIfTrue()
-
- // For accessibility
- currentView.requireViewById<ViewGroup>(
- R.id.media_ttt_sender_chip_inner
- ).contentDescription = "${iconInfo.contentDescription} $chipText"
- }
-
- override fun animateViewIn(view: ViewGroup) {
- val chipInnerView = view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner)
- ViewHierarchyAnimator.animateAddition(
- chipInnerView,
- ViewHierarchyAnimator.Hotspot.TOP,
- Interpolators.EMPHASIZED_DECELERATE,
- duration = ANIMATION_DURATION,
- includeMargins = true,
- includeFadeIn = true,
- // We can only request focus once the animation finishes.
- onAnimationEnd = { chipInnerView.requestAccessibilityFocus() },
- )
- }
-
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
- ViewHierarchyAnimator.animateRemoval(
- view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner),
- ViewHierarchyAnimator.Hotspot.TOP,
- Interpolators.EMPHASIZED_ACCELERATE,
- ANIMATION_DURATION,
- onAnimationEnd,
- )
- // TODO(b/203800644): Add includeMargins as an option to ViewHierarchyAnimator so that the
- // animateChipOut matches the animateChipIn.
- }
-
- override fun shouldIgnoreViewRemoval(info: ChipSenderInfo, removalReason: String): Boolean {
- // Don't remove the chip if we're in progress or succeeded, since the user should still be
- // able to see the status of the transfer. (But do remove it if it's finally timed out.)
- val transferStatus = info.state.transferStatus
- if (
- (transferStatus == TransferStatus.IN_PROGRESS ||
- transferStatus == TransferStatus.SUCCEEDED) &&
- removalReason != TemporaryDisplayRemovalReason.REASON_TIMEOUT
- ) {
- logger.logRemovalBypass(
- removalReason, bypassReason = "transferStatus=${transferStatus.name}"
- )
- return true
- }
- return false
- }
-
- override fun getTouchableRegion(view: View, outRect: Rect) {
- viewUtil.setRectToViewWindowLocation(view, outRect)
- }
-
- private fun Boolean.visibleIfTrue(): Int {
- return if (this) {
- View.VISIBLE
- } else {
- View.GONE
- }
- }
-}
-
-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/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
new file mode 100644
index 0000000..1fa8fae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.taptotransfer.sender
+
+import android.app.StatusBarManager
+import android.content.Context
+import android.media.MediaRoute2Info
+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.ChipbarCoordinator
+import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem
+import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
+import javax.inject.Inject
+
+/**
+ * A coordinator for showing/hiding the Media Tap-To-Transfer UI on the **sending** device. This UI
+ * is shown when a user is transferring media to/from this device and a receiver device.
+ */
+@SysUISingleton
+class MediaTttSenderCoordinator
+@Inject
+constructor(
+ private val chipbarCoordinator: ChipbarCoordinator,
+ private val commandQueue: CommandQueue,
+ private val context: Context,
+ @MediaTttSenderLogger private val logger: MediaTttLogger,
+ private val mediaTttFlags: MediaTttFlags,
+ private val uiEventLogger: MediaTttSenderUiEventLogger,
+) : CoreStartable {
+
+ private var displayedState: ChipStateSender? = null
+
+ private val commandQueueCallbacks =
+ object : CommandQueue.Callbacks {
+ override fun updateMediaTapToTransferSenderDisplay(
+ @StatusBarManager.MediaTransferSenderState displayState: Int,
+ routeInfo: MediaRoute2Info,
+ undoCallback: IUndoMediaTransferCallback?
+ ) {
+ this@MediaTttSenderCoordinator.updateMediaTapToTransferSenderDisplay(
+ displayState,
+ routeInfo,
+ undoCallback
+ )
+ }
+ }
+
+ override fun start() {
+ if (mediaTttFlags.isMediaTttEnabled()) {
+ commandQueue.addCallback(commandQueueCallbacks)
+ }
+ }
+
+ private fun updateMediaTapToTransferSenderDisplay(
+ @StatusBarManager.MediaTransferSenderState displayState: Int,
+ routeInfo: MediaRoute2Info,
+ undoCallback: IUndoMediaTransferCallback?
+ ) {
+ val chipState: ChipStateSender? = ChipStateSender.getSenderStateFromId(displayState)
+ val stateName = chipState?.name ?: "Invalid"
+ logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName)
+
+ if (chipState == null) {
+ logger.logStateChangeError(displayState)
+ return
+ }
+ uiEventLogger.logSenderStateChange(chipState)
+
+ if (chipState == ChipStateSender.FAR_FROM_RECEIVER) {
+ // Return early if we're not displaying a chip anyway
+ val currentDisplayedState = displayedState ?: return
+
+ val removalReason = ChipStateSender.FAR_FROM_RECEIVER.name
+ if (
+ currentDisplayedState.transferStatus == TransferStatus.IN_PROGRESS ||
+ currentDisplayedState.transferStatus == TransferStatus.SUCCEEDED
+ ) {
+ // Don't remove the chip if we're in progress or succeeded, since the user should
+ // still be able to see the status of the transfer.
+ logger.logRemovalBypass(
+ removalReason,
+ bypassReason = "transferStatus=${currentDisplayedState.transferStatus.name}"
+ )
+ return
+ }
+
+ displayedState = null
+ chipbarCoordinator.removeView(removalReason)
+ } else {
+ displayedState = chipState
+ chipbarCoordinator.displayView(
+ createChipbarInfo(
+ chipState,
+ routeInfo,
+ undoCallback,
+ context,
+ logger,
+ )
+ )
+ }
+ }
+
+ /**
+ * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display.
+ */
+ private fun createChipbarInfo(
+ chipStateSender: ChipStateSender,
+ routeInfo: MediaRoute2Info,
+ undoCallback: IUndoMediaTransferCallback?,
+ context: Context,
+ logger: MediaTttLogger,
+ ): ChipbarInfo {
+ val packageName = routeInfo.clientPackageName
+ val otherDeviceName = routeInfo.name.toString()
+
+ return ChipbarInfo(
+ // Display the app's icon as the start icon
+ startIcon = MediaTttUtils.getIconFromPackageName(context, packageName, logger),
+ text = chipStateSender.getChipTextString(context, otherDeviceName),
+ endItem =
+ when (chipStateSender.endItem) {
+ null -> null
+ is SenderEndItem.Loading -> ChipbarEndItem.Loading
+ is SenderEndItem.Error -> ChipbarEndItem.Error
+ is SenderEndItem.UndoButton -> {
+ if (undoCallback != null) {
+ getUndoButton(
+ undoCallback,
+ chipStateSender.endItem.uiEventOnClick,
+ chipStateSender.endItem.newState,
+ routeInfo,
+ )
+ } else {
+ null
+ }
+ }
+ },
+ vibrationEffect = chipStateSender.transferStatus.vibrationEffect,
+ )
+ }
+
+ /**
+ * Returns an undo button for the chip.
+ *
+ * When the button is clicked: [undoCallback] will be triggered, [uiEvent] will be logged, and
+ * this coordinator will transition to [newState].
+ */
+ private fun getUndoButton(
+ undoCallback: IUndoMediaTransferCallback,
+ uiEvent: UiEventLogger.UiEventEnum,
+ @StatusBarManager.MediaTransferSenderState newState: Int,
+ routeInfo: MediaRoute2Info,
+ ): ChipbarEndItem.Button {
+ val onClickListener =
+ View.OnClickListener {
+ uiEventLogger.logUndoClicked(uiEvent)
+ undoCallback.onUndoTriggered()
+
+ // The external service should eventually send us a new TransferTriggered state, but
+ // but that may take too long to go through the binder and the user may be confused
+ // as to why the UI hasn't changed yet. So, we immediately change the UI here.
+ updateMediaTapToTransferSenderDisplay(
+ newState,
+ routeInfo,
+ // Since we're force-updating the UI, we don't have any [undoCallback] from the
+ // external service (and TransferTriggered states don't have undo callbacks
+ // anyway).
+ undoCallback = null,
+ )
+ }
+
+ return ChipbarEndItem.Button(
+ Text.Resource(R.string.media_transfer_undo),
+ onClickListener,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt
index 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/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index da9fefa..33021e3 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -48,7 +48,6 @@
import androidx.annotation.NonNull;
-import com.android.keyguard.KeyguardViewController;
import com.android.systemui.Dumpable;
import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
@@ -61,6 +60,7 @@
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
import com.android.systemui.statusbar.phone.CentralSurfaces;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -90,7 +90,7 @@
private final AccessibilityManager mAccessibilityManager;
private final Lazy<AssistManager> mAssistManagerLazy;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
- private final KeyguardViewController mKeyguardViewController;
+ private final KeyguardStateController mKeyguardStateController;
private final UserTracker mUserTracker;
private final SystemActions mSystemActions;
private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver;
@@ -125,7 +125,7 @@
OverviewProxyService overviewProxyService,
Lazy<AssistManager> assistManagerLazy,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
- KeyguardViewController keyguardViewController,
+ KeyguardStateController keyguardStateController,
NavigationModeController navigationModeController,
UserTracker userTracker,
DumpManager dumpManager) {
@@ -134,7 +134,7 @@
mAccessibilityManager = accessibilityManager;
mAssistManagerLazy = assistManagerLazy;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
- mKeyguardViewController = keyguardViewController;
+ mKeyguardStateController = keyguardStateController;
mUserTracker = userTracker;
mSystemActions = systemActions;
accessibilityManager.addAccessibilityServicesStateChangeListener(this);
@@ -326,7 +326,7 @@
shadeWindowView =
mCentralSurfacesOptionalLazy.get().get().getNotificationShadeWindowView();
}
- boolean isKeyguardShowing = mKeyguardViewController.isShowing();
+ boolean isKeyguardShowing = mKeyguardStateController.isShowing();
boolean imeVisibleOnShade = shadeWindowView != null && shadeWindowView.isAttachedToWindow()
&& shadeWindowView.getRootWindowInsets().isVisible(WindowInsets.Type.ime());
return imeVisibleOnShade
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 50a10bc..c089511 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -114,6 +114,7 @@
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope;
import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener;
@@ -211,6 +212,7 @@
private final NotificationShadeDepthController mNotificationShadeDepthController;
private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener;
private final UserContextProvider mUserContextProvider;
+ private final WakefulnessLifecycle mWakefulnessLifecycle;
private final RegionSamplingHelper mRegionSamplingHelper;
private final int mNavColorSampleMargin;
private NavigationBarFrame mFrame;
@@ -451,6 +453,28 @@
}
};
+ private final WakefulnessLifecycle.Observer mWakefulnessObserver =
+ new WakefulnessLifecycle.Observer() {
+ private void notifyScreenStateChanged(boolean isScreenOn) {
+ notifyNavigationBarScreenOn();
+ mView.onScreenStateChanged(isScreenOn);
+ }
+
+ @Override
+ public void onStartedWakingUp() {
+ notifyScreenStateChanged(true);
+ if (isGesturalModeOnDefaultDisplay(getContext(), mNavBarMode)) {
+ mRegionSamplingHelper.start(mSamplingBounds);
+ }
+ }
+
+ @Override
+ public void onFinishedGoingToSleep() {
+ notifyScreenStateChanged(false);
+ mRegionSamplingHelper.stop();
+ }
+ };
+
@Inject
NavigationBar(
NavigationBarView navigationBarView,
@@ -491,7 +515,8 @@
NavigationBarTransitions navigationBarTransitions,
EdgeBackGestureHandler edgeBackGestureHandler,
Optional<BackAnimation> backAnimation,
- UserContextProvider userContextProvider) {
+ UserContextProvider userContextProvider,
+ WakefulnessLifecycle wakefulnessLifecycle) {
super(navigationBarView);
mFrame = navigationBarFrame;
mContext = context;
@@ -529,6 +554,7 @@
mTelecomManagerOptional = telecomManagerOptional;
mInputMethodManager = inputMethodManager;
mUserContextProvider = userContextProvider;
+ mWakefulnessLifecycle = wakefulnessLifecycle;
mNavColorSampleMargin = getResources()
.getDimensionPixelSize(R.dimen.navigation_handle_sample_horizontal_margin);
@@ -653,7 +679,9 @@
public void onViewAttached() {
final Display display = mView.getDisplay();
mView.setComponents(mRecentsOptional);
- mView.setComponents(mCentralSurfacesOptionalLazy.get().get().getPanelController());
+ if (mCentralSurfacesOptionalLazy.get().isPresent()) {
+ mView.setComponents(mCentralSurfacesOptionalLazy.get().get().getPanelController());
+ }
mView.setDisabledFlags(mDisabledFlags1, mSysUiFlagsContainer);
mView.setOnVerticalChangedListener(this::onVerticalChanged);
mView.setOnTouchListener(this::onNavigationTouch);
@@ -682,11 +710,10 @@
prepareNavigationBarView();
checkNavBarModes();
- IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
- filter.addAction(Intent.ACTION_SCREEN_ON);
- filter.addAction(Intent.ACTION_USER_SWITCHED);
+ IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
mBroadcastDispatcher.registerReceiverWithHandler(mBroadcastReceiver, filter,
Handler.getMain(), UserHandle.ALL);
+ mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
notifyNavigationBarScreenOn();
mOverviewProxyService.addCallback(mOverviewProxyListener);
@@ -737,6 +764,7 @@
getBarTransitions().destroy();
mOverviewProxyService.removeCallback(mOverviewProxyListener);
mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver);
+ mWakefulnessLifecycle.removeObserver(mWakefulnessObserver);
if (mOrientationHandle != null) {
resetSecondaryHandle();
getBarTransitions().removeDarkIntensityListener(mOrientationHandleIntensityListener);
@@ -1619,19 +1647,6 @@
return;
}
String action = intent.getAction();
- if (Intent.ACTION_SCREEN_OFF.equals(action)
- || Intent.ACTION_SCREEN_ON.equals(action)) {
- notifyNavigationBarScreenOn();
- boolean isScreenOn = Intent.ACTION_SCREEN_ON.equals(action);
- mView.onScreenStateChanged(isScreenOn);
- if (isScreenOn) {
- if (isGesturalModeOnDefaultDisplay(getContext(), mNavBarMode)) {
- mRegionSamplingHelper.start(mSamplingBounds);
- }
- } else {
- mRegionSamplingHelper.stop();
- }
- }
if (Intent.ACTION_USER_SWITCHED.equals(action)) {
// The accessibility settings may be different for the new user
updateAccessibilityStateFlags();
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
index 029cf68..3fd1aa7 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
@@ -21,6 +21,7 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
import static android.view.Display.DEFAULT_DISPLAY;
+import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG;
import static com.android.systemui.shared.recents.utilities.Utilities.isTablet;
import android.content.ContentResolver;
@@ -141,7 +142,13 @@
public void onConfigChanged(Configuration newConfig) {
boolean isOldConfigTablet = mIsTablet;
mIsTablet = isTablet(mContext);
+ boolean willApplyConfig = mConfigChanges.applyNewConfig(mContext.getResources());
boolean largeScreenChanged = mIsTablet != isOldConfigTablet;
+ // TODO(b/243765256): Disable this logging once b/243765256 is fixed.
+ Log.d(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig
+ + " mTaskbarDelegate initialized=" + mTaskbarDelegate.isInitialized()
+ + " willApplyConfigToNavbars=" + willApplyConfig
+ + " navBarCount=" + mNavigationBars.size());
if (mTaskbarDelegate.isInitialized()) {
mTaskbarDelegate.onConfigurationChanged(newConfig);
}
@@ -150,7 +157,7 @@
return;
}
- if (mConfigChanges.applyNewConfig(mContext.getResources())) {
+ if (willApplyConfig) {
for (int i = 0; i < mNavigationBars.size(); i++) {
recreateNavigationBar(mNavigationBars.keyAt(i));
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
index 9702488..403d276 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
@@ -148,6 +148,7 @@
private NavigationBarInflaterView mNavigationInflaterView;
private Optional<Recents> mRecentsOptional = Optional.empty();
+ @Nullable
private NotificationPanelViewController mPanelView;
private RotationContextButton mRotationContextButton;
private FloatingRotationButton mFloatingRotationButton;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index 6424256..6e927b0 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -417,6 +417,7 @@
stretchEntryBackIndicator(preThresholdStretchProgress(xTranslation))
GestureState.INACTIVE ->
mView.resetStretch()
+ else -> {}
}
// set y translation
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index a8799c7..709467f 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -113,7 +113,7 @@
private static final int MAX_NUM_LOGGED_GESTURES = 10;
static final boolean DEBUG_MISSING_GESTURE = false;
- static final String DEBUG_MISSING_GESTURE_TAG = "NoBackGesture";
+ public static final String DEBUG_MISSING_GESTURE_TAG = "NoBackGesture";
private ISystemGestureExclusionListener mGestureExclusionListener =
new ISystemGestureExclusionListener.Stub() {
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
index 67dae9e..1da866e 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
@@ -46,6 +46,7 @@
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -59,7 +60,7 @@
import dagger.Lazy;
@SysUISingleton
-public class PowerUI extends CoreStartable implements CommandQueue.Callbacks {
+public class PowerUI implements CoreStartable, CommandQueue.Callbacks {
static final String TAG = "PowerUI";
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -78,6 +79,7 @@
private final PowerManager mPowerManager;
private final WarningsUI mWarnings;
+ private final WakefulnessLifecycle mWakefulnessLifecycle;
private InattentiveSleepWarningView mOverlayView;
private final Configuration mLastConfiguration = new Configuration();
private int mPlugType = 0;
@@ -103,22 +105,37 @@
private IThermalEventListener mSkinThermalEventListener;
private IThermalEventListener mUsbThermalEventListener;
+ private final Context mContext;
private final BroadcastDispatcher mBroadcastDispatcher;
private final CommandQueue mCommandQueue;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
+ private final WakefulnessLifecycle.Observer mWakefulnessObserver =
+ new WakefulnessLifecycle.Observer() {
+ @Override
+ public void onStartedWakingUp() {
+ mScreenOffTime = -1;
+ }
+
+ @Override
+ public void onFinishedGoingToSleep() {
+ mScreenOffTime = SystemClock.elapsedRealtime();
+ }
+ };
@Inject
public PowerUI(Context context, BroadcastDispatcher broadcastDispatcher,
CommandQueue commandQueue, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
WarningsUI warningsUI, EnhancedEstimates enhancedEstimates,
+ WakefulnessLifecycle wakefulnessLifecycle,
PowerManager powerManager) {
- super(context);
+ mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
mCommandQueue = commandQueue;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
mWarnings = warningsUI;
mEnhancedEstimates = enhancedEstimates;
mPowerManager = powerManager;
+ mWakefulnessLifecycle = wakefulnessLifecycle;
}
public void start() {
@@ -137,6 +154,7 @@
false, obs, UserHandle.USER_ALL);
updateBatteryWarningLevels();
mReceiver.init();
+ mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
// Check to see if we need to let the user know that the phone previously shut down due
// to the temperature being too high.
@@ -169,7 +187,7 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(Configuration newConfig) {
final int mask = ActivityInfo.CONFIG_MCC | ActivityInfo.CONFIG_MNC;
// Safe to modify mLastConfiguration here as it's only updated by the main thread (here).
@@ -232,8 +250,6 @@
IntentFilter filter = new IntentFilter();
filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
- filter.addAction(Intent.ACTION_SCREEN_OFF);
- filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_USER_SWITCHED);
mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mHandler);
// Force get initial values. Relying on Sticky behavior until API for getting info.
@@ -316,10 +332,6 @@
plugged, bucket);
});
- } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
- mScreenOffTime = SystemClock.elapsedRealtime();
- } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
- mScreenOffTime = -1;
} else if (Intent.ACTION_USER_SWITCHED.equals(action)) {
mWarnings.userSwitched();
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
index 1ea9347..03503fd 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
@@ -17,10 +17,10 @@
package com.android.systemui.privacy.logging
import android.permission.PermissionGroupUsage
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogMessage
import com.android.systemui.log.dagger.PrivacyLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogMessage
import com.android.systemui.privacy.PrivacyDialog
import com.android.systemui.privacy.PrivacyItem
import java.text.SimpleDateFormat
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java b/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java
index 5510eb1..cd32a10 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java
+++ b/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java
@@ -67,7 +67,7 @@
* recording audio, accessing the camera or accessing the location.
*/
@SysUISingleton
-public class TvOngoingPrivacyChip extends CoreStartable implements PrivacyItemController.Callback,
+public class TvOngoingPrivacyChip implements CoreStartable, PrivacyItemController.Callback,
PrivacyChipDrawable.PrivacyChipDrawableListener {
private static final String TAG = "TvOngoingPrivacyChip";
private static final boolean DEBUG = false;
@@ -134,7 +134,6 @@
@Inject
public TvOngoingPrivacyChip(Context context, PrivacyItemController privacyItemController,
IWindowManager iWindowManager) {
- super(context);
if (DEBUG) Log.d(TAG, "Privacy chip running");
mContext = context;
mPrivacyItemController = privacyItemController;
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 1ef6426..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;
@@ -691,6 +691,15 @@
if (mQSAnimator != null) {
mQSAnimator.setPosition(expansion);
}
+ if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD
+ || mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) {
+ // At beginning, state is 0 and will apply wrong squishiness to MediaHost in lockscreen
+ // and media player expect no change by squishiness in lock screen shade
+ mQsMediaHost.setSquishFraction(1.0F);
+ } else {
+ mQsMediaHost.setSquishFraction(mSquishinessFraction);
+ }
+
}
private void setAlphaAnimationProgress(float progress) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
index 8544f61..c663aa6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
@@ -1,8 +1,8 @@
package com.android.systemui.qs
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.QSFragmentDisableLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.statusbar.DisableFlagsLogger
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 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/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index 84d7e65..27d9da6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -25,6 +25,7 @@
import android.util.AttributeSet;
import android.util.Pair;
import android.view.DisplayCutout;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -231,6 +232,16 @@
}
}
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // If using combined headers, only react to touches inside QuickQSPanel
+ if (!mUseCombinedQSHeader || event.getY() > mHeaderQsPanel.getTop()) {
+ return super.onTouchEvent(event);
+ } else {
+ return false;
+ }
+ }
+
void updateResources() {
Resources resources = mContext.getResources();
boolean largeScreenHeaderActive =
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/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index 3e445dd..d393680 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -36,6 +36,7 @@
import android.util.Log;
import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Main;
@@ -182,6 +183,10 @@
setBindService(true);
}
+ /**
+ * Binds or unbinds to IQSService
+ */
+ @WorkerThread
public void setBindService(boolean bind) {
if (mBound && mUnbindImmediate) {
// If we are already bound and expecting to unbind, this means we should stay bound
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
index 4cacbba..5d03da3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
@@ -35,6 +35,7 @@
import com.android.internal.statusbar.StatusBarIcon;
import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.settings.UserTracker;
@@ -53,6 +54,7 @@
/**
* Runs the day-to-day operations of which tiles should be bound and when.
*/
+@SysUISingleton
public class TileServices extends IQSService.Stub {
static final int DEFAULT_MAX_BOUND = 3;
static final int REDUCED_MAX_BOUND = 1;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
index 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 11d9555..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,12 +18,10 @@
import android.content.Context
import android.util.Log
-import android.view.View
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import com.android.settingslib.Utils
-import com.android.settingslib.drawable.UserIconDrawable
import com.android.systemui.R
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription
@@ -200,72 +198,70 @@
*/
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(
status: UserSwitcherStatusModel.Enabled
): FooterActionsButtonViewModel {
val icon = status.currentUserImage!!
- val iconTint =
- if (status.isGuestUser && icon !is UserIconDrawable) {
- Utils.getColorAttrDefaultColor(context, android.R.attr.colorForeground)
- } else {
- null
- }
return FooterActionsButtonViewModel(
id = R.id.multi_user_switch,
- Icon.Loaded(
- icon,
- ContentDescription.Loaded(userSwitcherContentDescription(status.currentUserName)),
- ),
- iconTint,
- R.drawable.qs_footer_action_circle,
- this::onUserSwitcherClicked,
+ icon =
+ Icon.Loaded(
+ icon,
+ ContentDescription.Loaded(
+ userSwitcherContentDescription(status.currentUserName)
+ ),
+ ),
+ iconTint = null,
+ background = R.drawable.qs_footer_action_circle,
+ onClick = this::onUserSwitcherClicked,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index 6038006..931dc8d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -17,12 +17,12 @@
package com.android.systemui.qs.logging
import android.service.quicksettings.Tile
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogMessage
import com.android.systemui.log.dagger.QSLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogMessage
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.statusbar.StatusBarState
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto
new file mode 100644
index 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/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index d2d5063..57a00c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -26,6 +26,7 @@
import android.view.View;
import android.view.ViewGroup;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
@@ -43,6 +44,9 @@
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.user.data.source.UserRecord;
+import java.util.List;
+import java.util.stream.Collectors;
+
import javax.inject.Inject;
/**
@@ -83,6 +87,13 @@
private final FalsingManager mFalsingManager;
private @Nullable UserSwitchDialogController.DialogShower mDialogShower;
+ @NonNull
+ @Override
+ protected List<UserRecord> getUsers() {
+ return super.getUsers().stream().filter(
+ userRecord -> !userRecord.isManageUsers).collect(Collectors.toList());
+ }
+
@Inject
public Adapter(Context context, UserSwitcherController controller,
UiEventLogger uiEventLogger, FalsingManager falsingManager) {
@@ -193,6 +204,15 @@
Trace.endSection();
}
+ @Override
+ public void onUserListItemClicked(@NonNull UserRecord record,
+ @Nullable UserSwitchDialogController.DialogShower dialogShower) {
+ if (dialogShower != null) {
+ mDialogShower.dismiss();
+ }
+ super.onUserListItemClicked(record, dialogShower);
+ }
+
public void linkToViewGroup(ViewGroup viewGroup) {
PseudoGridView.ViewGroupAdapterBridge.link(viewGroup, this);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
index 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/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
index 9b3b843..b041f95 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
@@ -29,13 +29,14 @@
/**
* A proxy to a Recents implementation.
*/
-public class Recents extends CoreStartable implements CommandQueue.Callbacks {
+public class Recents implements CoreStartable, CommandQueue.Callbacks {
+ private final Context mContext;
private final RecentsImplementation mImpl;
private final CommandQueue mCommandQueue;
public Recents(Context context, RecentsImplementation impl, CommandQueue commandQueue) {
- super(context);
+ mContext = context;
mImpl = impl;
mCommandQueue = commandQueue;
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
new file mode 100644
index 0000000..017e57f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.content.ClipData
+import android.content.ClipDescription
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import com.android.systemui.R
+
+object ActionIntentCreator {
+ /** @return a chooser intent to share the given URI with the optional provided subject. */
+ fun createShareIntent(uri: Uri, subject: String?): Intent {
+ // Create a share intent, this will always go through the chooser activity first
+ // which should not trigger auto-enter PiP
+ val sharingIntent =
+ Intent(Intent.ACTION_SEND).apply {
+ setDataAndType(uri, "image/png")
+ putExtra(Intent.EXTRA_STREAM, uri)
+
+ // Include URI in ClipData also, so that grantPermission picks it up.
+ // We don't use setData here because some apps interpret this as "to:".
+ clipData =
+ ClipData(
+ ClipDescription("content", arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)),
+ ClipData.Item(uri)
+ )
+
+ putExtra(Intent.EXTRA_SUBJECT, subject)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ }
+
+ return Intent.createChooser(sharingIntent, null)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+
+ /**
+ * @return an ACTION_EDIT intent for the given URI, directed to config_screenshotEditor if
+ * available.
+ */
+ fun createEditIntent(uri: Uri, context: Context): Intent {
+ val editIntent = Intent(Intent.ACTION_EDIT)
+
+ context.getString(R.string.config_screenshotEditor)?.let {
+ if (it.isNotEmpty()) {
+ editIntent.component = ComponentName.unflattenFromString(it)
+ }
+ }
+
+ return editIntent
+ .setDataAndType(uri, "image/png")
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
new file mode 100644
index 0000000..5961635
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.screenshot
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.RemoteException
+import android.os.UserHandle
+import android.util.Log
+import android.view.Display
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.IRemoteAnimationRunner
+import android.view.RemoteAnimationAdapter
+import android.view.RemoteAnimationTarget
+import android.view.WindowManager
+import android.view.WindowManagerGlobal
+import com.android.internal.infra.ServiceConnector
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import javax.inject.Inject
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class ActionIntentExecutor
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ private val context: Context,
+) {
+ /**
+ * Execute the given intent with startActivity while performing operations for screenshot action
+ * launching.
+ * - Dismiss the keyguard first
+ * - If the userId is not the current user, proxy to a service running as that user to execute
+ * - After startActivity, optionally override the pending app transition.
+ */
+ fun launchIntentAsync(
+ intent: Intent,
+ bundle: Bundle,
+ userId: Int,
+ overrideTransition: Boolean,
+ ) {
+ applicationScope.launch { launchIntent(intent, bundle, userId, overrideTransition) }
+ }
+
+ suspend fun launchIntent(
+ intent: Intent,
+ bundle: Bundle,
+ userId: Int,
+ overrideTransition: Boolean,
+ ) {
+ withContext(bgDispatcher) {
+ dismissKeyguard()
+
+ if (userId == UserHandle.myUserId()) {
+ context.startActivity(intent, bundle)
+ } else {
+ launchCrossProfileIntent(userId, intent, bundle)
+ }
+
+ if (overrideTransition) {
+ val runner = RemoteAnimationAdapter(SCREENSHOT_REMOTE_RUNNER, 0, 0)
+ try {
+ WindowManagerGlobal.getWindowManagerService()
+ .overridePendingAppTransitionRemote(runner, Display.DEFAULT_DISPLAY)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error overriding screenshot app transition", e)
+ }
+ }
+ }
+ }
+
+ private val proxyConnector: ServiceConnector<IScreenshotProxy> =
+ ServiceConnector.Impl(
+ context,
+ Intent(context, ScreenshotProxyService::class.java),
+ Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE,
+ context.userId,
+ IScreenshotProxy.Stub::asInterface,
+ )
+
+ private suspend fun dismissKeyguard() {
+ val completion = CompletableDeferred<Unit>()
+ val onDoneBinder =
+ object : IOnDoneCallback.Stub() {
+ override fun onDone(success: Boolean) {
+ completion.complete(Unit)
+ }
+ }
+ proxyConnector.post { it.dismissKeyguard(onDoneBinder) }
+ completion.await()
+ }
+
+ private fun getCrossProfileConnector(userId: Int): ServiceConnector<ICrossProfileService> =
+ ServiceConnector.Impl<ICrossProfileService>(
+ context,
+ Intent(context, ScreenshotCrossProfileService::class.java),
+ Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE,
+ userId,
+ ICrossProfileService.Stub::asInterface,
+ )
+
+ private suspend fun launchCrossProfileIntent(userId: Int, intent: Intent, bundle: Bundle) {
+ val connector = getCrossProfileConnector(userId)
+ val completion = CompletableDeferred<Unit>()
+ connector.post {
+ it.launchIntent(intent, bundle)
+ completion.complete(Unit)
+ }
+ completion.await()
+ }
+}
+
+private const val TAG: String = "ActionIntentExecutor"
+private const val SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"
+
+/**
+ * This is effectively a no-op, but we need something non-null to pass in, in order to successfully
+ * override the pending activity entrance animation.
+ */
+private val SCREENSHOT_REMOTE_RUNNER: IRemoteAnimationRunner.Stub =
+ object : IRemoteAnimationRunner.Stub() {
+ override fun onAnimationStart(
+ @WindowManager.TransitionOldType transit: Int,
+ apps: Array<RemoteAnimationTarget>,
+ wallpapers: Array<RemoteAnimationTarget>,
+ nonApps: Array<RemoteAnimationTarget>,
+ finishedCallback: IRemoteAnimationFinishedCallback,
+ ) {
+ try {
+ finishedCallback.onAnimationFinished()
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Error finishing screenshot remote animation", e)
+ }
+ }
+
+ override fun onAnimationCancelled(isKeyguardOccluded: Boolean) {}
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java b/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java
index 950806d..ead3b7b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java
@@ -49,7 +49,6 @@
private final SwipeDismissHandler mSwipeDismissHandler;
private final GestureDetector mSwipeDetector;
private View mActionsContainer;
- private View mActionsContainerBackground;
private SwipeDismissCallbacks mCallbacks;
private final DisplayMetrics mDisplayMetrics;
@@ -111,6 +110,9 @@
}
});
mSwipeDetector.setIsLongpressEnabled(false);
+
+ mCallbacks = new SwipeDismissCallbacks() {
+ }; // default to unimplemented callbacks
}
public void setCallbacks(SwipeDismissCallbacks callbacks) {
@@ -119,16 +121,13 @@
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
- if (mCallbacks != null) {
- mCallbacks.onInteraction();
- }
+ mCallbacks.onInteraction();
return super.onInterceptHoverEvent(event);
}
@Override // View
protected void onFinishInflate() {
mActionsContainer = findViewById(R.id.actions_container);
- mActionsContainerBackground = findViewById(R.id.actions_container_background);
}
@Override
@@ -186,6 +185,13 @@
inoutInfo.touchableRegion.set(r);
}
+ private int getBackgroundRight() {
+ // background expected to be null in testing.
+ // animation may have unexpected behavior if view is not present
+ View background = findViewById(R.id.actions_container_background);
+ return background == null ? 0 : background.getRight();
+ }
+
/**
* Allows a view to be swipe-dismissed, or returned to its location if distance threshold is not
* met
@@ -213,8 +219,6 @@
mGestureDetector = new GestureDetector(context, gestureListener);
mDisplayMetrics = new DisplayMetrics();
context.getDisplay().getRealMetrics(mDisplayMetrics);
- mCallbacks = new SwipeDismissCallbacks() {
- }; // default to unimplemented callbacks
}
@Override
@@ -230,7 +234,9 @@
return true;
}
if (isPastDismissThreshold()) {
- dismiss();
+ ValueAnimator anim = createSwipeDismissAnimation();
+ mCallbacks.onSwipeDismissInitiated(anim);
+ dismiss(anim);
} else {
// if we've moved, but not past the threshold, start the return animation
if (DEBUG_DISMISS) {
@@ -295,10 +301,7 @@
}
void dismiss() {
- float velocityPxPerMs = FloatingWindowUtil.dpToPx(mDisplayMetrics, VELOCITY_DP_PER_MS);
- ValueAnimator anim = createSwipeDismissAnimation(velocityPxPerMs);
- mCallbacks.onSwipeDismissInitiated(anim);
- dismiss(anim);
+ dismiss(createSwipeDismissAnimation());
}
private void dismiss(ValueAnimator animator) {
@@ -323,6 +326,11 @@
mDismissAnimation.start();
}
+ private ValueAnimator createSwipeDismissAnimation() {
+ float velocityPxPerMs = FloatingWindowUtil.dpToPx(mDisplayMetrics, VELOCITY_DP_PER_MS);
+ return createSwipeDismissAnimation(velocityPxPerMs);
+ }
+
private ValueAnimator createSwipeDismissAnimation(float velocity) {
// velocity is measured in pixels per millisecond
velocity = Math.min(3, Math.max(1, velocity));
@@ -337,7 +345,7 @@
if (startX > 0 || (startX == 0 && layoutDir == LAYOUT_DIRECTION_RTL)) {
finalX = mDisplayMetrics.widthPixels;
} else {
- finalX = -1 * mActionsContainerBackground.getRight();
+ finalX = -1 * getBackgroundRight();
}
float distance = Math.abs(finalX - startX);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ICrossProfileService.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/ICrossProfileService.aidl
new file mode 100644
index 0000000..da83472
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ICrossProfileService.aidl
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2009, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.screenshot;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+
+/** Interface implemented by ScreenshotCrossProfileService */
+interface ICrossProfileService {
+
+ void launchIntent(in Intent intent, in Bundle bundle);
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/IOnDoneCallback.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/IOnDoneCallback.aidl
new file mode 100644
index 0000000..e15030f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/IOnDoneCallback.aidl
@@ -0,0 +1,21 @@
+/**
+ * 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.screenshot;
+
+interface IOnDoneCallback {
+ void onDone(boolean success);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl
index f7c4dad..d2e3fbd 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl
@@ -16,9 +16,14 @@
package com.android.systemui.screenshot;
+import com.android.systemui.screenshot.IOnDoneCallback;
+
/** Interface implemented by ScreenshotProxyService */
interface IScreenshotProxy {
/** Is the notification shade currently exanded? */
boolean isNotificationShadeExpanded();
-}
\ No newline at end of file
+
+ /** Attempts to dismiss the keyguard. */
+ void dismissKeyguard(IOnDoneCallback callback);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
index 309059f..95cc0dc 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
@@ -76,7 +76,7 @@
)
} else {
// Create a new request of the same type which includes the top component
- ScreenshotRequest(request.source, request.type, info.component)
+ ScreenshotRequest(request.type, request.source, info.component)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index 077ad35..7143ba2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -173,6 +173,7 @@
mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri);
mImageData.quickShareAction = createQuickShareAction(mContext,
mQuickShareData.quickShareAction, uri);
+ mImageData.subject = getSubjectString();
mParams.mActionsReadyListener.onActionsReady(mImageData);
if (DEBUG_CALLBACK) {
@@ -237,8 +238,6 @@
// Create a share intent, this will always go through the chooser activity first
// which should not trigger auto-enter PiP
- String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
- String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
Intent sharingIntent = new Intent(Intent.ACTION_SEND);
sharingIntent.setDataAndType(uri, "image/png");
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
@@ -248,7 +247,7 @@
new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
new ClipData.Item(uri));
sharingIntent.setClipData(clipdata);
- sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
+ sharingIntent.putExtra(Intent.EXTRA_SUBJECT, getSubjectString());
sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
@@ -318,7 +317,7 @@
// by setting the (otherwise unused) request code to the current user id.
int requestCode = mContext.getUserId();
- // Create a edit action
+ // Create an edit action
PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode,
new Intent(context, ActionProxyReceiver.class)
.putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent)
@@ -479,4 +478,9 @@
mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
}
}
+
+ private String getSubjectString() {
+ String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
+ return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 704e115..d524a35 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -20,6 +20,7 @@
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
+import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY;
import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
@@ -28,6 +29,7 @@
import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW;
import static com.android.systemui.screenshot.LogConfig.logTag;
import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER;
+import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT;
import static java.util.Objects.requireNonNull;
@@ -173,7 +175,7 @@
public List<Notification.Action> smartActions;
public Notification.Action quickShareAction;
public UserHandle owner;
-
+ public String subject; // Title for sharing
/**
* POD for shared element transition.
@@ -194,6 +196,7 @@
deleteAction = null;
smartActions = null;
quickShareAction = null;
+ subject = null;
}
}
@@ -272,6 +275,7 @@
private final ScreenshotNotificationSmartActionsProvider
mScreenshotNotificationSmartActionsProvider;
private final TimeoutHandler mScreenshotHandler;
+ private final ActionIntentExecutor mActionExecutor;
private ScreenshotView mScreenshotView;
private Bitmap mScreenBitmap;
@@ -309,7 +313,8 @@
ActivityManager activityManager,
TimeoutHandler timeoutHandler,
BroadcastSender broadcastSender,
- ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider
+ ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
+ ActionIntentExecutor actionExecutor
) {
mScreenshotSmartActions = screenshotSmartActions;
mNotificationsController = screenshotNotificationsController;
@@ -331,9 +336,7 @@
if (DEBUG_UI) {
Log.d(TAG, "Corner timeout hit");
}
- mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT, 0,
- mPackageName);
- ScreenshotController.this.dismissScreenshot(false);
+ dismissScreenshot(SCREENSHOT_INTERACTION_TIMEOUT);
});
mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
@@ -341,6 +344,7 @@
mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
mWindowManager = mContext.getSystemService(WindowManager.class);
mFlags = flags;
+ mActionExecutor = actionExecutor;
mAccessibilityManager = AccessibilityManager.getInstance(mContext);
@@ -361,8 +365,7 @@
@Override
public void onReceive(Context context, Intent intent) {
if (ClipboardOverlayController.COPY_OVERLAY_ACTION.equals(intent.getAction())) {
- mUiEventLogger.log(SCREENSHOT_DISMISSED_OTHER);
- dismissScreenshot(false);
+ dismissScreenshot(SCREENSHOT_DISMISSED_OTHER);
}
}
};
@@ -410,24 +413,20 @@
/**
* Clears current screenshot
*/
- void dismissScreenshot(boolean immediate) {
+ void dismissScreenshot(ScreenshotEvent event) {
if (DEBUG_DISMISS) {
- Log.d(TAG, "dismissScreenshot(immediate=" + immediate + ")");
+ Log.d(TAG, "dismissScreenshot");
}
// If we're already animating out, don't restart the animation
- // (but do obey an immediate dismissal)
- if (!immediate && mScreenshotView.isDismissing()) {
+ if (mScreenshotView.isDismissing()) {
if (DEBUG_DISMISS) {
Log.v(TAG, "Already dismissing, ignoring duplicate command");
}
return;
}
+ mUiEventLogger.log(event, 0, mPackageName);
mScreenshotHandler.cancelTimeout();
- if (immediate) {
- finishDismiss();
- } else {
- mScreenshotView.animateDismissal();
- }
+ mScreenshotView.animateDismissal();
}
boolean isPendingSharedTransition() {
@@ -492,7 +491,7 @@
// TODO(159460485): Remove this when focus is handled properly in the system
setWindowFocusable(false);
}
- });
+ }, mActionExecutor, mFlags);
mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis());
mScreenshotView.setOnKeyListener((v, keyCode, event) -> {
@@ -500,7 +499,7 @@
if (DEBUG_INPUT) {
Log.d(TAG, "onKeyEvent: KeyEvent.KEYCODE_BACK");
}
- dismissScreenshot(false);
+ dismissScreenshot(SCREENSHOT_DISMISSED_OTHER);
return true;
}
return false;
@@ -636,6 +635,11 @@
return true;
}
});
+
+ if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+ mScreenshotView.badgeScreenshot(
+ mContext.getPackageManager().getUserBadgeForDensity(owner, 0));
+ }
mScreenshotView.setScreenshot(mScreenBitmap, screenInsets);
if (DEBUG_WINDOW) {
Log.d(TAG, "setContentView: " + mScreenshotView);
@@ -1040,7 +1044,7 @@
private boolean isUserSetupComplete(UserHandle owner) {
return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
- .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+ .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt
new file mode 100644
index 0000000..2e6c756
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt
@@ -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.
+ */
+
+package com.android.systemui.screenshot
+
+import android.app.Service
+import android.content.Intent
+import android.os.Bundle
+import android.os.IBinder
+import android.util.Log
+
+/**
+ * If a screenshot is saved to the work profile, any intents that grant access to the screenshot
+ * must come from a service running as the work profile user. This service is meant to be started as
+ * the desired user and just startActivity for the given intent.
+ */
+class ScreenshotCrossProfileService : Service() {
+
+ private val mBinder: IBinder =
+ object : ICrossProfileService.Stub() {
+ override fun launchIntent(intent: Intent, bundle: Bundle) {
+ startActivity(intent, bundle)
+ }
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ Log.d(TAG, "onBind: $intent")
+ return mBinder
+ }
+
+ companion object {
+ const val TAG = "ScreenshotProxyService"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
index 9654e03..c41e2bc 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
@@ -19,14 +19,17 @@
import android.content.Intent
import android.os.IBinder
import android.util.Log
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.statusbar.phone.CentralSurfaces
+import java.util.Optional
import javax.inject.Inject
/**
* Provides state from the main SystemUI process on behalf of the Screenshot process.
*/
internal class ScreenshotProxyService @Inject constructor(
- private val mExpansionMgr: PanelExpansionStateManager
+ private val mExpansionMgr: ShadeExpansionStateManager,
+ private val mCentralSurfacesOptional: Optional<CentralSurfaces>,
) : Service() {
private val mBinder: IBinder = object : IScreenshotProxy.Stub() {
@@ -38,6 +41,20 @@
Log.d(TAG, "isNotificationShadeExpanded(): $expanded")
return expanded
}
+
+ override fun dismissKeyguard(callback: IOnDoneCallback) {
+ if (mCentralSurfacesOptional.isPresent) {
+ mCentralSurfacesOptional.get().executeRunnableDismissingKeyguard(
+ Runnable {
+ callback.onDone(true)
+ }, null,
+ true /* dismissShade */, true /* afterKeyguardGone */,
+ true /* deferred */
+ )
+ } else {
+ callback.onDone(false)
+ }
+ }
}
override fun onBind(intent: Intent): IBinder? {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index be41a6b..27331ae 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -74,7 +74,6 @@
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityManager;
-import android.view.animation.AccelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
@@ -87,6 +86,8 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.shared.system.InputMonitorCompat;
@@ -120,15 +121,9 @@
private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234;
private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100;
- private static final long SCREENSHOT_DISMISS_X_DURATION_MS = 350;
- private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 350;
- private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade
private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f;
- private static final float ROUNDED_CORNER_RADIUS = .25f;
private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
- private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator();
-
private final Resources mResources;
private final Interpolator mFastOutSlowIn;
private final DisplayMetrics mDisplayMetrics;
@@ -143,6 +138,7 @@
private ImageView mScrollingScrim;
private DraggableConstraintLayout mScreenshotStatic;
private ImageView mScreenshotPreview;
+ private ImageView mScreenshotBadge;
private View mScreenshotPreviewBorder;
private ImageView mScrollablePreview;
private ImageView mScreenshotFlash;
@@ -168,6 +164,8 @@
private final InteractionJankMonitor mInteractionJankMonitor;
private long mDefaultTimeoutOfTimeoutHandler;
+ private ActionIntentExecutor mActionExecutor;
+ private FeatureFlags mFlags;
private enum PendingInteraction {
PREVIEW,
@@ -351,6 +349,7 @@
mScreenshotPreviewBorder = requireNonNull(
findViewById(R.id.screenshot_preview_border));
mScreenshotPreview.setClipToOutline(true);
+ mScreenshotBadge = requireNonNull(findViewById(R.id.screenshot_badge));
mActionsContainerBackground = requireNonNull(findViewById(
R.id.actions_container_background));
@@ -422,9 +421,12 @@
* Note: must be called before any other (non-constructor) method or null pointer exceptions
* may occur.
*/
- void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks) {
+ void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks,
+ ActionIntentExecutor actionExecutor, FeatureFlags flags) {
mUiEventLogger = uiEventLogger;
mCallbacks = callbacks;
+ mActionExecutor = actionExecutor;
+ mFlags = flags;
}
void setScreenshot(Bitmap bitmap, Insets screenInsets) {
@@ -588,8 +590,11 @@
ValueAnimator borderFadeIn = ValueAnimator.ofFloat(0, 1);
borderFadeIn.setDuration(100);
- borderFadeIn.addUpdateListener((animation) ->
- mScreenshotPreviewBorder.setAlpha(animation.getAnimatedFraction()));
+ borderFadeIn.addUpdateListener((animation) -> {
+ float borderAlpha = animation.getAnimatedFraction();
+ mScreenshotPreviewBorder.setAlpha(borderAlpha);
+ mScreenshotBadge.setAlpha(borderAlpha);
+ });
if (showFlash) {
dropInAnimation.play(flashOutAnimator).after(flashInAnimator);
@@ -756,21 +761,49 @@
return animator;
}
+ void badgeScreenshot(Drawable badge) {
+ mScreenshotBadge.setImageDrawable(badge);
+ mScreenshotBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE);
+ }
+
void setChipIntents(ScreenshotController.SavedImageData imageData) {
mShareChip.setOnClickListener(v -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
- startSharedTransition(
- imageData.shareTransition.get());
+ if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+ prepareSharedTransition();
+ mActionExecutor.launchIntentAsync(
+ ActionIntentCreator.INSTANCE.createShareIntent(
+ imageData.uri, imageData.subject),
+ imageData.shareTransition.get().bundle,
+ imageData.owner.getIdentifier(), false);
+ } else {
+ startSharedTransition(imageData.shareTransition.get());
+ }
});
mEditChip.setOnClickListener(v -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName);
- startSharedTransition(
- imageData.editTransition.get());
+ if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+ prepareSharedTransition();
+ mActionExecutor.launchIntentAsync(
+ ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
+ imageData.editTransition.get().bundle,
+ imageData.owner.getIdentifier(), true);
+ } else {
+ startSharedTransition(imageData.editTransition.get());
+ }
});
mScreenshotPreview.setOnClickListener(v -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName);
- startSharedTransition(
- imageData.editTransition.get());
+ if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+ prepareSharedTransition();
+ mActionExecutor.launchIntentAsync(
+ ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
+ imageData.editTransition.get().bundle,
+ imageData.owner.getIdentifier(), true);
+ } else {
+ startSharedTransition(
+ imageData.editTransition.get());
+ }
});
if (mQuickShareChip != null) {
mQuickShareChip.setPendingIntent(imageData.quickShareAction.actionIntent,
@@ -997,6 +1030,9 @@
mScreenshotPreview.setVisibility(View.INVISIBLE);
mScreenshotPreview.setAlpha(1f);
mScreenshotPreviewBorder.setAlpha(0);
+ mScreenshotBadge.setAlpha(0f);
+ mScreenshotBadge.setVisibility(View.GONE);
+ mScreenshotBadge.setImageDrawable(null);
mPendingSharedTransition = false;
mActionsContainerBackground.setVisibility(View.GONE);
mActionsContainer.setVisibility(View.GONE);
@@ -1038,6 +1074,12 @@
}
}
+ private void prepareSharedTransition() {
+ mPendingSharedTransition = true;
+ // fade out non-preview UI
+ createScreenshotFadeDismissAnimation().start();
+ }
+
ValueAnimator createScreenshotFadeDismissAnimation() {
ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
alphaAnim.addUpdateListener(animation -> {
@@ -1046,6 +1088,7 @@
mActionsContainerBackground.setAlpha(alpha);
mActionsContainer.setAlpha(alpha);
mScreenshotPreviewBorder.setAlpha(alpha);
+ mScreenshotBadge.setAlpha(alpha);
});
alphaAnim.setDuration(600);
return alphaAnim;
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index a4a59ce..2176825 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -89,8 +89,7 @@
Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS");
}
if (!mScreenshot.isPendingSharedTransition()) {
- mUiEventLogger.log(SCREENSHOT_DISMISSED_OTHER);
- mScreenshot.dismissScreenshot(false);
+ mScreenshot.dismissScreenshot(SCREENSHOT_DISMISSED_OTHER);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
index ad073c0..d450afa 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
@@ -42,11 +42,11 @@
@SysUISingleton
class UserFileManagerImpl @Inject constructor(
// Context of system process and system user.
- val context: Context,
+ private val context: Context,
val userManager: UserManager,
val broadcastDispatcher: BroadcastDispatcher,
@Background val backgroundExecutor: DelayableExecutor
-) : UserFileManager, CoreStartable(context) {
+) : UserFileManager, CoreStartable {
companion object {
private const val FILES = "files"
@VisibleForTesting internal const val SHARED_PREFS = "shared_prefs"
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/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index d3ed474..6b540aa 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -280,6 +280,9 @@
context.getString(com.android.internal.R.string.status_bar_alarm_clock)
)
}
+ if (combinedHeaders) {
+ privacyIconsController.onParentVisible()
+ }
}
override fun onViewAttached() {
@@ -289,6 +292,7 @@
clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f
v.pivotX = newPivot
+ v.pivotY = v.height.toFloat() / 2
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt
index 07e8b9f..754036d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt
@@ -16,7 +16,7 @@
import android.view.MotionEvent
import com.android.systemui.dump.DumpsysTableLogger
import com.android.systemui.dump.Row
-import com.android.systemui.util.collection.RingBuffer
+import com.android.systemui.plugins.util.RingBuffer
import java.text.SimpleDateFormat
import java.util.Locale
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 1110386..ddb57f7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -17,6 +17,8 @@
package com.android.systemui.shade;
import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
import static androidx.constraintlayout.widget.ConstraintSet.END;
import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID;
@@ -26,8 +28,15 @@
import static com.android.keyguard.KeyguardClockSwitch.SMALL;
import static com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE;
import static com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE;
+import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
+import static com.android.systemui.classifier.Classifier.GENERIC;
import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
+import static com.android.systemui.classifier.Classifier.UNLOCK;
+import static com.android.systemui.shade.NotificationPanelView.DEBUG;
+import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
+import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
+import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPENING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
@@ -36,11 +45,10 @@
import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
-import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED;
-import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN;
-import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPENING;
import static com.android.systemui.util.DumpUtilsKt.asIndenting;
+import static java.lang.Float.isNaN;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
@@ -48,6 +56,8 @@
import android.app.Fragment;
import android.app.StatusBarManager;
import android.content.ContentResolver;
+import android.content.res.Configuration;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -69,15 +79,20 @@
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;
+import android.view.InputDevice;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.View.AccessibilityDelegate;
+import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.ViewStub;
@@ -86,6 +101,7 @@
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
@@ -131,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;
@@ -178,6 +196,7 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.statusbar.phone.BounceInterpolator;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
@@ -202,8 +221,6 @@
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
-import com.android.systemui.statusbar.phone.panelstate.PanelState;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -232,8 +249,13 @@
import javax.inject.Provider;
@CentralSurfacesComponent.CentralSurfacesScope
-public final class NotificationPanelViewController extends PanelViewController {
+public final class NotificationPanelViewController {
+ public static final String TAG = NotificationPanelView.class.getSimpleName();
+ public static final float FLING_MAX_LENGTH_SECONDS = 0.6f;
+ public static final float FLING_SPEED_UP_FACTOR = 0.6f;
+ public static final float FLING_CLOSING_MAX_LENGTH_SECONDS = 0.6f;
+ public static final float FLING_CLOSING_SPEED_UP_FACTOR = 0.6f;
private static final boolean DEBUG_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
private static final boolean SPEW_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
private static final boolean DEBUG_DRAWABLE = false;
@@ -264,6 +286,22 @@
ActivityLaunchAnimator.TIMINGS.getTotalDuration()
- CollapsedStatusBarFragment.FADE_IN_DURATION
- CollapsedStatusBarFragment.FADE_IN_DELAY - 48;
+ private static final int NO_FIXED_DURATION = -1;
+ private static final long SHADE_OPEN_SPRING_OUT_DURATION = 350L;
+ private static final long SHADE_OPEN_SPRING_BACK_DURATION = 400L;
+ /**
+ * The factor of the usual high velocity that is needed in order to reach the maximum overshoot
+ * when flinging. A low value will make it that most flings will reach the maximum overshoot.
+ */
+ private static final float FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT = 0.5f;
+ private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
+ private final Resources mResources;
+ private final KeyguardStateController mKeyguardStateController;
+ private final SysuiStatusBarStateController mStatusBarStateController;
+ private final AmbientState mAmbientState;
+ private final LockscreenGestureLogger mLockscreenGestureLogger;
+ private final SystemClock mSystemClock;
+ private final ShadeLogger mShadeLog;
private final DozeParameters mDozeParameters;
private final OnHeightChangedListener mOnHeightChangedListener = new OnHeightChangedListener();
@@ -335,6 +373,28 @@
private final LargeScreenShadeHeaderController mLargeScreenShadeHeaderController;
private final RecordingController mRecordingController;
private final PanelEventsEmitter mPanelEventsEmitter;
+ private final boolean mVibrateOnOpening;
+ private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+ private final FlingAnimationUtils mFlingAnimationUtilsClosing;
+ private final FlingAnimationUtils mFlingAnimationUtilsDismissing;
+ private final LatencyTracker mLatencyTracker;
+ private final DozeLog mDozeLog;
+ /** Whether or not the NotificationPanelView can be expanded or collapsed with a drag. */
+ private final boolean mNotificationsDragEnabled;
+ private final Interpolator mBounceInterpolator;
+ private final NotificationShadeWindowController mNotificationShadeWindowController;
+ private final ShadeExpansionStateManager mShadeExpansionStateManager;
+ private long mDownTime;
+ private boolean mTouchSlopExceededBeforeDown;
+ private boolean mIsLaunchAnimationRunning;
+ private float mOverExpansion;
+ private CentralSurfaces mCentralSurfaces;
+ private HeadsUpManagerPhone mHeadsUpManager;
+ private float mExpandedHeight = 0;
+ private boolean mTracking;
+ private boolean mHintAnimationRunning;
+ private KeyguardBottomAreaView mKeyguardBottomArea;
+ private boolean mExpanding;
private boolean mSplitShadeEnabled;
/** The bottom padding reserved for elements of the keyguard measuring notifications. */
private float mKeyguardNotificationBottomPadding;
@@ -525,6 +585,7 @@
private final SysUiState mSysUiState;
private final NotificationShadeDepthController mDepthController;
+ private final NavigationBarController mNavigationBarController;
private final int mDisplayId;
private KeyguardIndicationController mKeyguardIndicationController;
@@ -634,6 +695,7 @@
private int mScreenCornerRadius;
private boolean mQSAnimatingHiddenFromCollapsed;
private boolean mUseLargeScreenShadeHeader;
+ private boolean mEnableQsClipping;
private int mQsClipTop;
private int mQsClipBottom;
@@ -708,6 +770,54 @@
private final CameraGestureHelper mCameraGestureHelper;
private final KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
private final KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
+ private float mMinExpandHeight;
+ private boolean mPanelUpdateWhenAnimatorEnds;
+ private boolean mHasVibratedOnOpen = false;
+ private int mFixedDuration = NO_FIXED_DURATION;
+ /** The overshoot amount when the panel flings open. */
+ private float mPanelFlingOvershootAmount;
+ /** The amount of pixels that we have overexpanded the last time with a gesture. */
+ private float mLastGesturedOverExpansion = -1;
+ /** Whether the current animator is the spring back animation. */
+ private boolean mIsSpringBackAnimation;
+ private boolean mInSplitShade;
+ private float mHintDistance;
+ private float mInitialOffsetOnTouch;
+ private boolean mCollapsedAndHeadsUpOnDown;
+ private float mExpandedFraction = 0;
+ private float mExpansionDragDownAmountPx = 0;
+ private boolean mPanelClosedOnDown;
+ private boolean mHasLayoutedSinceDown;
+ private float mUpdateFlingVelocity;
+ private boolean mUpdateFlingOnLayout;
+ private boolean mClosing;
+ private boolean mTouchSlopExceeded;
+ private int mTrackingPointer;
+ private int mTouchSlop;
+ private float mSlopMultiplier;
+ private boolean mTouchAboveFalsingThreshold;
+ private boolean mTouchStartedInEmptyArea;
+ private boolean mMotionAborted;
+ private boolean mUpwardsWhenThresholdReached;
+ private boolean mAnimatingOnDown;
+ private boolean mHandlingPointerUp;
+ private ValueAnimator mHeightAnimator;
+ /** Whether an instant expand request is currently pending and we are waiting for layout. */
+ private boolean mInstantExpanding;
+ private boolean mAnimateAfterExpanding;
+ private boolean mIsFlinging;
+ private String mViewName;
+ private float mInitialExpandY;
+ private float mInitialExpandX;
+ private boolean mTouchDisabled;
+ private boolean mInitialTouchFromKeyguard;
+ /** Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time. */
+ private float mNextCollapseSpeedUpFactor = 1.0f;
+ private boolean mGestureWaitForTouchSlop;
+ private boolean mIgnoreXTouchSlop;
+ private boolean mExpandLatencyTracking;
+ private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
+ mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
@Inject
public NotificationPanelViewController(NotificationPanelView view,
@@ -754,13 +864,14 @@
PrivacyDotViewController privacyDotViewController,
TapAgainViewController tapAgainViewController,
NavigationModeController navigationModeController,
+ NavigationBarController navigationBarController,
FragmentService fragmentService,
ContentResolver contentResolver,
RecordingController recordingController,
LargeScreenShadeHeaderController largeScreenShadeHeaderController,
ScreenOffAnimationController screenOffAnimationController,
LockscreenGestureLogger lockscreenGestureLogger,
- PanelExpansionStateManager panelExpansionStateManager,
+ ShadeExpansionStateManager shadeExpansionStateManager,
NotificationRemoteInputManager remoteInputManager,
Optional<SysUIUnfoldComponent> unfoldComponent,
InteractionJankMonitor interactionJankMonitor,
@@ -777,35 +888,77 @@
CameraGestureHelper cameraGestureHelper,
KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
KeyguardBottomAreaInteractor keyguardBottomAreaInteractor) {
- super(view,
- falsingManager,
- dozeLog,
- keyguardStateController,
- (SysuiStatusBarStateController) statusBarStateController,
- notificationShadeWindowController,
- vibratorHelper,
- statusBarKeyguardViewManager,
- latencyTracker,
- flingAnimationUtilsBuilder.get(),
- statusBarTouchableRegionManager,
- lockscreenGestureLogger,
- panelExpansionStateManager,
- ambientState,
- interactionJankMonitor,
- shadeLogger,
- systemClock);
+ keyguardStateController.addCallback(new KeyguardStateController.Callback() {
+ @Override
+ public void onKeyguardFadingAwayChanged() {
+ updateExpandedHeightToMaxHeight();
+ }
+ });
+ mAmbientState = ambientState;
mView = view;
+ mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+ mLockscreenGestureLogger = lockscreenGestureLogger;
+ mShadeExpansionStateManager = shadeExpansionStateManager;
+ mShadeLog = shadeLogger;
+ TouchHandler touchHandler = createTouchHandler();
+ mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ mViewName = mResources.getResourceName(mView.getId());
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ }
+ });
+
+ mView.addOnLayoutChangeListener(createLayoutChangeListener());
+ mView.setOnTouchListener(touchHandler);
+ mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener());
+
+ mResources = mView.getResources();
+ mKeyguardStateController = keyguardStateController;
+ mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController;
+ mNotificationShadeWindowController = notificationShadeWindowController;
+ FlingAnimationUtils.Builder fauBuilder = flingAnimationUtilsBuilder.get();
+ mFlingAnimationUtils = fauBuilder
+ .reset()
+ .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS)
+ .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
+ .build();
+ mFlingAnimationUtilsClosing = fauBuilder
+ .reset()
+ .setMaxLengthSeconds(FLING_CLOSING_MAX_LENGTH_SECONDS)
+ .setSpeedUpFactor(FLING_CLOSING_SPEED_UP_FACTOR)
+ .build();
+ mFlingAnimationUtilsDismissing = fauBuilder
+ .reset()
+ .setMaxLengthSeconds(0.5f)
+ .setSpeedUpFactor(0.6f)
+ .setX2(0.6f)
+ .setY2(0.84f)
+ .build();
+ mLatencyTracker = latencyTracker;
+ mBounceInterpolator = new BounceInterpolator();
+ mFalsingManager = falsingManager;
+ mDozeLog = dozeLog;
+ mNotificationsDragEnabled = mResources.getBoolean(
+ R.bool.config_enableNotificationShadeDrag);
mVibratorHelper = vibratorHelper;
+ mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation);
+ mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
+ mInteractionJankMonitor = interactionJankMonitor;
+ mSystemClock = systemClock;
mKeyguardMediaController = keyguardMediaController;
mPrivacyDotViewController = privacyDotViewController;
mMetricsLogger = metricsLogger;
mConfigurationController = configurationController;
mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder;
mMediaHierarchyManager = mediaHierarchyManager;
- mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mNotificationsQSContainerController = notificationsQSContainerController;
mNotificationListContainer = notificationListContainer;
mNotificationStackSizeCalculator = notificationStackSizeCalculator;
+ mNavigationBarController = navigationBarController;
mKeyguardBottomAreaViewControllerProvider = keyguardBottomAreaViewControllerProvider;
mNotificationsQSContainerController.init();
mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
@@ -824,7 +977,6 @@
mLargeScreenShadeHeaderController = largeScreenShadeHeaderController;
mLayoutInflater = layoutInflater;
mFeatureFlags = featureFlags;
- mFalsingManager = falsingManager;
mFalsingCollector = falsingCollector;
mPowerManager = powerManager;
mWakeUpCoordinator = coordinator;
@@ -840,7 +992,6 @@
mUserManager = userManager;
mMediaDataManager = mediaDataManager;
mTapAgainViewController = tapAgainViewController;
- mInteractionJankMonitor = interactionJankMonitor;
mSysUiState = sysUiState;
mPanelEventsEmitter = panelEventsEmitter;
pulseExpansionHandler.setPulseExpandAbortListener(() -> {
@@ -860,7 +1011,7 @@
new DynamicPrivacyControlListener();
dynamicPrivacyController.addListener(dynamicPrivacyControlListener);
- panelExpansionStateManager.addStateListener(this::onPanelStateChanged);
+ shadeExpansionStateManager.addStateListener(this::onPanelStateChanged);
mBottomAreaShadeAlphaAnimator = ValueAnimator.ofFloat(1f, 0);
mBottomAreaShadeAlphaAnimator.addUpdateListener(animation -> {
@@ -1042,9 +1193,14 @@
controller.setup(mNotificationContainerParent));
}
- @Override
- protected void loadDimens() {
- super.loadDimens();
+ @VisibleForTesting
+ void loadDimens() {
+ final ViewConfiguration configuration = ViewConfiguration.get(this.mView.getContext());
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
+ mHintDistance = mResources.getDimension(R.dimen.hint_move_distance);
+ mPanelFlingOvershootAmount = mResources.getDimension(R.dimen.panel_overshoot_amount);
+ mInSplitShade = mResources.getBoolean(R.bool.config_use_split_notification_shade);
mFlingAnimationUtils = mFlingAnimationUtilsBuilder.get()
.setMaxLengthSeconds(0.4f).build();
mStatusBarMinHeight = SystemBarUtils.getStatusBarHeight(mView.getContext());
@@ -1151,6 +1307,8 @@
mSplitShadeFullTransitionDistance =
mResources.getDimensionPixelSize(R.dimen.split_shade_full_transition_distance);
+
+ mEnableQsClipping = mResources.getBoolean(R.bool.qs_enable_clipping);
}
private void onSplitShadeEnabledChanged() {
@@ -1290,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));
@@ -1517,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);
@@ -1717,11 +1916,10 @@
// it's possible that nothing animated, so we replicate the termination
// conditions of panelExpansionChanged here
// TODO(b/200063118): This can likely go away in a future refactor CL.
- getPanelExpansionStateManager().updateState(STATE_CLOSED);
+ getShadeExpansionStateManager().updateState(STATE_CLOSED);
}
}
- @Override
public void collapse(boolean delayed, float speedUpFactor) {
if (!canPanelBeCollapsed()) {
return;
@@ -1731,7 +1929,20 @@
setQsExpandImmediate(true);
setShowShelfOnly(true);
}
- super.collapse(delayed, speedUpFactor);
+ if (DEBUG) this.logf("collapse: " + this);
+ if (canPanelBeCollapsed()) {
+ cancelHeightAnimator();
+ notifyExpandingStarted();
+
+ // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
+ setIsClosing(true);
+ if (delayed) {
+ mNextCollapseSpeedUpFactor = speedUpFactor;
+ this.mView.postDelayed(mFlingCollapseRunnable, 120);
+ } else {
+ fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */);
+ }
+ }
}
private void setQsExpandImmediate(boolean expandImmediate) {
@@ -1751,10 +1962,15 @@
setQsExpansion(mQsMinExpansionHeight);
}
- @Override
@VisibleForTesting
- protected void cancelHeightAnimator() {
- super.cancelHeightAnimator();
+ void cancelHeightAnimator() {
+ if (mHeightAnimator != null) {
+ if (mHeightAnimator.isRunning()) {
+ mPanelUpdateWhenAnimatorEnds = false;
+ }
+ mHeightAnimator.cancel();
+ }
+ endClosing();
}
public void cancelAnimation() {
@@ -1822,28 +2038,124 @@
}
}
- @Override
public void fling(float vel, boolean expand) {
GestureRecorder gr = mCentralSurfaces.getGestureRecorder();
if (gr != null) {
gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
}
- super.fling(vel, expand);
+ fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
}
- @Override
- protected void flingToHeight(float vel, boolean expand, float target,
+ @VisibleForTesting
+ void flingToHeight(float vel, boolean expand, float target,
float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
mHeadsUpTouchHelper.notifyFling(!expand);
mKeyguardStateController.notifyPanelFlingStart(!expand /* flingingToDismiss */);
setClosingWithAlphaFadeout(!expand && !isOnKeyguard() && getFadeoutAlpha() == 1.0f);
mNotificationStackScrollLayoutController.setPanelFlinging(true);
- super.flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
+ if (target == mExpandedHeight && mOverExpansion == 0.0f) {
+ // We're at the target and didn't fling and there's no overshoot
+ onFlingEnd(false /* cancelled */);
+ return;
+ }
+ mIsFlinging = true;
+ // we want to perform an overshoot animation when flinging open
+ final boolean addOverscroll =
+ expand
+ && !mInSplitShade // Split shade has its own overscroll logic
+ && mStatusBarStateController.getState() != KEYGUARD
+ && mOverExpansion == 0.0f
+ && vel >= 0;
+ final boolean shouldSpringBack = addOverscroll || (mOverExpansion != 0.0f && expand);
+ float overshootAmount = 0.0f;
+ if (addOverscroll) {
+ // Let's overshoot depending on the amount of velocity
+ overshootAmount = MathUtils.lerp(
+ 0.2f,
+ 1.0f,
+ MathUtils.saturate(vel
+ / (this.mFlingAnimationUtils.getHighVelocityPxPerSecond()
+ * FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT)));
+ overshootAmount += mOverExpansion / mPanelFlingOvershootAmount;
+ }
+ ValueAnimator animator = createHeightAnimator(target, overshootAmount);
+ if (expand) {
+ if (expandBecauseOfFalsing && vel < 0) {
+ vel = 0;
+ }
+ this.mFlingAnimationUtils.apply(animator, mExpandedHeight,
+ target + overshootAmount * mPanelFlingOvershootAmount, vel,
+ this.mView.getHeight());
+ if (vel == 0) {
+ animator.setDuration(SHADE_OPEN_SPRING_OUT_DURATION);
+ }
+ } else {
+ if (shouldUseDismissingAnimation()) {
+ if (vel == 0) {
+ animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
+ long duration = (long) (200 + mExpandedHeight / this.mView.getHeight() * 100);
+ animator.setDuration(duration);
+ } else {
+ mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel,
+ this.mView.getHeight());
+ }
+ } else {
+ mFlingAnimationUtilsClosing.apply(
+ animator, mExpandedHeight, target, vel, this.mView.getHeight());
+ }
+
+ // Make it shorter if we run a canned animation
+ if (vel == 0) {
+ animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor));
+ }
+ if (mFixedDuration != NO_FIXED_DURATION) {
+ animator.setDuration(mFixedDuration);
+ }
+ }
+ animator.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (!mStatusBarStateController.isDozing()) {
+ beginJankMonitoring();
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (shouldSpringBack && !mCancelled) {
+ // After the shade is flinged open to an overscrolled state, spring back
+ // the shade by reducing section padding to 0.
+ springBack();
+ } else {
+ onFlingEnd(mCancelled);
+ }
+ }
+ });
+ setAnimator(animator);
+ animator.start();
}
- @Override
- protected void onFlingEnd(boolean cancelled) {
- super.onFlingEnd(cancelled);
+ @VisibleForTesting
+ void onFlingEnd(boolean cancelled) {
+ mIsFlinging = false;
+ // No overshoot when the animation ends
+ setOverExpansionInternal(0, false /* isFromGesture */);
+ setAnimator(null);
+ mKeyguardStateController.notifyPanelFlingEnd();
+ if (!cancelled) {
+ endJankMonitoring();
+ notifyExpandingFinished();
+ } else {
+ cancelJankMonitoring();
+ }
+ updatePanelExpansionAndVisibility();
mNotificationStackScrollLayoutController.setPanelFlinging(false);
}
@@ -1905,7 +2217,8 @@
mShadeLog.logMotionEvent(event,
"onQsIntercept: move ignored because qs tracking disabled");
}
- if ((h > getTouchSlop(event) || (h < -getTouchSlop(event) && mQsExpanded))
+ float touchSlop = getTouchSlop(event);
+ if ((h > touchSlop || (h < -touchSlop && mQsExpanded))
&& Math.abs(h) > Math.abs(x - mInitialTouchX)
&& shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept - start tracking expansion");
@@ -1920,6 +2233,9 @@
mInitialTouchX = x;
mNotificationStackScrollLayoutController.cancelLongPress();
return true;
+ } else {
+ mShadeLog.logQsTrackingNotStarted(mInitialTouchY, y, h, touchSlop, mQsExpanded,
+ mCollapsedOnDown, mKeyguardShowing, isQsExpansionEnabled());
}
break;
@@ -1938,8 +2254,7 @@
return mQsTracking;
}
- @Override
- protected boolean isInContentBounds(float x, float y) {
+ private boolean isInContentBounds(float x, float y) {
float stackScrollerX = mNotificationStackScrollLayoutController.getX();
return !mNotificationStackScrollLayoutController
.isBelowLastNotification(x - stackScrollerX, y)
@@ -2072,9 +2387,8 @@
- mQsMinExpansionHeight));
}
- @Override
- protected boolean shouldExpandWhenNotFlinging() {
- if (super.shouldExpandWhenNotFlinging()) {
+ private boolean shouldExpandWhenNotFlinging() {
+ if (getExpandedFraction() > 0.5f) {
return true;
}
if (mAllowExpandForSmallExpansion) {
@@ -2086,8 +2400,7 @@
return false;
}
- @Override
- protected float getOpeningHeight() {
+ private float getOpeningHeight() {
return mNotificationStackScrollLayoutController.getOpeningHeight();
}
@@ -2238,9 +2551,20 @@
}
}
- @Override
- protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
- boolean expands = super.flingExpands(vel, vectorVel, x, y);
+ private boolean flingExpands(float vel, float vectorVel, float x, float y) {
+ boolean expands = true;
+ if (!this.mFalsingManager.isUnlockingDisabled()) {
+ @Classifier.InteractionType int interactionType = y - mInitialExpandY > 0
+ ? QUICK_SETTINGS : (
+ mKeyguardStateController.canDismissLockScreen() ? UNLOCK : BOUNCER_UNLOCK);
+ if (!isFalseTouch(x, y, interactionType)) {
+ if (Math.abs(vectorVel) < this.mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
+ expands = shouldExpandWhenNotFlinging();
+ } else {
+ expands = vel > 0;
+ }
+ }
+ }
// If we are already running a QS expansion, make sure that we keep the panel open.
if (mQsExpansionAnimator != null) {
@@ -2249,8 +2573,7 @@
return expands;
}
- @Override
- protected boolean shouldGestureWaitForTouchSlop() {
+ private boolean shouldGestureWaitForTouchSlop() {
if (mExpectingSynthesizedDown) {
mExpectingSynthesizedDown = false;
return false;
@@ -2328,7 +2651,7 @@
}
}
- protected int getFalsingThreshold() {
+ private int getFalsingThreshold() {
float factor = mCentralSurfaces.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
return (int) (mQsFalsingThreshold * factor);
}
@@ -2363,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);
}
}
@@ -2682,8 +3009,10 @@
mQsTranslationForFullShadeTransition = qsTranslation;
updateQsFrameTranslation();
float currentTranslation = mQsFrame.getTranslationY();
- mQsClipTop = (int) (top - currentTranslation - mQsFrame.getTop());
- mQsClipBottom = (int) (bottom - currentTranslation - mQsFrame.getTop());
+ mQsClipTop = mEnableQsClipping
+ ? (int) (top - currentTranslation - mQsFrame.getTop()) : 0;
+ mQsClipBottom = mEnableQsClipping
+ ? (int) (bottom - currentTranslation - mQsFrame.getTop()) : 0;
mQsVisible = qsVisible;
mQs.setQsVisible(mQsVisible);
mQs.setFancyClipping(
@@ -3068,8 +3397,8 @@
}
}
- @Override
- protected boolean canCollapsePanelOnTouch() {
+ @VisibleForTesting
+ boolean canCollapsePanelOnTouch() {
if (!isInSettings() && mBarState == KEYGUARD) {
return true;
}
@@ -3081,7 +3410,6 @@
return !mSplitShadeEnabled && (isInSettings() || mIsPanelCollapseOnQQS);
}
- @Override
public int getMaxPanelHeight() {
int min = mStatusBarMinHeight;
if (!(mBarState == KEYGUARD)
@@ -3115,8 +3443,7 @@
return mIsExpanding;
}
- @Override
- protected void onHeightUpdated(float expandedHeight) {
+ private void onHeightUpdated(float expandedHeight) {
if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) {
// Updating the clock position will set the top padding which might
// trigger a new panel height and re-position the clock.
@@ -3295,9 +3622,7 @@
mLockIconViewController.setAlpha(alpha);
}
- @Override
- protected void onExpandingStarted() {
- super.onExpandingStarted();
+ private void onExpandingStarted() {
mNotificationStackScrollLayoutController.onExpansionStarted();
mIsExpanding = true;
mQsExpandedWhenExpandingStarted = mQsFullyExpanded;
@@ -3313,8 +3638,7 @@
mQs.setHeaderListening(true);
}
- @Override
- protected void onExpandingFinished() {
+ private void onExpandingFinished() {
mScrimController.onExpandingFinished();
mNotificationStackScrollLayoutController.onExpansionStopped();
mHeadsUpManager.onExpandingFinished();
@@ -3366,18 +3690,63 @@
mQs.setListening(listening);
}
- @Override
public void expand(boolean animate) {
- super.expand(animate);
+ if (isFullyCollapsed() || isCollapsing()) {
+ mInstantExpanding = true;
+ mAnimateAfterExpanding = animate;
+ mUpdateFlingOnLayout = false;
+ abortAnimations();
+ if (mTracking) {
+ // The panel is expanded after this call.
+ onTrackingStopped(true /* expands */);
+ }
+ if (mExpanding) {
+ notifyExpandingFinished();
+ }
+ updatePanelExpansionAndVisibility();
+ // Wait for window manager to pickup the change, so we know the maximum height of the
+ // panel then.
+ this.mView.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ if (!mInstantExpanding) {
+ mView.getViewTreeObserver().removeOnGlobalLayoutListener(
+ this);
+ return;
+ }
+ if (mCentralSurfaces.getNotificationShadeWindowView()
+ .isVisibleToUser()) {
+ mView.getViewTreeObserver().removeOnGlobalLayoutListener(
+ this);
+ if (mAnimateAfterExpanding) {
+ notifyExpandingStarted();
+ beginJankMonitoring();
+ fling(0, true /* expand */);
+ } else {
+ setExpandedFraction(1f);
+ }
+ mInstantExpanding = false;
+ }
+ }
+ });
+ // Make sure a layout really happens.
+ this.mView.requestLayout();
+ }
+
setListening(true);
}
- @Override
+ @VisibleForTesting
+ void setTouchSlopExceeded(boolean isTouchSlopExceeded) {
+ mTouchSlopExceeded = isTouchSlopExceeded;
+ }
+
public void setOverExpansion(float overExpansion) {
if (overExpansion == mOverExpansion) {
return;
}
- super.setOverExpansion(overExpansion);
+ mOverExpansion = overExpansion;
// Translating the quick settings by half the overexpansion to center it in the background
// frame
updateQsFrameTranslation();
@@ -3385,14 +3754,18 @@
}
private void updateQsFrameTranslation() {
- mQsFrameTranslateController.translateQsFrame(mQsFrame, mQs, mOverExpansion,
- mQsTranslationForFullShadeTransition);
+ mQsFrameTranslateController.translateQsFrame(mQsFrame, mQs,
+ mNavigationBarBottomHeight + mAmbientState.getStackTopMargin());
+
}
- @Override
- protected void onTrackingStarted() {
+ private void onTrackingStarted() {
mFalsingCollector.onTrackingStarted(!mKeyguardStateController.canDismissLockScreen());
- super.onTrackingStarted();
+ endClosing();
+ mTracking = true;
+ mCentralSurfaces.onTrackingStarted();
+ notifyExpandingStarted();
+ updatePanelExpansionAndVisibility();
mScrimController.onTrackingStarted();
if (mQsFullyExpanded) {
setQsExpandImmediate(true);
@@ -3402,10 +3775,11 @@
cancelPendingPanelCollapse();
}
- @Override
- protected void onTrackingStopped(boolean expand) {
+ private void onTrackingStopped(boolean expand) {
mFalsingCollector.onTrackingStopped();
- super.onTrackingStopped(expand);
+ mTracking = false;
+ mCentralSurfaces.onTrackingStopped(expand);
+ updatePanelExpansionAndVisibility();
if (expand) {
mNotificationStackScrollLayoutController.setOverScrollAmount(0.0f, true /* onTop */,
true /* animate */);
@@ -3422,37 +3796,48 @@
getHeight(), mNavigationBarBottomHeight);
}
- @Override
- protected void startUnlockHintAnimation() {
+ @VisibleForTesting
+ void startUnlockHintAnimation() {
if (mPowerManager.isPowerSaveMode() || mAmbientState.getDozeAmount() > 0f) {
onUnlockHintStarted();
onUnlockHintFinished();
return;
}
- super.startUnlockHintAnimation();
+
+ // We don't need to hint the user if an animation is already running or the user is changing
+ // the expansion.
+ if (mHeightAnimator != null || mTracking) {
+ return;
+ }
+ notifyExpandingStarted();
+ startUnlockHintAnimationPhase1(() -> {
+ notifyExpandingFinished();
+ onUnlockHintFinished();
+ mHintAnimationRunning = false;
+ });
+ onUnlockHintStarted();
+ mHintAnimationRunning = true;
}
- @Override
- protected void onUnlockHintFinished() {
- super.onUnlockHintFinished();
+ @VisibleForTesting
+ void onUnlockHintFinished() {
+ mCentralSurfaces.onHintFinished();
mScrimController.setExpansionAffectsAlpha(true);
mNotificationStackScrollLayoutController.setUnlockHintRunning(false);
}
- @Override
- protected void onUnlockHintStarted() {
- super.onUnlockHintStarted();
+ @VisibleForTesting
+ void onUnlockHintStarted() {
+ mCentralSurfaces.onUnlockHintStarted();
mScrimController.setExpansionAffectsAlpha(false);
mNotificationStackScrollLayoutController.setUnlockHintRunning(true);
}
- @Override
- protected boolean shouldUseDismissingAnimation() {
+ private boolean shouldUseDismissingAnimation() {
return mBarState != StatusBarState.SHADE && (mKeyguardStateController.canDismissLockScreen()
|| !isTracking());
}
- @Override
public int getMaxPanelTransitionDistance() {
// Traditionally the value is based on the number of notifications. On split-shade, we want
// the required distance to be a specific and constant value, to make sure the expansion
@@ -3477,8 +3862,8 @@
}
}
- @Override
- protected boolean isTrackingBlocked() {
+ @VisibleForTesting
+ boolean isTrackingBlocked() {
return mConflictingQsExpansionGesture && mQsExpanded || mBlockingExpansionForCurrentTouch;
}
@@ -3500,22 +3885,22 @@
return mIsLaunchTransitionFinished;
}
- @Override
public void setIsLaunchAnimationRunning(boolean running) {
boolean wasRunning = mIsLaunchAnimationRunning;
- super.setIsLaunchAnimationRunning(running);
+ mIsLaunchAnimationRunning = running;
if (wasRunning != mIsLaunchAnimationRunning) {
mPanelEventsEmitter.notifyLaunchingActivityChanged(running);
}
}
- @Override
- protected void setIsClosing(boolean isClosing) {
+ @VisibleForTesting
+ void setIsClosing(boolean isClosing) {
boolean wasClosing = isClosing();
- super.setIsClosing(isClosing);
+ mClosing = isClosing;
if (wasClosing != isClosing) {
mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing);
}
+ mAmbientState.setIsClosing(isClosing);
}
private void updateDozingVisibilities(boolean animate) {
@@ -3525,7 +3910,6 @@
}
}
- @Override
public boolean isDozing() {
return mDozing;
}
@@ -3542,17 +3926,20 @@
mKeyguardStatusViewController.dozeTimeTick();
}
- @Override
- protected boolean onMiddleClicked() {
+ private boolean onMiddleClicked() {
switch (mBarState) {
case KEYGUARD:
if (!mDozingOnDown) {
- if (mUpdateMonitor.isFaceEnrolled()
- && !mUpdateMonitor.isFaceDetectionRunning()
- && !mUpdateMonitor.getUserCanSkipBouncer(
- KeyguardUpdateMonitor.getCurrentUser())) {
- mUpdateMonitor.requestFaceAuth(true,
- FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
+ mShadeLog.v("onMiddleClicked on Keyguard, mDozingOnDown: false");
+ // Try triggering face auth, this "might" run. Check
+ // KeyguardUpdateMonitor#shouldListenForFace to see when face auth won't run.
+ boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true,
+ FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
+
+ if (didFaceAuthRun) {
+ mUpdateMonitor.requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ "lockScreenEmptySpaceTap");
} else {
mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT,
0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */);
@@ -3560,11 +3947,6 @@
.log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT);
startUnlockHintAnimation();
}
- if (mUpdateMonitor.isFaceEnrolled()) {
- mUpdateMonitor.requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
- "lockScreenEmptySpaceTap");
- }
}
return true;
case StatusBarState.SHADE_LOCKED:
@@ -3602,15 +3984,13 @@
updateVisibility();
}
- @Override
- protected boolean shouldPanelBeVisible() {
+ private boolean shouldPanelBeVisible() {
boolean headsUpVisible = mHeadsUpAnimatingAway || mHeadsUpPinnedMode;
return headsUpVisible || isExpanded() || mBouncerShowing;
}
- @Override
public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
- super.setHeadsUpManager(headsUpManager);
+ mHeadsUpManager = headsUpManager;
mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager,
mNotificationStackScrollLayoutController.getHeadsUpCallback(),
NotificationPanelViewController.this);
@@ -3624,8 +4004,7 @@
// otherwise we update the state when the expansion is finished
}
- @Override
- protected void onClosingFinished() {
+ private void onClosingFinished() {
mCentralSurfaces.onClosingFinished();
setClosingWithAlphaFadeout(false);
mMediaHierarchyManager.closeGuts();
@@ -3714,8 +4093,7 @@
mCentralSurfaces.clearNotificationEffects();
}
- @Override
- protected boolean isPanelVisibleBecauseOfHeadsUp() {
+ private boolean isPanelVisibleBecauseOfHeadsUp() {
return (mHeadsUpManager.hasPinnedHeadsUp() || mHeadsUpAnimatingAway)
&& mBarState == StatusBarState.SHADE;
}
@@ -3830,17 +4208,23 @@
mNotificationBoundsAnimationDelay = delay;
}
- @Override
public void setTouchAndAnimationDisabled(boolean disabled) {
- super.setTouchAndAnimationDisabled(disabled);
+ mTouchDisabled = disabled;
+ if (mTouchDisabled) {
+ cancelHeightAnimator();
+ if (mTracking) {
+ onTrackingStopped(true /* expanded */);
+ }
+ notifyExpandingFinished();
+ }
mNotificationStackScrollLayoutController.setAnimationsEnabled(!disabled);
}
/**
* 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;
@@ -3980,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();
}
/**
@@ -4032,9 +4416,14 @@
mBlockingExpansionForCurrentTouch = mTracking;
}
- @Override
public void dump(PrintWriter pw, String[] args) {
- super.dump(pw, args);
+ pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
+ + " tracking=%s timeAnim=%s%s "
+ + "touchDisabled=%s" + "]",
+ this.getClass().getSimpleName(), getExpandedHeight(), getMaxPanelHeight(),
+ mClosing ? "T" : "f", mTracking ? "T" : "f", mHeightAnimator,
+ ((mHeightAnimator != null && mHeightAnimator.isStarted()) ? " (started)" : ""),
+ mTouchDisabled ? "T" : "f"));
IndentingPrintWriter ipw = asIndenting(pw);
ipw.increaseIndent();
ipw.println("gestureExclusionRect:" + calculateGestureExclusionRect());
@@ -4177,126 +4566,13 @@
mConfigurationListener.onThemeChanged();
}
- @Override
- protected OnLayoutChangeListener createLayoutChangeListener() {
- return new OnLayoutChangeListenerImpl();
+ private OnLayoutChangeListener createLayoutChangeListener() {
+ return new OnLayoutChangeListener();
}
- @Override
- protected TouchHandler createTouchHandler() {
- return new TouchHandler() {
-
- private long mLastTouchDownTime = -1L;
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
- if (SPEW_LOGCAT) {
- Log.v(TAG,
- "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX()
- + "," + event.getY() + ")");
- }
- if (mQs.disallowPanelTouches()) {
- return false;
- }
- initDownStates(event);
- // Do not let touches go to shade or QS if the bouncer is visible,
- // but still let user swipe down to expand the panel, dismissing the bouncer.
- if (mCentralSurfaces.isBouncerShowing()) {
- return true;
- }
- if (mCommandQueue.panelsEnabled()
- && !mNotificationStackScrollLayoutController.isLongPressInProgress()
- && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
- mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
- mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
- return true;
- }
- if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0)
- && mPulseExpansionHandler.onInterceptTouchEvent(event)) {
- return true;
- }
-
- if (!isFullyCollapsed() && onQsIntercept(event)) {
- if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept true");
- return true;
- }
- return super.onInterceptTouchEvent(event);
- }
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- if (event.getDownTime() == mLastTouchDownTime) {
- // An issue can occur when swiping down after unlock, where multiple down
- // events are received in this handler with identical downTimes. Until the
- // source of the issue can be located, detect this case and ignore.
- // see b/193350347
- Log.w(TAG, "Duplicate down event detected... ignoring");
- return true;
- }
- mLastTouchDownTime = event.getDownTime();
- }
-
-
- if (mQsFullyExpanded && mQs != null && mQs.disallowPanelTouches()) {
- return false;
- }
-
- // Do not allow panel expansion if bouncer is scrimmed or showing over a dream,
- // otherwise user would be able to pull down QS or expand the shade.
- if (mCentralSurfaces.isBouncerShowingScrimmed()
- || mCentralSurfaces.isBouncerShowingOverDream()) {
- return false;
- }
-
- // Make sure the next touch won't the blocked after the current ends.
- if (event.getAction() == MotionEvent.ACTION_UP
- || event.getAction() == MotionEvent.ACTION_CANCEL) {
- mBlockingExpansionForCurrentTouch = false;
- }
- // When touch focus transfer happens, ACTION_DOWN->ACTION_UP may happen immediately
- // without any ACTION_MOVE event.
- // In such case, simply expand the panel instead of being stuck at the bottom bar.
- if (mLastEventSynthesizedDown && event.getAction() == MotionEvent.ACTION_UP) {
- expand(true /* animate */);
- }
- initDownStates(event);
-
- // If pulse is expanding already, let's give it the touch. There are situations
- // where the panel starts expanding even though we're also pulsing
- boolean pulseShouldGetTouch = (!mIsExpanding
- && !shouldQuickSettingsIntercept(mDownX, mDownY, 0))
- || mPulseExpansionHandler.isExpanding();
- if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) {
- // We're expanding all the other ones shouldn't get this anymore
- mShadeLog.logMotionEvent(event, "onTouch: PulseExpansionHandler handled event");
- return true;
- }
- if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp()
- && !mNotificationStackScrollLayoutController.isLongPressInProgress()
- && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
- mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
- }
- boolean handled = mHeadsUpTouchHelper.onTouchEvent(event);
-
- if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) {
- mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event");
- return true;
- }
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
- mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
- handled = true;
- }
-
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyExpanded()
- && mStatusBarKeyguardViewManager.isShowing()) {
- mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX());
- }
-
- handled |= super.onTouch(v, event);
- return !mDozing || mPulsing || handled;
- }
- };
+ @VisibleForTesting
+ TouchHandler createTouchHandler() {
+ return new TouchHandler();
}
private final PhoneStatusBarView.TouchEventHandler mStatusBarViewTouchEventHandler =
@@ -4348,8 +4624,7 @@
}
};
- @Override
- protected OnConfigurationChangedListener createOnConfigurationChangedListener() {
+ private OnConfigurationChangedListener createOnConfigurationChangedListener() {
return new OnConfigurationChangedListener();
}
@@ -4407,10 +4682,605 @@
}
mSysUiState.setFlag(SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED,
isFullyExpanded() && !isInSettings())
- .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED, isInSettings())
+ .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED, isFullyExpanded() && isInSettings())
.commitUpdate(mDisplayId);
}
+ private void logf(String fmt, Object... args) {
+ Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
+ }
+
+ @VisibleForTesting
+ void notifyExpandingStarted() {
+ if (!mExpanding) {
+ mExpanding = true;
+ onExpandingStarted();
+ }
+ }
+
+ @VisibleForTesting
+ void notifyExpandingFinished() {
+ endClosing();
+ if (mExpanding) {
+ mExpanding = false;
+ onExpandingFinished();
+ }
+ }
+
+ private float getTouchSlop(MotionEvent event) {
+ // Adjust the touch slop if another gesture may be being performed.
+ return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
+ ? mTouchSlop * mSlopMultiplier
+ : mTouchSlop;
+ }
+
+ private void addMovement(MotionEvent event) {
+ // Add movement to velocity tracker using raw screen X and Y coordinates instead
+ // of window coordinates because the window frame may be moving at the same time.
+ float deltaX = event.getRawX() - event.getX();
+ float deltaY = event.getRawY() - event.getY();
+ event.offsetLocation(deltaX, deltaY);
+ mVelocityTracker.addMovement(event);
+ event.offsetLocation(-deltaX, -deltaY);
+ }
+
+ /** If the latency tracker is enabled, begins tracking expand latency. */
+ public void startExpandLatencyTracking() {
+ if (mLatencyTracker.isEnabled()) {
+ mLatencyTracker.onActionStart(LatencyTracker.ACTION_EXPAND_PANEL);
+ mExpandLatencyTracking = true;
+ }
+ }
+
+ private void startOpening(MotionEvent event) {
+ updatePanelExpansionAndVisibility();
+ // Reset at start so haptic can be triggered as soon as panel starts to open.
+ mHasVibratedOnOpen = false;
+ //TODO: keyguard opens QS a different way; log that too?
+
+ // Log the position of the swipe that opened the panel
+ float width = mCentralSurfaces.getDisplayWidth();
+ float height = mCentralSurfaces.getDisplayHeight();
+ int rot = mCentralSurfaces.getRotation();
+
+ mLockscreenGestureLogger.writeAtFractionalPosition(MetricsEvent.ACTION_PANEL_VIEW_EXPAND,
+ (int) (event.getX() / width * 100), (int) (event.getY() / height * 100), rot);
+ mLockscreenGestureLogger
+ .log(LockscreenUiEvent.LOCKSCREEN_UNLOCKED_NOTIFICATION_PANEL_EXPAND);
+ }
+
+ /**
+ * 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.
+ */
+ private void maybeVibrateOnOpening(boolean openingWithTouch) {
+ if (mVibrateOnOpening) {
+ if (!openingWithTouch || !mHasVibratedOnOpen) {
+ mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
+ mHasVibratedOnOpen = true;
+ }
+ }
+ }
+
+ /**
+ * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
+ * horizontal direction
+ */
+ private boolean isDirectionUpwards(float x, float y) {
+ float xDiff = x - mInitialExpandX;
+ float yDiff = y - mInitialExpandY;
+ if (yDiff >= 0) {
+ return false;
+ }
+ return Math.abs(yDiff) >= Math.abs(xDiff);
+ }
+
+ /** Called when a MotionEvent is about to trigger Shade expansion. */
+ public void startExpandMotion(float newX, float newY, boolean startTracking,
+ float expandedHeight) {
+ if (!mHandlingPointerUp && !mStatusBarStateController.isDozing()) {
+ beginJankMonitoring();
+ }
+ mInitialOffsetOnTouch = expandedHeight;
+ mInitialExpandY = newY;
+ mInitialExpandX = newX;
+ mInitialTouchFromKeyguard = mKeyguardStateController.isShowing();
+ if (startTracking) {
+ mTouchSlopExceeded = true;
+ setExpandedHeight(mInitialOffsetOnTouch);
+ onTrackingStarted();
+ }
+ }
+
+ private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
+ mTrackingPointer = -1;
+ mAmbientState.setSwipingUp(false);
+ if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop
+ || Math.abs(y - mInitialExpandY) > mTouchSlop
+ || (!isFullyExpanded() && !isFullyCollapsed())
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
+ mVelocityTracker.computeCurrentVelocity(1000);
+ float vel = mVelocityTracker.getYVelocity();
+ float vectorVel = (float) Math.hypot(
+ mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+
+ final boolean onKeyguard = mKeyguardStateController.isShowing();
+ final boolean expand;
+ if (mKeyguardStateController.isKeyguardFadingAway()
+ || (mInitialTouchFromKeyguard && !onKeyguard)) {
+ // Don't expand for any touches that started from the keyguard and ended after the
+ // keyguard is gone.
+ expand = false;
+ } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
+ if (onKeyguard) {
+ expand = true;
+ } else if (mCentralSurfaces.isBouncerShowingOverDream()) {
+ expand = false;
+ } else {
+ // If we get a cancel, put the shade back to the state it was in when the
+ // gesture started
+ expand = !mPanelClosedOnDown;
+ }
+ } else {
+ expand = flingExpands(vel, vectorVel, x, y);
+ }
+
+ mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
+ mCentralSurfaces.isFalsingThresholdNeeded(),
+ mCentralSurfaces.isWakeUpComingFromTouch());
+ // Log collapse gesture if on lock screen.
+ if (!expand && onKeyguard) {
+ float displayDensity = mCentralSurfaces.getDisplayDensity();
+ int heightDp = (int) Math.abs((y - mInitialExpandY) / displayDensity);
+ int velocityDp = (int) Math.abs(vel / displayDensity);
+ mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_UNLOCK, heightDp, velocityDp);
+ mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_UNLOCK);
+ }
+ @Classifier.InteractionType int interactionType = vel == 0 ? GENERIC
+ : y - mInitialExpandY > 0 ? QUICK_SETTINGS
+ : (mKeyguardStateController.canDismissLockScreen()
+ ? UNLOCK : BOUNCER_UNLOCK);
+
+ fling(vel, expand, isFalseTouch(x, y, interactionType));
+ onTrackingStopped(expand);
+ mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
+ if (mUpdateFlingOnLayout) {
+ mUpdateFlingVelocity = vel;
+ }
+ } else if (!mCentralSurfaces.isBouncerShowing()
+ && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()
+ && !mKeyguardStateController.isKeyguardGoingAway()) {
+ boolean expands = onEmptySpaceClick();
+ onTrackingStopped(expands);
+ }
+ mVelocityTracker.clear();
+ }
+
+ private float getCurrentExpandVelocity() {
+ mVelocityTracker.computeCurrentVelocity(1000);
+ return mVelocityTracker.getYVelocity();
+ }
+
+ private void endClosing() {
+ if (mClosing) {
+ setIsClosing(false);
+ onClosingFinished();
+ }
+ }
+
+ /**
+ * @param x the final x-coordinate when the finger was lifted
+ * @param y the final y-coordinate when the finger was lifted
+ * @return whether this motion should be regarded as a false touch
+ */
+ private boolean isFalseTouch(float x, float y,
+ @Classifier.InteractionType int interactionType) {
+ if (!mCentralSurfaces.isFalsingThresholdNeeded()) {
+ return false;
+ }
+ if (mFalsingManager.isClassifierEnabled()) {
+ return mFalsingManager.isFalseTouch(interactionType);
+ }
+ if (!mTouchAboveFalsingThreshold) {
+ return true;
+ }
+ if (mUpwardsWhenThresholdReached) {
+ return false;
+ }
+ return !isDirectionUpwards(x, y);
+ }
+
+ private void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) {
+ fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing);
+ }
+
+ private void fling(float vel, boolean expand, float collapseSpeedUpFactor,
+ boolean expandBecauseOfFalsing) {
+ float target = expand ? getMaxPanelHeight() : 0;
+ if (!expand) {
+ setIsClosing(true);
+ }
+ flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
+ }
+
+ private void springBack() {
+ if (mOverExpansion == 0) {
+ onFlingEnd(false /* cancelled */);
+ return;
+ }
+ mIsSpringBackAnimation = true;
+ ValueAnimator animator = ValueAnimator.ofFloat(mOverExpansion, 0);
+ animator.addUpdateListener(
+ animation -> setOverExpansionInternal((float) animation.getAnimatedValue(),
+ false /* isFromGesture */));
+ animator.setDuration(SHADE_OPEN_SPRING_BACK_DURATION);
+ animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ animator.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mIsSpringBackAnimation = false;
+ onFlingEnd(mCancelled);
+ }
+ });
+ setAnimator(animator);
+ animator.start();
+ }
+
+ public String getName() {
+ return mViewName;
+ }
+
+ @VisibleForTesting
+ void setExpandedHeight(float height) {
+ if (DEBUG) logf("setExpandedHeight(%.1f)", height);
+ setExpandedHeightInternal(height);
+ }
+
+ private void updateExpandedHeightToMaxHeight() {
+ float currentMaxPanelHeight = getMaxPanelHeight();
+
+ if (isFullyCollapsed()) {
+ return;
+ }
+
+ if (currentMaxPanelHeight == mExpandedHeight) {
+ return;
+ }
+
+ if (mTracking && !isTrackingBlocked()) {
+ return;
+ }
+
+ if (mHeightAnimator != null && !mIsSpringBackAnimation) {
+ mPanelUpdateWhenAnimatorEnds = true;
+ return;
+ }
+
+ setExpandedHeight(currentMaxPanelHeight);
+ }
+
+ private void setExpandedHeightInternal(float h) {
+ if (isNaN(h)) {
+ Log.wtf(TAG, "ExpandedHeight set to NaN");
+ }
+ mNotificationShadeWindowController.batchApplyWindowLayoutParams(() -> {
+ if (mExpandLatencyTracking && h != 0f) {
+ DejankUtils.postAfterTraversal(
+ () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL));
+ mExpandLatencyTracking = false;
+ }
+ float maxPanelHeight = getMaxPanelTransitionDistance();
+ if (mHeightAnimator == null) {
+ // Split shade has its own overscroll logic
+ if (mTracking && !mInSplitShade) {
+ float overExpansionPixels = Math.max(0, h - maxPanelHeight);
+ setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */);
+ }
+ }
+ mExpandedHeight = Math.min(h, maxPanelHeight);
+ // If we are closing the panel and we are almost there due to a slow decelerating
+ // interpolator, abort the animation.
+ if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) {
+ mExpandedHeight = 0f;
+ if (mHeightAnimator != null) {
+ mHeightAnimator.end();
+ }
+ }
+ mExpansionDragDownAmountPx = h;
+ mExpandedFraction = Math.min(1f,
+ maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight);
+ mAmbientState.setExpansionFraction(mExpandedFraction);
+ onHeightUpdated(mExpandedHeight);
+ updatePanelExpansionAndVisibility();
+ });
+ }
+
+ /**
+ * Set the current overexpansion
+ *
+ * @param overExpansion the amount of overexpansion to apply
+ * @param isFromGesture is this amount from a gesture and needs to be rubberBanded?
+ */
+ private void setOverExpansionInternal(float overExpansion, boolean isFromGesture) {
+ if (!isFromGesture) {
+ mLastGesturedOverExpansion = -1;
+ setOverExpansion(overExpansion);
+ } else if (mLastGesturedOverExpansion != overExpansion) {
+ mLastGesturedOverExpansion = overExpansion;
+ final float heightForFullOvershoot = mView.getHeight() / 3.0f;
+ float newExpansion = MathUtils.saturate(overExpansion / heightForFullOvershoot);
+ newExpansion = Interpolators.getOvershootInterpolation(newExpansion);
+ setOverExpansion(newExpansion * mPanelFlingOvershootAmount * 2.0f);
+ }
+ }
+
+ /** Sets the expanded height relative to a number from 0 to 1. */
+ public void setExpandedFraction(float frac) {
+ setExpandedHeight(getMaxPanelTransitionDistance() * frac);
+ }
+
+ @VisibleForTesting
+ float getExpandedHeight() {
+ return mExpandedHeight;
+ }
+
+ public float getExpandedFraction() {
+ return mExpandedFraction;
+ }
+
+ public boolean isFullyExpanded() {
+ return mExpandedHeight >= getMaxPanelHeight();
+ }
+
+ public boolean isFullyCollapsed() {
+ return mExpandedFraction <= 0.0f;
+ }
+
+ public boolean isCollapsing() {
+ return mClosing || mIsLaunchAnimationRunning;
+ }
+
+ public boolean isFlinging() {
+ return mIsFlinging;
+ }
+
+ public boolean isTracking() {
+ return mTracking;
+ }
+
+ /** Returns whether the shade can be collapsed. */
+ public boolean canPanelBeCollapsed() {
+ return !isFullyCollapsed() && !mTracking && !mClosing;
+ }
+
+ /** Collapses the shade instantly without animation. */
+ public void instantCollapse() {
+ abortAnimations();
+ setExpandedFraction(0f);
+ if (mExpanding) {
+ notifyExpandingFinished();
+ }
+ if (mInstantExpanding) {
+ mInstantExpanding = false;
+ updatePanelExpansionAndVisibility();
+ }
+ }
+
+ private void abortAnimations() {
+ cancelHeightAnimator();
+ mView.removeCallbacks(mFlingCollapseRunnable);
+ }
+
+ public boolean isUnlockHintRunning() {
+ return mHintAnimationRunning;
+ }
+
+ /**
+ * Phase 1: Move everything upwards.
+ */
+ private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
+ float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
+ ValueAnimator animator = createHeightAnimator(target);
+ animator.setDuration(250);
+ animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ animator.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mCancelled) {
+ setAnimator(null);
+ onAnimationFinished.run();
+ } else {
+ startUnlockHintAnimationPhase2(onAnimationFinished);
+ }
+ }
+ });
+ animator.start();
+ setAnimator(animator);
+
+ final List<ViewPropertyAnimator> indicationAnimators =
+ mKeyguardBottomArea.getIndicationAreaAnimators();
+ for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) {
+ indicationAreaAnimator
+ .translationY(-mHintDistance)
+ .setDuration(250)
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .withEndAction(() -> indicationAreaAnimator
+ .translationY(0)
+ .setDuration(450)
+ .setInterpolator(mBounceInterpolator)
+ .start())
+ .start();
+ }
+ }
+
+ private void setAnimator(ValueAnimator animator) {
+ mHeightAnimator = animator;
+ if (animator == null && mPanelUpdateWhenAnimatorEnds) {
+ mPanelUpdateWhenAnimatorEnds = false;
+ updateExpandedHeightToMaxHeight();
+ }
+ }
+
+ /**
+ * Phase 2: Bounce down.
+ */
+ private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
+ ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
+ animator.setDuration(450);
+ animator.setInterpolator(mBounceInterpolator);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setAnimator(null);
+ onAnimationFinished.run();
+ updatePanelExpansionAndVisibility();
+ }
+ });
+ animator.start();
+ setAnimator(animator);
+ }
+
+ private ValueAnimator createHeightAnimator(float targetHeight) {
+ return createHeightAnimator(targetHeight, 0.0f /* performOvershoot */);
+ }
+
+ /**
+ * Create an animator that can also overshoot
+ *
+ * @param targetHeight the target height
+ * @param overshootAmount the amount of overshoot desired
+ */
+ private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) {
+ float startExpansion = mOverExpansion;
+ ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
+ animator.addUpdateListener(
+ animation -> {
+ if (overshootAmount > 0.0f
+ // Also remove the overExpansion when collapsing
+ || (targetHeight == 0.0f && startExpansion != 0)) {
+ final float expansion = MathUtils.lerp(
+ startExpansion,
+ mPanelFlingOvershootAmount * overshootAmount,
+ Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
+ animator.getAnimatedFraction()));
+ setOverExpansionInternal(expansion, false /* isFromGesture */);
+ }
+ setExpandedHeightInternal((float) animation.getAnimatedValue());
+ });
+ return animator;
+ }
+
+ /** Update the visibility of {@link NotificationPanelView} if necessary. */
+ private void updateVisibility() {
+ mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE);
+ }
+
+ /**
+ * Updates the panel expansion and {@link NotificationPanelView} visibility if necessary.
+ *
+ * TODO(b/200063118): Could public calls to this method be replaced with calls to
+ * {@link #updateVisibility()}? That would allow us to make this method private.
+ */
+ public void updatePanelExpansionAndVisibility() {
+ mShadeExpansionStateManager.onPanelExpansionChanged(
+ mExpandedFraction, isExpanded(),
+ mTracking, mExpansionDragDownAmountPx);
+ updateVisibility();
+ }
+
+ public boolean isExpanded() {
+ return mExpandedFraction > 0f
+ || mInstantExpanding
+ || isPanelVisibleBecauseOfHeadsUp()
+ || mTracking
+ || mHeightAnimator != null
+ && !mIsSpringBackAnimation;
+ }
+
+ /**
+ * Gets called when the user performs a click anywhere in the empty area of the panel.
+ *
+ * @return whether the panel will be expanded after the action performed by this method
+ */
+ private boolean onEmptySpaceClick() {
+ if (mHintAnimationRunning) {
+ return true;
+ }
+ return onMiddleClicked();
+ }
+
+ @VisibleForTesting
+ boolean isClosing() {
+ return mClosing;
+ }
+
+ /** Collapses the shade with an animation duration in milliseconds. */
+ public void collapseWithDuration(int animationDuration) {
+ mFixedDuration = animationDuration;
+ collapse(false /* delayed */, 1.0f /* speedUpFactor */);
+ mFixedDuration = NO_FIXED_DURATION;
+ }
+
+ /** Returns the NotificationPanelView. */
+ public ViewGroup getView() {
+ // TODO: remove this method, or at least reduce references to it.
+ return mView;
+ }
+
+ private void beginJankMonitoring() {
+ if (mInteractionJankMonitor == null) {
+ return;
+ }
+ InteractionJankMonitor.Configuration.Builder builder =
+ InteractionJankMonitor.Configuration.Builder.withView(
+ InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
+ mView)
+ .setTag(isFullyCollapsed() ? "Expand" : "Collapse");
+ mInteractionJankMonitor.begin(builder);
+ }
+
+ private void endJankMonitoring() {
+ if (mInteractionJankMonitor == null) {
+ return;
+ }
+ InteractionJankMonitor.getInstance().end(
+ InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
+ }
+
+ private void cancelJankMonitoring() {
+ if (mInteractionJankMonitor == null) {
+ return;
+ }
+ InteractionJankMonitor.getInstance().cancel(
+ InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
+ }
+
+ private float getExpansionFraction() {
+ return mExpandedFraction;
+ }
+
+ private ShadeExpansionStateManager getShadeExpansionStateManager() {
+ return mShadeExpansionStateManager;
+ }
+
private class OnHeightChangedListener implements ExpandableView.OnHeightChangedListener {
@Override
public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
@@ -4819,13 +5689,18 @@
}
}
- private class OnLayoutChangeListenerImpl extends OnLayoutChangeListener {
-
+ private final class OnLayoutChangeListener implements View.OnLayoutChangeListener {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
int oldTop, int oldRight, int oldBottom) {
DejankUtils.startDetectingBlockingIpcs("NVP#onLayout");
- super.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom);
+ updateExpandedHeightToMaxHeight();
+ mHasLayoutedSinceDown = true;
+ if (mUpdateFlingOnLayout) {
+ abortAnimations();
+ fling(mUpdateFlingVelocity, true /* expands */);
+ mUpdateFlingOnLayout = false;
+ }
updateMaxDisplayedNotifications(!shouldAvoidChangingNotificationsCount());
setIsFullWidth(mNotificationStackScrollLayoutController.getWidth() == mView.getWidth());
@@ -5083,4 +5958,415 @@
}
}
}
+
+ /** Handles MotionEvents for the Shade. */
+ public final class TouchHandler implements View.OnTouchListener {
+ private long mLastTouchDownTime = -1L;
+
+ /** @see ViewGroup#onInterceptTouchEvent(MotionEvent) */
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (SPEW_LOGCAT) {
+ Log.v(TAG,
+ "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX()
+ + "," + event.getY() + ")");
+ }
+ if (mQs.disallowPanelTouches()) {
+ return false;
+ }
+ initDownStates(event);
+ // Do not let touches go to shade or QS if the bouncer is visible,
+ // but still let user swipe down to expand the panel, dismissing the bouncer.
+ if (mCentralSurfaces.isBouncerShowing()) {
+ return true;
+ }
+ if (mCommandQueue.panelsEnabled()
+ && !mNotificationStackScrollLayoutController.isLongPressInProgress()
+ && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
+ mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
+ mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
+ return true;
+ }
+ if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0)
+ && mPulseExpansionHandler.onInterceptTouchEvent(event)) {
+ return true;
+ }
+
+ if (!isFullyCollapsed() && onQsIntercept(event)) {
+ if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept true");
+ return true;
+ }
+ if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted
+ && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
+ return false;
+ }
+
+ /* If the user drags anywhere inside the panel we intercept it if the movement is
+ upwards. This allows closing the shade from anywhere inside the panel.
+ We only do this if the current content is scrolled to the bottom, i.e.
+ canCollapsePanelOnTouch() is true and therefore there is no conflicting scrolling
+ gesture possible. */
+ int pointerIndex = event.findPointerIndex(mTrackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ mTrackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+ boolean canCollapsePanel = canCollapsePanelOnTouch();
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mCentralSurfaces.userActivity();
+ mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
+ mMinExpandHeight = 0.0f;
+ mDownTime = mSystemClock.uptimeMillis();
+ if (mAnimatingOnDown && mClosing && !mHintAnimationRunning) {
+ cancelHeightAnimator();
+ mTouchSlopExceeded = true;
+ return true;
+ }
+ mInitialExpandY = y;
+ mInitialExpandX = x;
+ mTouchStartedInEmptyArea = !isInContentBounds(x, y);
+ mTouchSlopExceeded = mTouchSlopExceededBeforeDown;
+ mMotionAborted = false;
+ mPanelClosedOnDown = isFullyCollapsed();
+ mCollapsedAndHeadsUpOnDown = false;
+ mHasLayoutedSinceDown = false;
+ mUpdateFlingOnLayout = false;
+ mTouchAboveFalsingThreshold = false;
+ addMovement(event);
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (mTrackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ mTrackingPointer = event.getPointerId(newIndex);
+ mInitialExpandX = event.getX(newIndex);
+ mInitialExpandY = event.getY(newIndex);
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
+ mMotionAborted = true;
+ mVelocityTracker.clear();
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ final float h = y - mInitialExpandY;
+ addMovement(event);
+ final boolean openShadeWithoutHun =
+ mPanelClosedOnDown && !mCollapsedAndHeadsUpOnDown;
+ if (canCollapsePanel || mTouchStartedInEmptyArea || mAnimatingOnDown
+ || openShadeWithoutHun) {
+ float hAbs = Math.abs(h);
+ float touchSlop = getTouchSlop(event);
+ if ((h < -touchSlop
+ || ((openShadeWithoutHun || mAnimatingOnDown) && hAbs > touchSlop))
+ && hAbs > Math.abs(x - mInitialExpandX)) {
+ cancelHeightAnimator();
+ startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
+ return true;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ mVelocityTracker.clear();
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (event.getDownTime() == mLastTouchDownTime) {
+ // An issue can occur when swiping down after unlock, where multiple down
+ // events are received in this handler with identical downTimes. Until the
+ // source of the issue can be located, detect this case and ignore.
+ // see b/193350347
+ Log.w(TAG, "Duplicate down event detected... ignoring");
+ return true;
+ }
+ mLastTouchDownTime = event.getDownTime();
+ }
+
+
+ if (mQsFullyExpanded && mQs != null && mQs.disallowPanelTouches()) {
+ return false;
+ }
+
+ // Do not allow panel expansion if bouncer is scrimmed or showing over a dream,
+ // otherwise user would be able to pull down QS or expand the shade.
+ if (mCentralSurfaces.isBouncerShowingScrimmed()
+ || mCentralSurfaces.isBouncerShowingOverDream()) {
+ return false;
+ }
+
+ // Make sure the next touch won't the blocked after the current ends.
+ if (event.getAction() == MotionEvent.ACTION_UP
+ || event.getAction() == MotionEvent.ACTION_CANCEL) {
+ mBlockingExpansionForCurrentTouch = false;
+ }
+ // When touch focus transfer happens, ACTION_DOWN->ACTION_UP may happen immediately
+ // without any ACTION_MOVE event.
+ // In such case, simply expand the panel instead of being stuck at the bottom bar.
+ if (mLastEventSynthesizedDown && event.getAction() == MotionEvent.ACTION_UP) {
+ expand(true /* animate */);
+ }
+ initDownStates(event);
+
+ // If pulse is expanding already, let's give it the touch. There are situations
+ // where the panel starts expanding even though we're also pulsing
+ boolean pulseShouldGetTouch = (!mIsExpanding
+ && !shouldQuickSettingsIntercept(mDownX, mDownY, 0))
+ || mPulseExpansionHandler.isExpanding();
+ if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) {
+ // We're expanding all the other ones shouldn't get this anymore
+ mShadeLog.logMotionEvent(event, "onTouch: PulseExpansionHandler handled event");
+ return true;
+ }
+ if (mPulsing) {
+ mShadeLog.logMotionEvent(event, "onTouch: eat touch, device pulsing");
+ return true;
+ }
+ if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp()
+ && !mNotificationStackScrollLayoutController.isLongPressInProgress()
+ && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
+ mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
+ }
+ boolean handled = mHeadsUpTouchHelper.onTouchEvent(event);
+
+ if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) {
+ mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event");
+ return true;
+ }
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
+ mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
+ handled = true;
+ }
+
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyExpanded()
+ && mKeyguardStateController.isShowing()) {
+ mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX());
+ }
+
+ handled |= handleTouch(event);
+ return !mDozing || handled;
+ }
+
+ private boolean handleTouch(MotionEvent event) {
+ if (mInstantExpanding) {
+ mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding");
+ return false;
+ }
+ if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) {
+ mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled");
+ return false;
+ }
+ if (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
+ mShadeLog.logMotionEvent(event, "onTouch: non-down action, motion was aborted");
+ return false;
+ }
+
+ // If dragging should not expand the notifications shade, then return false.
+ if (!mNotificationsDragEnabled) {
+ if (mTracking) {
+ // Turn off tracking if it's on or the shade can get stuck in the down position.
+ onTrackingStopped(true /* expand */);
+ }
+ mShadeLog.logMotionEvent(event, "onTouch: drag not enabled");
+ return false;
+ }
+
+ // On expanding, single mouse click expands the panel instead of dragging.
+ if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ expand(true);
+ }
+ return true;
+ }
+
+ /*
+ * We capture touch events here and update the expand height here in case according to
+ * the users fingers. This also handles multi-touch.
+ *
+ * Flinging is also enabled in order to open or close the shade.
+ */
+
+ int pointerIndex = event.findPointerIndex(mTrackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ mTrackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop();
+ mIgnoreXTouchSlop = true;
+ }
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
+ mMinExpandHeight = 0.0f;
+ mPanelClosedOnDown = isFullyCollapsed();
+ mHasLayoutedSinceDown = false;
+ mUpdateFlingOnLayout = false;
+ mMotionAborted = false;
+ mDownTime = mSystemClock.uptimeMillis();
+ mTouchAboveFalsingThreshold = false;
+ mCollapsedAndHeadsUpOnDown =
+ isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp();
+ addMovement(event);
+ boolean regularHeightAnimationRunning = mHeightAnimator != null
+ && !mHintAnimationRunning && !mIsSpringBackAnimation;
+ if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) {
+ mTouchSlopExceeded = regularHeightAnimationRunning
+ || mTouchSlopExceededBeforeDown;
+ cancelHeightAnimator();
+ onTrackingStarted();
+ }
+ if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()
+ && !mCentralSurfaces.isBouncerShowing()) {
+ startOpening(event);
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (mTrackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ final float newY = event.getY(newIndex);
+ final float newX = event.getX(newIndex);
+ mTrackingPointer = event.getPointerId(newIndex);
+ mHandlingPointerUp = true;
+ startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
+ mHandlingPointerUp = false;
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
+ mMotionAborted = true;
+ endMotionEvent(event, x, y, true /* forceCancel */);
+ return false;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ addMovement(event);
+ if (!isFullyCollapsed()) {
+ maybeVibrateOnOpening(true /* openingWithTouch */);
+ }
+ float h = y - mInitialExpandY;
+
+ // If the panel was collapsed when touching, we only need to check for the
+ // y-component of the gesture, as we have no conflicting horizontal gesture.
+ if (Math.abs(h) > getTouchSlop(event)
+ && (Math.abs(h) > Math.abs(x - mInitialExpandX)
+ || mIgnoreXTouchSlop)) {
+ mTouchSlopExceeded = true;
+ if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
+ if (mInitialOffsetOnTouch != 0f) {
+ startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
+ h = 0;
+ }
+ cancelHeightAnimator();
+ onTrackingStarted();
+ }
+ }
+ float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
+ newHeight = Math.max(newHeight, mMinExpandHeight);
+ if (-h >= getFalsingThreshold()) {
+ mTouchAboveFalsingThreshold = true;
+ mUpwardsWhenThresholdReached = isDirectionUpwards(x, y);
+ }
+ if ((!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) {
+ // Count h==0 as part of swipe-up,
+ // otherwise {@link NotificationStackScrollLayout}
+ // wrongly enables stack height updates at the start of lockscreen swipe-up
+ mAmbientState.setSwipingUp(h <= 0);
+ setExpandedHeightInternal(newHeight);
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ addMovement(event);
+ endMotionEvent(event, x, y, false /* forceCancel */);
+ // mHeightAnimator is null, there is no remaining frame, ends instrumenting.
+ if (mHeightAnimator == null) {
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ endJankMonitoring();
+ } else {
+ cancelJankMonitoring();
+ }
+ }
+ break;
+ }
+ return !mGestureWaitForTouchSlop || mTracking;
+ }
+ }
+
+ /** Listens for config changes. */
+ public class OnConfigurationChangedListener implements
+ NotificationPanelView.OnConfigurationChangedListener {
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ 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/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 6be9bbb..65bd58d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -52,7 +52,6 @@
import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
import java.io.PrintWriter;
@@ -91,7 +90,7 @@
private boolean mExpandingBelowNotch;
private final DockManager mDockManager;
private final NotificationPanelViewController mNotificationPanelViewController;
- private final PanelExpansionStateManager mPanelExpansionStateManager;
+ private final ShadeExpansionStateManager mShadeExpansionStateManager;
private boolean mIsTrackingBarGesture = false;
@@ -104,7 +103,7 @@
NotificationShadeDepthController depthController,
NotificationShadeWindowView notificationShadeWindowView,
NotificationPanelViewController notificationPanelViewController,
- PanelExpansionStateManager panelExpansionStateManager,
+ ShadeExpansionStateManager shadeExpansionStateManager,
NotificationStackScrollLayoutController notificationStackScrollLayoutController,
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
StatusBarWindowStateController statusBarWindowStateController,
@@ -124,7 +123,7 @@
mView = notificationShadeWindowView;
mDockManager = dockManager;
mNotificationPanelViewController = notificationPanelViewController;
- mPanelExpansionStateManager = panelExpansionStateManager;
+ mShadeExpansionStateManager = shadeExpansionStateManager;
mDepthController = depthController;
mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
@@ -404,7 +403,7 @@
setDragDownHelper(mLockscreenShadeTransitionController.getTouchHelper());
mDepthController.setRoot(mView);
- mPanelExpansionStateManager.addExpansionListener(mDepthController);
+ mShadeExpansionStateManager.addExpansionListener(mDepthController);
}
public NotificationShadeWindowView getView() {
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/OWNERS b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
index 7dc9dc7..49709a8 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
@@ -1,3 +1,6 @@
+justinweir@google.com
+syeonlee@google.com
+
per-file *Notification* = set noparent
per-file *Notification* = file:../statusbar/notification/OWNERS
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java
deleted file mode 100644
index b4ce95c..0000000
--- a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java
+++ /dev/null
@@ -1,1494 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shade;
-
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-
-import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
-import static com.android.systemui.classifier.Classifier.GENERIC;
-import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
-import static com.android.systemui.classifier.Classifier.UNLOCK;
-import static com.android.systemui.shade.NotificationPanelView.DEBUG;
-
-import static java.lang.Float.isNaN;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.VibrationEffect;
-import android.util.Log;
-import android.util.MathUtils;
-import android.view.InputDevice;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewPropertyAnimator;
-import android.view.ViewTreeObserver;
-import android.view.animation.Interpolator;
-
-import com.android.internal.jank.InteractionJankMonitor;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.internal.util.LatencyTracker;
-import com.android.systemui.DejankUtils;
-import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
-import com.android.systemui.classifier.Classifier;
-import com.android.systemui.doze.DozeLog;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.notification.stack.AmbientState;
-import com.android.systemui.statusbar.phone.BounceInterpolator;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
-import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
-import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
-import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.time.SystemClock;
-import com.android.wm.shell.animation.FlingAnimationUtils;
-
-import java.io.PrintWriter;
-import java.util.List;
-
-public abstract class PanelViewController {
- public static final String TAG = NotificationPanelView.class.getSimpleName();
- public static final float FLING_MAX_LENGTH_SECONDS = 0.6f;
- public static final float FLING_SPEED_UP_FACTOR = 0.6f;
- public static final float FLING_CLOSING_MAX_LENGTH_SECONDS = 0.6f;
- public static final float FLING_CLOSING_SPEED_UP_FACTOR = 0.6f;
- private static final int NO_FIXED_DURATION = -1;
- private static final long SHADE_OPEN_SPRING_OUT_DURATION = 350L;
- private static final long SHADE_OPEN_SPRING_BACK_DURATION = 400L;
-
- /**
- * The factor of the usual high velocity that is needed in order to reach the maximum overshoot
- * when flinging. A low value will make it that most flings will reach the maximum overshoot.
- */
- private static final float FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT = 0.5f;
-
- protected long mDownTime;
- protected boolean mTouchSlopExceededBeforeDown;
- private float mMinExpandHeight;
- private boolean mPanelUpdateWhenAnimatorEnds;
- private final boolean mVibrateOnOpening;
- private boolean mHasVibratedOnOpen = false;
- protected boolean mIsLaunchAnimationRunning;
- private int mFixedDuration = NO_FIXED_DURATION;
- protected float mOverExpansion;
-
- /**
- * The overshoot amount when the panel flings open
- */
- private float mPanelFlingOvershootAmount;
-
- /**
- * The amount of pixels that we have overexpanded the last time with a gesture
- */
- private float mLastGesturedOverExpansion = -1;
-
- /**
- * Is the current animator the spring back animation?
- */
- private boolean mIsSpringBackAnimation;
-
- private boolean mInSplitShade;
-
- private void logf(String fmt, Object... args) {
- Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
- }
-
- protected CentralSurfaces mCentralSurfaces;
- protected HeadsUpManagerPhone mHeadsUpManager;
- protected final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
-
- private float mHintDistance;
- private float mInitialOffsetOnTouch;
- private boolean mCollapsedAndHeadsUpOnDown;
- private float mExpandedFraction = 0;
- private float mExpansionDragDownAmountPx = 0;
- protected float mExpandedHeight = 0;
- private boolean mPanelClosedOnDown;
- private boolean mHasLayoutedSinceDown;
- private float mUpdateFlingVelocity;
- private boolean mUpdateFlingOnLayout;
- private boolean mClosing;
- protected boolean mTracking;
- private boolean mTouchSlopExceeded;
- private int mTrackingPointer;
- private int mTouchSlop;
- private float mSlopMultiplier;
- protected boolean mHintAnimationRunning;
- private boolean mTouchAboveFalsingThreshold;
- private boolean mTouchStartedInEmptyArea;
- private boolean mMotionAborted;
- private boolean mUpwardsWhenThresholdReached;
- private boolean mAnimatingOnDown;
- private boolean mHandlingPointerUp;
-
- private ValueAnimator mHeightAnimator;
- private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
- private final FlingAnimationUtils mFlingAnimationUtils;
- private final FlingAnimationUtils mFlingAnimationUtilsClosing;
- private final FlingAnimationUtils mFlingAnimationUtilsDismissing;
- private final LatencyTracker mLatencyTracker;
- private final FalsingManager mFalsingManager;
- private final DozeLog mDozeLog;
- private final VibratorHelper mVibratorHelper;
-
- /**
- * Whether an instant expand request is currently pending and we are just waiting for layout.
- */
- private boolean mInstantExpanding;
- private boolean mAnimateAfterExpanding;
- private boolean mIsFlinging;
-
- private String mViewName;
- private float mInitialExpandY;
- private float mInitialExpandX;
- private boolean mTouchDisabled;
- private boolean mInitialTouchFromKeyguard;
-
- /**
- * Whether or not the NotificationPanelView can be expanded or collapsed with a drag.
- */
- private final boolean mNotificationsDragEnabled;
-
- private final Interpolator mBounceInterpolator;
- protected KeyguardBottomAreaView mKeyguardBottomArea;
-
- /**
- * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time.
- */
- private float mNextCollapseSpeedUpFactor = 1.0f;
-
- protected boolean mExpanding;
- private boolean mGestureWaitForTouchSlop;
- private boolean mIgnoreXTouchSlop;
- private boolean mExpandLatencyTracking;
- private final NotificationPanelView mView;
- private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
- private final NotificationShadeWindowController mNotificationShadeWindowController;
- protected final Resources mResources;
- protected final KeyguardStateController mKeyguardStateController;
- protected final SysuiStatusBarStateController mStatusBarStateController;
- protected final AmbientState mAmbientState;
- protected final LockscreenGestureLogger mLockscreenGestureLogger;
- private final PanelExpansionStateManager mPanelExpansionStateManager;
- private final InteractionJankMonitor mInteractionJankMonitor;
- protected final SystemClock mSystemClock;
-
- protected final ShadeLogger mShadeLog;
-
- protected abstract void onExpandingFinished();
-
- protected void onExpandingStarted() {
- }
-
- protected void notifyExpandingStarted() {
- if (!mExpanding) {
- mExpanding = true;
- onExpandingStarted();
- }
- }
-
- protected final void notifyExpandingFinished() {
- endClosing();
- if (mExpanding) {
- mExpanding = false;
- onExpandingFinished();
- }
- }
-
- protected AmbientState getAmbientState() {
- return mAmbientState;
- }
-
- public PanelViewController(
- NotificationPanelView view,
- FalsingManager falsingManager,
- DozeLog dozeLog,
- KeyguardStateController keyguardStateController,
- SysuiStatusBarStateController statusBarStateController,
- NotificationShadeWindowController notificationShadeWindowController,
- VibratorHelper vibratorHelper,
- StatusBarKeyguardViewManager statusBarKeyguardViewManager,
- LatencyTracker latencyTracker,
- FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
- StatusBarTouchableRegionManager statusBarTouchableRegionManager,
- LockscreenGestureLogger lockscreenGestureLogger,
- PanelExpansionStateManager panelExpansionStateManager,
- AmbientState ambientState,
- InteractionJankMonitor interactionJankMonitor,
- ShadeLogger shadeLogger,
- SystemClock systemClock) {
- keyguardStateController.addCallback(new KeyguardStateController.Callback() {
- @Override
- public void onKeyguardFadingAwayChanged() {
- updateExpandedHeightToMaxHeight();
- }
- });
- mAmbientState = ambientState;
- mView = view;
- mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
- mLockscreenGestureLogger = lockscreenGestureLogger;
- mPanelExpansionStateManager = panelExpansionStateManager;
- mShadeLog = shadeLogger;
- TouchHandler touchHandler = createTouchHandler();
- mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View v) {
- mViewName = mResources.getResourceName(mView.getId());
- }
-
- @Override
- public void onViewDetachedFromWindow(View v) {
- }
- });
-
- mView.addOnLayoutChangeListener(createLayoutChangeListener());
- mView.setOnTouchListener(touchHandler);
- mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener());
-
- mResources = mView.getResources();
- mKeyguardStateController = keyguardStateController;
- mStatusBarStateController = statusBarStateController;
- mNotificationShadeWindowController = notificationShadeWindowController;
- mFlingAnimationUtils = flingAnimationUtilsBuilder
- .reset()
- .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS)
- .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
- .build();
- mFlingAnimationUtilsClosing = flingAnimationUtilsBuilder
- .reset()
- .setMaxLengthSeconds(FLING_CLOSING_MAX_LENGTH_SECONDS)
- .setSpeedUpFactor(FLING_CLOSING_SPEED_UP_FACTOR)
- .build();
- mFlingAnimationUtilsDismissing = flingAnimationUtilsBuilder
- .reset()
- .setMaxLengthSeconds(0.5f)
- .setSpeedUpFactor(0.6f)
- .setX2(0.6f)
- .setY2(0.84f)
- .build();
- mLatencyTracker = latencyTracker;
- mBounceInterpolator = new BounceInterpolator();
- mFalsingManager = falsingManager;
- mDozeLog = dozeLog;
- mNotificationsDragEnabled = mResources.getBoolean(
- R.bool.config_enableNotificationShadeDrag);
- mVibratorHelper = vibratorHelper;
- mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation);
- mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
- mInteractionJankMonitor = interactionJankMonitor;
- mSystemClock = systemClock;
- }
-
- protected void loadDimens() {
- final ViewConfiguration configuration = ViewConfiguration.get(mView.getContext());
- mTouchSlop = configuration.getScaledTouchSlop();
- mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
- mHintDistance = mResources.getDimension(R.dimen.hint_move_distance);
- mPanelFlingOvershootAmount = mResources.getDimension(R.dimen.panel_overshoot_amount);
- mInSplitShade = mResources.getBoolean(R.bool.config_use_split_notification_shade);
- }
-
- protected float getTouchSlop(MotionEvent event) {
- // Adjust the touch slop if another gesture may be being performed.
- return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
- ? mTouchSlop * mSlopMultiplier
- : mTouchSlop;
- }
-
- private void addMovement(MotionEvent event) {
- // Add movement to velocity tracker using raw screen X and Y coordinates instead
- // of window coordinates because the window frame may be moving at the same time.
- float deltaX = event.getRawX() - event.getX();
- float deltaY = event.getRawY() - event.getY();
- event.offsetLocation(deltaX, deltaY);
- mVelocityTracker.addMovement(event);
- event.offsetLocation(-deltaX, -deltaY);
- }
-
- public void setTouchAndAnimationDisabled(boolean disabled) {
- mTouchDisabled = disabled;
- if (mTouchDisabled) {
- cancelHeightAnimator();
- if (mTracking) {
- onTrackingStopped(true /* expanded */);
- }
- notifyExpandingFinished();
- }
- }
-
- public void startExpandLatencyTracking() {
- if (mLatencyTracker.isEnabled()) {
- mLatencyTracker.onActionStart(LatencyTracker.ACTION_EXPAND_PANEL);
- mExpandLatencyTracking = true;
- }
- }
-
- private void startOpening(MotionEvent event) {
- updatePanelExpansionAndVisibility();
- // Reset at start so haptic can be triggered as soon as panel starts to open.
- mHasVibratedOnOpen = false;
- //TODO: keyguard opens QS a different way; log that too?
-
- // Log the position of the swipe that opened the panel
- float width = mCentralSurfaces.getDisplayWidth();
- float height = mCentralSurfaces.getDisplayHeight();
- int rot = mCentralSurfaces.getRotation();
-
- mLockscreenGestureLogger.writeAtFractionalPosition(MetricsEvent.ACTION_PANEL_VIEW_EXPAND,
- (int) (event.getX() / width * 100), (int) (event.getY() / height * 100), rot);
- mLockscreenGestureLogger
- .log(LockscreenUiEvent.LOCKSCREEN_UNLOCKED_NOTIFICATION_PANEL_EXPAND);
- }
-
- /**
- * 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.
- */
- protected void maybeVibrateOnOpening(boolean openingWithTouch) {
- if (mVibrateOnOpening) {
- if (!openingWithTouch || !mHasVibratedOnOpen) {
- mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
- mHasVibratedOnOpen = true;
- }
- }
- }
-
- protected abstract float getOpeningHeight();
-
- /**
- * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
- * horizontal direction
- */
- private boolean isDirectionUpwards(float x, float y) {
- float xDiff = x - mInitialExpandX;
- float yDiff = y - mInitialExpandY;
- if (yDiff >= 0) {
- return false;
- }
- return Math.abs(yDiff) >= Math.abs(xDiff);
- }
-
- public void startExpandMotion(float newX, float newY, boolean startTracking,
- float expandedHeight) {
- if (!mHandlingPointerUp && !mStatusBarStateController.isDozing()) {
- beginJankMonitoring();
- }
- mInitialOffsetOnTouch = expandedHeight;
- mInitialExpandY = newY;
- mInitialExpandX = newX;
- mInitialTouchFromKeyguard = mKeyguardStateController.isShowing();
- if (startTracking) {
- mTouchSlopExceeded = true;
- setExpandedHeight(mInitialOffsetOnTouch);
- onTrackingStarted();
- }
- }
-
- private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
- mTrackingPointer = -1;
- mAmbientState.setSwipingUp(false);
- if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop
- || Math.abs(y - mInitialExpandY) > mTouchSlop
- || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
- mVelocityTracker.computeCurrentVelocity(1000);
- float vel = mVelocityTracker.getYVelocity();
- float vectorVel = (float) Math.hypot(
- mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
-
- final boolean onKeyguard = mKeyguardStateController.isShowing();
- final boolean expand;
- if (mKeyguardStateController.isKeyguardFadingAway()
- || (mInitialTouchFromKeyguard && !onKeyguard)) {
- // Don't expand for any touches that started from the keyguard and ended after the
- // keyguard is gone.
- expand = false;
- } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
- if (onKeyguard) {
- expand = true;
- } else if (mCentralSurfaces.isBouncerShowingOverDream()) {
- expand = false;
- } else {
- // If we get a cancel, put the shade back to the state it was in when the
- // gesture started
- expand = !mPanelClosedOnDown;
- }
- } else {
- expand = flingExpands(vel, vectorVel, x, y);
- }
-
- mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
- mCentralSurfaces.isFalsingThresholdNeeded(),
- mCentralSurfaces.isWakeUpComingFromTouch());
- // Log collapse gesture if on lock screen.
- if (!expand && onKeyguard) {
- float displayDensity = mCentralSurfaces.getDisplayDensity();
- int heightDp = (int) Math.abs((y - mInitialExpandY) / displayDensity);
- int velocityDp = (int) Math.abs(vel / displayDensity);
- mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_UNLOCK, heightDp, velocityDp);
- mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_UNLOCK);
- }
- @Classifier.InteractionType int interactionType = vel == 0 ? GENERIC
- : y - mInitialExpandY > 0 ? QUICK_SETTINGS
- : (mKeyguardStateController.canDismissLockScreen()
- ? UNLOCK : BOUNCER_UNLOCK);
-
- fling(vel, expand, isFalseTouch(x, y, interactionType));
- onTrackingStopped(expand);
- mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
- if (mUpdateFlingOnLayout) {
- mUpdateFlingVelocity = vel;
- }
- } else if (!mCentralSurfaces.isBouncerShowing()
- && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()
- && !mKeyguardStateController.isKeyguardGoingAway()) {
- boolean expands = onEmptySpaceClick();
- onTrackingStopped(expands);
- }
- mVelocityTracker.clear();
- }
-
- protected float getCurrentExpandVelocity() {
- mVelocityTracker.computeCurrentVelocity(1000);
- return mVelocityTracker.getYVelocity();
- }
-
- protected abstract int getFalsingThreshold();
-
- protected abstract boolean shouldGestureWaitForTouchSlop();
-
- protected void onTrackingStopped(boolean expand) {
- mTracking = false;
- mCentralSurfaces.onTrackingStopped(expand);
- updatePanelExpansionAndVisibility();
- }
-
- protected void onTrackingStarted() {
- endClosing();
- mTracking = true;
- mCentralSurfaces.onTrackingStarted();
- notifyExpandingStarted();
- updatePanelExpansionAndVisibility();
- }
-
- /**
- * @return Whether a pair of coordinates are inside the visible view content bounds.
- */
- protected abstract boolean isInContentBounds(float x, float y);
-
- protected void cancelHeightAnimator() {
- if (mHeightAnimator != null) {
- if (mHeightAnimator.isRunning()) {
- mPanelUpdateWhenAnimatorEnds = false;
- }
- mHeightAnimator.cancel();
- }
- endClosing();
- }
-
- private void endClosing() {
- if (mClosing) {
- setIsClosing(false);
- onClosingFinished();
- }
- }
-
- protected abstract boolean canCollapsePanelOnTouch();
-
- protected float getContentHeight() {
- return mExpandedHeight;
- }
-
- /**
- * @param vel the current vertical velocity of the motion
- * @param vectorVel the length of the vectorial velocity
- * @return whether a fling should expands the panel; contracts otherwise
- */
- protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
- if (mFalsingManager.isUnlockingDisabled()) {
- return true;
- }
-
- @Classifier.InteractionType int interactionType = y - mInitialExpandY > 0
- ? QUICK_SETTINGS : (
- mKeyguardStateController.canDismissLockScreen() ? UNLOCK : BOUNCER_UNLOCK);
-
- if (isFalseTouch(x, y, interactionType)) {
- return true;
- }
- if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
- return shouldExpandWhenNotFlinging();
- } else {
- return vel > 0;
- }
- }
-
- protected boolean shouldExpandWhenNotFlinging() {
- return getExpandedFraction() > 0.5f;
- }
-
- /**
- * @param x the final x-coordinate when the finger was lifted
- * @param y the final y-coordinate when the finger was lifted
- * @return whether this motion should be regarded as a false touch
- */
- private boolean isFalseTouch(float x, float y,
- @Classifier.InteractionType int interactionType) {
- if (!mCentralSurfaces.isFalsingThresholdNeeded()) {
- return false;
- }
- if (mFalsingManager.isClassifierEnabled()) {
- return mFalsingManager.isFalseTouch(interactionType);
- }
- if (!mTouchAboveFalsingThreshold) {
- return true;
- }
- if (mUpwardsWhenThresholdReached) {
- return false;
- }
- return !isDirectionUpwards(x, y);
- }
-
- protected void fling(float vel, boolean expand) {
- fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
- }
-
- protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) {
- fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing);
- }
-
- protected void fling(float vel, boolean expand, float collapseSpeedUpFactor,
- boolean expandBecauseOfFalsing) {
- float target = expand ? getMaxPanelHeight() : 0;
- if (!expand) {
- setIsClosing(true);
- }
- flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
- }
-
- protected void flingToHeight(float vel, boolean expand, float target,
- float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
- if (target == mExpandedHeight && mOverExpansion == 0.0f) {
- // We're at the target and didn't fling and there's no overshoot
- onFlingEnd(false /* cancelled */);
- return;
- }
- mIsFlinging = true;
- // we want to perform an overshoot animation when flinging open
- final boolean addOverscroll =
- expand
- && !mInSplitShade // Split shade has its own overscroll logic
- && mStatusBarStateController.getState() != StatusBarState.KEYGUARD
- && mOverExpansion == 0.0f
- && vel >= 0;
- final boolean shouldSpringBack = addOverscroll || (mOverExpansion != 0.0f && expand);
- float overshootAmount = 0.0f;
- if (addOverscroll) {
- // Let's overshoot depending on the amount of velocity
- overshootAmount = MathUtils.lerp(
- 0.2f,
- 1.0f,
- MathUtils.saturate(vel
- / (mFlingAnimationUtils.getHighVelocityPxPerSecond()
- * FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT)));
- overshootAmount += mOverExpansion / mPanelFlingOvershootAmount;
- }
- ValueAnimator animator = createHeightAnimator(target, overshootAmount);
- if (expand) {
- if (expandBecauseOfFalsing && vel < 0) {
- vel = 0;
- }
- mFlingAnimationUtils.apply(animator, mExpandedHeight,
- target + overshootAmount * mPanelFlingOvershootAmount, vel, mView.getHeight());
- if (vel == 0) {
- animator.setDuration(SHADE_OPEN_SPRING_OUT_DURATION);
- }
- } else {
- if (shouldUseDismissingAnimation()) {
- if (vel == 0) {
- animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
- long duration = (long) (200 + mExpandedHeight / mView.getHeight() * 100);
- animator.setDuration(duration);
- } else {
- mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel,
- mView.getHeight());
- }
- } else {
- mFlingAnimationUtilsClosing.apply(
- animator, mExpandedHeight, target, vel, mView.getHeight());
- }
-
- // Make it shorter if we run a canned animation
- if (vel == 0) {
- animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor));
- }
- if (mFixedDuration != NO_FIXED_DURATION) {
- animator.setDuration(mFixedDuration);
- }
- }
- animator.addListener(new AnimatorListenerAdapter() {
- private boolean mCancelled;
-
- @Override
- public void onAnimationStart(Animator animation) {
- if (!mStatusBarStateController.isDozing()) {
- beginJankMonitoring();
- }
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- mCancelled = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- if (shouldSpringBack && !mCancelled) {
- // After the shade is flinged open to an overscrolled state, spring back
- // the shade by reducing section padding to 0.
- springBack();
- } else {
- onFlingEnd(mCancelled);
- }
- }
- });
- setAnimator(animator);
- animator.start();
- }
-
- private void springBack() {
- if (mOverExpansion == 0) {
- onFlingEnd(false /* cancelled */);
- return;
- }
- mIsSpringBackAnimation = true;
- ValueAnimator animator = ValueAnimator.ofFloat(mOverExpansion, 0);
- animator.addUpdateListener(
- animation -> setOverExpansionInternal((float) animation.getAnimatedValue(),
- false /* isFromGesture */));
- animator.setDuration(SHADE_OPEN_SPRING_BACK_DURATION);
- animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
- animator.addListener(new AnimatorListenerAdapter() {
- private boolean mCancelled;
- @Override
- public void onAnimationCancel(Animator animation) {
- mCancelled = true;
- }
- @Override
- public void onAnimationEnd(Animator animation) {
- mIsSpringBackAnimation = false;
- onFlingEnd(mCancelled);
- }
- });
- setAnimator(animator);
- animator.start();
- }
-
- protected void onFlingEnd(boolean cancelled) {
- mIsFlinging = false;
- // No overshoot when the animation ends
- setOverExpansionInternal(0, false /* isFromGesture */);
- setAnimator(null);
- mKeyguardStateController.notifyPanelFlingEnd();
- if (!cancelled) {
- endJankMonitoring();
- notifyExpandingFinished();
- } else {
- cancelJankMonitoring();
- }
- updatePanelExpansionAndVisibility();
- }
-
- protected abstract boolean shouldUseDismissingAnimation();
-
- public String getName() {
- return mViewName;
- }
-
- public void setExpandedHeight(float height) {
- if (DEBUG) logf("setExpandedHeight(%.1f)", height);
- setExpandedHeightInternal(height);
- }
-
- void updateExpandedHeightToMaxHeight() {
- float currentMaxPanelHeight = getMaxPanelHeight();
-
- if (isFullyCollapsed()) {
- return;
- }
-
- if (currentMaxPanelHeight == mExpandedHeight) {
- return;
- }
-
- if (mTracking && !isTrackingBlocked()) {
- return;
- }
-
- if (mHeightAnimator != null && !mIsSpringBackAnimation) {
- mPanelUpdateWhenAnimatorEnds = true;
- return;
- }
-
- setExpandedHeight(currentMaxPanelHeight);
- }
-
- /**
- * Returns drag down distance after which panel should be fully expanded. Usually it's the
- * same as max panel height but for large screen devices (especially split shade) we might
- * want to return different value to shorten drag distance
- */
- public abstract int getMaxPanelTransitionDistance();
-
- public void setExpandedHeightInternal(float h) {
- if (isNaN(h)) {
- Log.wtf(TAG, "ExpandedHeight set to NaN");
- }
- mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
- if (mExpandLatencyTracking && h != 0f) {
- DejankUtils.postAfterTraversal(
- () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL));
- mExpandLatencyTracking = false;
- }
- float maxPanelHeight = getMaxPanelTransitionDistance();
- if (mHeightAnimator == null) {
- // Split shade has its own overscroll logic
- if (mTracking && !mInSplitShade) {
- float overExpansionPixels = Math.max(0, h - maxPanelHeight);
- setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */);
- }
- }
- mExpandedHeight = Math.min(h, maxPanelHeight);
- // If we are closing the panel and we are almost there due to a slow decelerating
- // interpolator, abort the animation.
- if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) {
- mExpandedHeight = 0f;
- if (mHeightAnimator != null) {
- mHeightAnimator.end();
- }
- }
- mExpansionDragDownAmountPx = h;
- mExpandedFraction = Math.min(1f,
- maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight);
- mAmbientState.setExpansionFraction(mExpandedFraction);
- onHeightUpdated(mExpandedHeight);
- updatePanelExpansionAndVisibility();
- });
- }
-
- /**
- * @return true if the panel tracking should be temporarily blocked; this is used when a
- * conflicting gesture (opening QS) is happening
- */
- protected abstract boolean isTrackingBlocked();
-
- protected void setOverExpansion(float overExpansion) {
- mOverExpansion = overExpansion;
- }
-
- /**
- * Set the current overexpansion
- *
- * @param overExpansion the amount of overexpansion to apply
- * @param isFromGesture is this amount from a gesture and needs to be rubberBanded?
- */
- private void setOverExpansionInternal(float overExpansion, boolean isFromGesture) {
- if (!isFromGesture) {
- mLastGesturedOverExpansion = -1;
- setOverExpansion(overExpansion);
- } else if (mLastGesturedOverExpansion != overExpansion) {
- mLastGesturedOverExpansion = overExpansion;
- final float heightForFullOvershoot = mView.getHeight() / 3.0f;
- float newExpansion = MathUtils.saturate(overExpansion / heightForFullOvershoot);
- newExpansion = Interpolators.getOvershootInterpolation(newExpansion);
- setOverExpansion(newExpansion * mPanelFlingOvershootAmount * 2.0f);
- }
- }
-
- protected abstract void onHeightUpdated(float expandedHeight);
-
- /**
- * This returns the maximum height of the panel. Children should override this if their
- * desired height is not the full height.
- *
- * @return the default implementation simply returns the maximum height.
- */
- protected abstract int getMaxPanelHeight();
-
- public void setExpandedFraction(float frac) {
- setExpandedHeight(getMaxPanelTransitionDistance() * frac);
- }
-
- public float getExpandedHeight() {
- return mExpandedHeight;
- }
-
- public float getExpandedFraction() {
- return mExpandedFraction;
- }
-
- public boolean isFullyExpanded() {
- return mExpandedHeight >= getMaxPanelHeight();
- }
-
- public boolean isFullyCollapsed() {
- return mExpandedFraction <= 0.0f;
- }
-
- public boolean isCollapsing() {
- return mClosing || mIsLaunchAnimationRunning;
- }
-
- public boolean isFlinging() {
- return mIsFlinging;
- }
-
- public boolean isTracking() {
- return mTracking;
- }
-
- public void collapse(boolean delayed, float speedUpFactor) {
- if (DEBUG) logf("collapse: " + this);
- if (canPanelBeCollapsed()) {
- cancelHeightAnimator();
- notifyExpandingStarted();
-
- // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
- setIsClosing(true);
- if (delayed) {
- mNextCollapseSpeedUpFactor = speedUpFactor;
- mView.postDelayed(mFlingCollapseRunnable, 120);
- } else {
- fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */);
- }
- }
- }
-
- public boolean canPanelBeCollapsed() {
- return !isFullyCollapsed() && !mTracking && !mClosing;
- }
-
- private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
- mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
-
- public void expand(final boolean animate) {
- if (!isFullyCollapsed() && !isCollapsing()) {
- return;
- }
-
- mInstantExpanding = true;
- mAnimateAfterExpanding = animate;
- mUpdateFlingOnLayout = false;
- abortAnimations();
- if (mTracking) {
- onTrackingStopped(true /* expands */); // The panel is expanded after this call.
- }
- if (mExpanding) {
- notifyExpandingFinished();
- }
- updatePanelExpansionAndVisibility();
-
- // Wait for window manager to pickup the change, so we know the maximum height of the panel
- // then.
- mView.getViewTreeObserver().addOnGlobalLayoutListener(
- new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- if (!mInstantExpanding) {
- mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
- return;
- }
- if (mCentralSurfaces.getNotificationShadeWindowView().isVisibleToUser()) {
- mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
- if (mAnimateAfterExpanding) {
- notifyExpandingStarted();
- beginJankMonitoring();
- fling(0, true /* expand */);
- } else {
- setExpandedFraction(1f);
- }
- mInstantExpanding = false;
- }
- }
- });
-
- // Make sure a layout really happens.
- mView.requestLayout();
- }
-
- public void instantCollapse() {
- abortAnimations();
- setExpandedFraction(0f);
- if (mExpanding) {
- notifyExpandingFinished();
- }
- if (mInstantExpanding) {
- mInstantExpanding = false;
- updatePanelExpansionAndVisibility();
- }
- }
-
- private void abortAnimations() {
- cancelHeightAnimator();
- mView.removeCallbacks(mFlingCollapseRunnable);
- }
-
- protected abstract void onClosingFinished();
-
- protected void startUnlockHintAnimation() {
-
- // We don't need to hint the user if an animation is already running or the user is changing
- // the expansion.
- if (mHeightAnimator != null || mTracking) {
- return;
- }
- notifyExpandingStarted();
- startUnlockHintAnimationPhase1(() -> {
- notifyExpandingFinished();
- onUnlockHintFinished();
- mHintAnimationRunning = false;
- });
- onUnlockHintStarted();
- mHintAnimationRunning = true;
- }
-
- protected void onUnlockHintFinished() {
- mCentralSurfaces.onHintFinished();
- }
-
- protected void onUnlockHintStarted() {
- mCentralSurfaces.onUnlockHintStarted();
- }
-
- public boolean isUnlockHintRunning() {
- return mHintAnimationRunning;
- }
-
- /**
- * Phase 1: Move everything upwards.
- */
- private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
- float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
- ValueAnimator animator = createHeightAnimator(target);
- animator.setDuration(250);
- animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
- animator.addListener(new AnimatorListenerAdapter() {
- private boolean mCancelled;
-
- @Override
- public void onAnimationCancel(Animator animation) {
- mCancelled = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- if (mCancelled) {
- setAnimator(null);
- onAnimationFinished.run();
- } else {
- startUnlockHintAnimationPhase2(onAnimationFinished);
- }
- }
- });
- animator.start();
- setAnimator(animator);
-
- final List<ViewPropertyAnimator> indicationAnimators =
- mKeyguardBottomArea.getIndicationAreaAnimators();
- for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) {
- indicationAreaAnimator
- .translationY(-mHintDistance)
- .setDuration(250)
- .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
- .withEndAction(() -> indicationAreaAnimator
- .translationY(0)
- .setDuration(450)
- .setInterpolator(mBounceInterpolator)
- .start())
- .start();
- }
- }
-
- private void setAnimator(ValueAnimator animator) {
- mHeightAnimator = animator;
- if (animator == null && mPanelUpdateWhenAnimatorEnds) {
- mPanelUpdateWhenAnimatorEnds = false;
- updateExpandedHeightToMaxHeight();
- }
- }
-
- /**
- * Phase 2: Bounce down.
- */
- private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
- ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
- animator.setDuration(450);
- animator.setInterpolator(mBounceInterpolator);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- setAnimator(null);
- onAnimationFinished.run();
- updatePanelExpansionAndVisibility();
- }
- });
- animator.start();
- setAnimator(animator);
- }
-
- private ValueAnimator createHeightAnimator(float targetHeight) {
- return createHeightAnimator(targetHeight, 0.0f /* performOvershoot */);
- }
-
- /**
- * Create an animator that can also overshoot
- *
- * @param targetHeight the target height
- * @param overshootAmount the amount of overshoot desired
- */
- private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) {
- float startExpansion = mOverExpansion;
- ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
- animator.addUpdateListener(
- animation -> {
- if (overshootAmount > 0.0f
- // Also remove the overExpansion when collapsing
- || (targetHeight == 0.0f && startExpansion != 0)) {
- final float expansion = MathUtils.lerp(
- startExpansion,
- mPanelFlingOvershootAmount * overshootAmount,
- Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
- animator.getAnimatedFraction()));
- setOverExpansionInternal(expansion, false /* isFromGesture */);
- }
- setExpandedHeightInternal((float) animation.getAnimatedValue());
- });
- return animator;
- }
-
- /** Update the visibility of {@link NotificationPanelView} if necessary. */
- public void updateVisibility() {
- mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE);
- }
-
- /** Returns true if {@link NotificationPanelView} should be visible. */
- abstract protected boolean shouldPanelBeVisible();
-
- /**
- * Updates the panel expansion and {@link NotificationPanelView} visibility if necessary.
- *
- * TODO(b/200063118): Could public calls to this method be replaced with calls to
- * {@link #updateVisibility()}? That would allow us to make this method private.
- */
- public void updatePanelExpansionAndVisibility() {
- mPanelExpansionStateManager.onPanelExpansionChanged(
- mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
- updateVisibility();
- }
-
- public boolean isExpanded() {
- return mExpandedFraction > 0f
- || mInstantExpanding
- || isPanelVisibleBecauseOfHeadsUp()
- || mTracking
- || mHeightAnimator != null
- && !mIsSpringBackAnimation;
- }
-
- protected abstract boolean isPanelVisibleBecauseOfHeadsUp();
-
- /**
- * Gets called when the user performs a click anywhere in the empty area of the panel.
- *
- * @return whether the panel will be expanded after the action performed by this method
- */
- protected boolean onEmptySpaceClick() {
- if (mHintAnimationRunning) {
- return true;
- }
- return onMiddleClicked();
- }
-
- protected abstract boolean onMiddleClicked();
-
- protected abstract boolean isDozing();
-
- public void dump(PrintWriter pw, String[] args) {
- pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
- + " tracking=%s timeAnim=%s%s "
- + "touchDisabled=%s" + "]",
- this.getClass().getSimpleName(), getExpandedHeight(), getMaxPanelHeight(),
- mClosing ? "T" : "f", mTracking ? "T" : "f", mHeightAnimator,
- ((mHeightAnimator != null && mHeightAnimator.isStarted()) ? " (started)" : ""),
- mTouchDisabled ? "T" : "f"));
- }
-
- public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
- mHeadsUpManager = headsUpManager;
- }
-
- public void setIsLaunchAnimationRunning(boolean running) {
- mIsLaunchAnimationRunning = running;
- }
-
- protected void setIsClosing(boolean isClosing) {
- mClosing = isClosing;
- }
-
- protected boolean isClosing() {
- return mClosing;
- }
-
- public void collapseWithDuration(int animationDuration) {
- mFixedDuration = animationDuration;
- collapse(false /* delayed */, 1.0f /* speedUpFactor */);
- mFixedDuration = NO_FIXED_DURATION;
- }
-
- public ViewGroup getView() {
- // TODO: remove this method, or at least reduce references to it.
- return mView;
- }
-
- protected abstract OnLayoutChangeListener createLayoutChangeListener();
-
- protected abstract TouchHandler createTouchHandler();
-
- protected OnConfigurationChangedListener createOnConfigurationChangedListener() {
- return new OnConfigurationChangedListener();
- }
-
- public class TouchHandler implements View.OnTouchListener {
-
- public boolean onInterceptTouchEvent(MotionEvent event) {
- if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted
- && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
- return false;
- }
-
- /*
- * If the user drags anywhere inside the panel we intercept it if the movement is
- * upwards. This allows closing the shade from anywhere inside the panel.
- *
- * We only do this if the current content is scrolled to the bottom,
- * i.e canCollapsePanelOnTouch() is true and therefore there is no conflicting scrolling
- * gesture
- * possible.
- */
- int pointerIndex = event.findPointerIndex(mTrackingPointer);
- if (pointerIndex < 0) {
- pointerIndex = 0;
- mTrackingPointer = event.getPointerId(pointerIndex);
- }
- final float x = event.getX(pointerIndex);
- final float y = event.getY(pointerIndex);
- boolean canCollapsePanel = canCollapsePanelOnTouch();
-
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- mCentralSurfaces.userActivity();
- mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
- mMinExpandHeight = 0.0f;
- mDownTime = mSystemClock.uptimeMillis();
- if (mAnimatingOnDown && mClosing && !mHintAnimationRunning) {
- cancelHeightAnimator();
- mTouchSlopExceeded = true;
- return true;
- }
- mInitialExpandY = y;
- mInitialExpandX = x;
- mTouchStartedInEmptyArea = !isInContentBounds(x, y);
- mTouchSlopExceeded = mTouchSlopExceededBeforeDown;
- mMotionAborted = false;
- mPanelClosedOnDown = isFullyCollapsed();
- mCollapsedAndHeadsUpOnDown = false;
- mHasLayoutedSinceDown = false;
- mUpdateFlingOnLayout = false;
- mTouchAboveFalsingThreshold = false;
- addMovement(event);
- break;
- case MotionEvent.ACTION_POINTER_UP:
- final int upPointer = event.getPointerId(event.getActionIndex());
- if (mTrackingPointer == upPointer) {
- // gesture is ongoing, find a new pointer to track
- final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
- mTrackingPointer = event.getPointerId(newIndex);
- mInitialExpandX = event.getX(newIndex);
- mInitialExpandY = event.getY(newIndex);
- }
- break;
- case MotionEvent.ACTION_POINTER_DOWN:
- if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
- mMotionAborted = true;
- mVelocityTracker.clear();
- }
- break;
- case MotionEvent.ACTION_MOVE:
- final float h = y - mInitialExpandY;
- addMovement(event);
- final boolean openShadeWithoutHun =
- mPanelClosedOnDown && !mCollapsedAndHeadsUpOnDown;
- if (canCollapsePanel || mTouchStartedInEmptyArea || mAnimatingOnDown
- || openShadeWithoutHun) {
- float hAbs = Math.abs(h);
- float touchSlop = getTouchSlop(event);
- if ((h < -touchSlop
- || ((openShadeWithoutHun || mAnimatingOnDown) && hAbs > touchSlop))
- && hAbs > Math.abs(x - mInitialExpandX)) {
- cancelHeightAnimator();
- startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
- return true;
- }
- }
- break;
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- mVelocityTracker.clear();
- break;
- }
- return false;
- }
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- if (mInstantExpanding) {
- mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding");
- return false;
- }
- if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) {
- mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled");
- return false;
- }
- if (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
- mShadeLog.logMotionEvent(event, "onTouch: non-down action, motion was aborted");
- return false;
- }
-
- // If dragging should not expand the notifications shade, then return false.
- if (!mNotificationsDragEnabled) {
- if (mTracking) {
- // Turn off tracking if it's on or the shade can get stuck in the down position.
- onTrackingStopped(true /* expand */);
- }
- mShadeLog.logMotionEvent(event, "onTouch: drag not enabled");
- return false;
- }
-
- // On expanding, single mouse click expands the panel instead of dragging.
- if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
- if (event.getAction() == MotionEvent.ACTION_UP) {
- expand(true);
- }
- return true;
- }
-
- /*
- * We capture touch events here and update the expand height here in case according to
- * the users fingers. This also handles multi-touch.
- *
- * Flinging is also enabled in order to open or close the shade.
- */
-
- int pointerIndex = event.findPointerIndex(mTrackingPointer);
- if (pointerIndex < 0) {
- pointerIndex = 0;
- mTrackingPointer = event.getPointerId(pointerIndex);
- }
- final float x = event.getX(pointerIndex);
- final float y = event.getY(pointerIndex);
-
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
- mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop();
- mIgnoreXTouchSlop = true;
- }
-
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
- mMinExpandHeight = 0.0f;
- mPanelClosedOnDown = isFullyCollapsed();
- mHasLayoutedSinceDown = false;
- mUpdateFlingOnLayout = false;
- mMotionAborted = false;
- mDownTime = mSystemClock.uptimeMillis();
- mTouchAboveFalsingThreshold = false;
- mCollapsedAndHeadsUpOnDown =
- isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp();
- addMovement(event);
- boolean regularHeightAnimationRunning = mHeightAnimator != null
- && !mHintAnimationRunning && !mIsSpringBackAnimation;
- if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) {
- mTouchSlopExceeded = regularHeightAnimationRunning
- || mTouchSlopExceededBeforeDown;
- cancelHeightAnimator();
- onTrackingStarted();
- }
- if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()
- && !mCentralSurfaces.isBouncerShowing()) {
- startOpening(event);
- }
- break;
-
- case MotionEvent.ACTION_POINTER_UP:
- final int upPointer = event.getPointerId(event.getActionIndex());
- if (mTrackingPointer == upPointer) {
- // gesture is ongoing, find a new pointer to track
- final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
- final float newY = event.getY(newIndex);
- final float newX = event.getX(newIndex);
- mTrackingPointer = event.getPointerId(newIndex);
- mHandlingPointerUp = true;
- startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
- mHandlingPointerUp = false;
- }
- break;
- case MotionEvent.ACTION_POINTER_DOWN:
- if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
- mMotionAborted = true;
- endMotionEvent(event, x, y, true /* forceCancel */);
- return false;
- }
- break;
- case MotionEvent.ACTION_MOVE:
- addMovement(event);
- if (!isFullyCollapsed()) {
- maybeVibrateOnOpening(true /* openingWithTouch */);
- }
- float h = y - mInitialExpandY;
-
- // If the panel was collapsed when touching, we only need to check for the
- // y-component of the gesture, as we have no conflicting horizontal gesture.
- if (Math.abs(h) > getTouchSlop(event)
- && (Math.abs(h) > Math.abs(x - mInitialExpandX)
- || mIgnoreXTouchSlop)) {
- mTouchSlopExceeded = true;
- if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
- if (mInitialOffsetOnTouch != 0f) {
- startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
- h = 0;
- }
- cancelHeightAnimator();
- onTrackingStarted();
- }
- }
- float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
- newHeight = Math.max(newHeight, mMinExpandHeight);
- if (-h >= getFalsingThreshold()) {
- mTouchAboveFalsingThreshold = true;
- mUpwardsWhenThresholdReached = isDirectionUpwards(x, y);
- }
- if ((!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) {
- // Count h==0 as part of swipe-up,
- // otherwise {@link NotificationStackScrollLayout}
- // wrongly enables stack height updates at the start of lockscreen swipe-up
- mAmbientState.setSwipingUp(h <= 0);
- setExpandedHeightInternal(newHeight);
- }
- break;
-
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- addMovement(event);
- endMotionEvent(event, x, y, false /* forceCancel */);
- // mHeightAnimator is null, there is no remaining frame, ends instrumenting.
- if (mHeightAnimator == null) {
- if (event.getActionMasked() == MotionEvent.ACTION_UP) {
- endJankMonitoring();
- } else {
- cancelJankMonitoring();
- }
- }
- break;
- }
- return !mGestureWaitForTouchSlop || mTracking;
- }
- }
-
- protected abstract class OnLayoutChangeListener implements View.OnLayoutChangeListener {
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
- int oldTop, int oldRight, int oldBottom) {
- updateExpandedHeightToMaxHeight();
- mHasLayoutedSinceDown = true;
- if (mUpdateFlingOnLayout) {
- abortAnimations();
- fling(mUpdateFlingVelocity, true /* expands */);
- mUpdateFlingOnLayout = false;
- }
- }
- }
-
- public class OnConfigurationChangedListener implements
- NotificationPanelView.OnConfigurationChangedListener {
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- loadDimens();
- }
- }
-
- private void beginJankMonitoring() {
- if (mInteractionJankMonitor == null) {
- return;
- }
- InteractionJankMonitor.Configuration.Builder builder =
- InteractionJankMonitor.Configuration.Builder.withView(
- InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
- mView)
- .setTag(isFullyCollapsed() ? "Expand" : "Collapse");
- mInteractionJankMonitor.begin(builder);
- }
-
- private void endJankMonitoring() {
- if (mInteractionJankMonitor == null) {
- return;
- }
- InteractionJankMonitor.getInstance().end(
- InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
- }
-
- private void cancelJankMonitoring() {
- if (mInteractionJankMonitor == null) {
- return;
- }
- InteractionJankMonitor.getInstance().cancel(
- InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
- }
-
- protected float getExpansionFraction() {
- return mExpandedFraction;
- }
-
- protected PanelExpansionStateManager getPanelExpansionStateManager() {
- return mPanelExpansionStateManager;
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionChangeEvent.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionChangeEvent.kt
similarity index 91%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionChangeEvent.kt
rename to packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionChangeEvent.kt
index 7c61b29..71dfafa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionChangeEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionChangeEvent.kt
@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone.panelstate
+package com.android.systemui.shade
import android.annotation.FloatRange
-data class PanelExpansionChangeEvent(
+data class ShadeExpansionChangeEvent(
/** 0 when collapsed, 1 when fully expanded. */
@FloatRange(from = 0.0, to = 1.0) val fraction: Float,
/** Whether the panel should be considered expanded */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionListener.kt
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.kt
rename to packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionListener.kt
index d003824..a5a9ffd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionListener.kt
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone.panelstate
+package com.android.systemui.shade
/** A listener interface to be notified of expansion events for the notification panel. */
-fun interface PanelExpansionListener {
+fun interface ShadeExpansionListener {
/**
* Invoked whenever the notification panel expansion changes, at every animation frame. This is
* the main expansion that happens when the user is swiping up to dismiss the lock screen and
* swiping to pull down the notification shade.
*/
- fun onPanelExpansionChanged(event: PanelExpansionChangeEvent)
+ fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
similarity index 70%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
rename to packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
index 6b7c42e..7bba74a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -14,13 +14,14 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone.panelstate
+package com.android.systemui.shade
import android.annotation.IntDef
import android.util.Log
import androidx.annotation.FloatRange
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.util.Compile
+import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
/**
@@ -29,14 +30,16 @@
* TODO(b/200063118): Make this class the one source of truth for the state of panel expansion.
*/
@SysUISingleton
-class PanelExpansionStateManager @Inject constructor() {
+class ShadeExpansionStateManager @Inject constructor() {
- private val expansionListeners = mutableListOf<PanelExpansionListener>()
- private val stateListeners = mutableListOf<PanelStateListener>()
+ 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
@@ -45,24 +48,34 @@
*
* Listener will also be immediately notified with the current values.
*/
- fun addExpansionListener(listener: PanelExpansionListener) {
+ fun addExpansionListener(listener: ShadeExpansionListener) {
expansionListeners.add(listener)
listener.onPanelExpansionChanged(
- PanelExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount))
+ ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount)
+ )
}
/** Removes an expansion listener. */
- fun removeExpansionListener(listener: PanelExpansionListener) {
+ fun removeExpansionListener(listener: ShadeExpansionListener) {
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: PanelStateListener) {
+ fun addStateListener(listener: ShadeStateListener) {
stateListeners.add(listener)
}
/** Removes a state listener. */
- fun removeStateListener(listener: PanelStateListener) {
+ fun removeStateListener(listener: ShadeStateListener) {
stateListeners.remove(listener)
}
@@ -110,25 +123,34 @@
debugLog(
"panelExpansionChanged:" +
- "start state=${oldState.panelStateToString()} " +
- "end state=${state.panelStateToString()} " +
- "f=$fraction " +
- "expanded=$expanded " +
- "tracking=$tracking " +
- "dragDownPxAmount=$dragDownPxAmount " +
- "${if (fullyOpened) " fullyOpened" else ""} " +
- if (fullyClosed) " fullyClosed" else ""
+ "start state=${oldState.panelStateToString()} " +
+ "end state=${state.panelStateToString()} " +
+ "f=$fraction " +
+ "expanded=$expanded " +
+ "tracking=$tracking " +
+ "dragDownPxAmount=$dragDownPxAmount " +
+ "${if (fullyOpened) " fullyOpened" else ""} " +
+ if (fullyClosed) " fullyClosed" else ""
)
val expansionChangeEvent =
- PanelExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount)
+ ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount)
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(
- "update state: ${this.state.panelStateToString()} -> ${state.panelStateToString()}")
+ "update state: ${this.state.panelStateToString()} -> ${state.panelStateToString()}"
+ )
if (this.state != state) {
updateStateInternal(state)
}
@@ -165,5 +187,5 @@
}
}
-private val TAG = PanelExpansionStateManager::class.simpleName
+private val TAG = ShadeExpansionStateManager::class.simpleName
private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index f1e44ce..2b788d8 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -1,20 +1,17 @@
package com.android.systemui.shade
import android.view.MotionEvent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogMessage
import com.android.systemui.log.dagger.ShadeLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogMessage
import com.google.errorprone.annotations.CompileTimeConstant
import javax.inject.Inject
private const val TAG = "systemui.shade"
/** Lightweight logging utility for the Shade. */
-class ShadeLogger @Inject constructor(
- @ShadeLog
- private val buffer: LogBuffer
-) {
+class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) {
fun v(@CompileTimeConstant msg: String) {
buffer.log(TAG, LogLevel.VERBOSE, msg)
}
@@ -28,21 +25,56 @@
}
fun onQsInterceptMoveQsTrackingEnabled(h: Float) {
- log(LogLevel.VERBOSE,
+ log(
+ LogLevel.VERBOSE,
{ double1 = h.toDouble() },
- { "onQsIn[tercept: move action, QS tracking enabled. h = $double1" })
+ { "onQsIntercept: move action, QS tracking enabled. h = $double1" }
+ )
+ }
+
+ fun logQsTrackingNotStarted(
+ initialTouchY: Float,
+ y: Float,
+ h: Float,
+ touchSlop: Float,
+ qsExpanded: Boolean,
+ collapsedOnDown: Boolean,
+ keyguardShowing: Boolean,
+ qsExpansionEnabled: Boolean
+ ) {
+ log(
+ LogLevel.VERBOSE,
+ {
+ int1 = initialTouchY.toInt()
+ int2 = y.toInt()
+ long1 = h.toLong()
+ double1 = touchSlop.toDouble()
+ bool1 = qsExpanded
+ bool2 = collapsedOnDown
+ bool3 = keyguardShowing
+ bool4 = qsExpansionEnabled
+ },
+ {
+ "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded" +
+ "=$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4"
+ }
+ )
}
fun logMotionEvent(event: MotionEvent, message: String) {
- log(LogLevel.VERBOSE, {
- str1 = message
- long1 = event.eventTime
- long2 = event.downTime
- int1 = event.action
- int2 = event.classification
- double1 = event.y.toDouble()
- }, {
- "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,classification=$int2"
- })
+ log(
+ LogLevel.VERBOSE,
+ {
+ str1 = message
+ long1 = event.eventTime
+ long2 = event.downTime
+ int1 = event.action
+ int2 = event.classification
+ double1 = event.y.toDouble()
+ },
+ {
+ "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,class=$int2"
+ }
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt
new file mode 100644
index 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/phone/panelstate/PanelStateListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateListener.kt
similarity index 82%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
rename to packages/SystemUI/src/com/android/systemui/shade/ShadeStateListener.kt
index ca667dd..74468a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateListener.kt
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone.panelstate
+package com.android.systemui.shade
/** A listener interface to be notified of state change events for the notification panel. */
-fun interface PanelStateListener {
- /** Called when the panel's expansion state has changed. */
+fun interface ShadeStateListener {
+ /** Called when the panel's expansion state has changed. */
fun onPanelStateChanged(@PanelState state: Int)
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
new file mode 100644
index 0000000..09019a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.shade.data.repository
+
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionListener
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.domain.model.ShadeModel
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+/** Business logic for shade interactions */
+@SysUISingleton
+class ShadeRepository @Inject constructor(shadeExpansionStateManager: ShadeExpansionStateManager) {
+
+ val shadeModel: Flow<ShadeModel> =
+ conflatedCallbackFlow {
+ val callback =
+ object : ShadeExpansionListener {
+ override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
+ // Don't propagate ShadeExpansionChangeEvent.dragDownPxAmount field.
+ // It is too noisy and produces extra events that consumers won't care
+ // about
+ val info =
+ ShadeModel(
+ expansionAmount = event.fraction,
+ isExpanded = event.expanded,
+ isUserDragging = event.tracking
+ )
+ trySendWithFailureLogging(info, TAG, "updated shade expansion info")
+ }
+ }
+
+ shadeExpansionStateManager.addExpansionListener(callback)
+ trySendWithFailureLogging(ShadeModel(), TAG, "initial shade expansion info")
+
+ awaitClose { shadeExpansionStateManager.removeExpansionListener(callback) }
+ }
+ .distinctUntilChanged()
+
+ companion object {
+ private const val TAG = "ShadeRepository"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionChangeEvent.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionChangeEvent.kt
copy to packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt
index 7c61b29..ce0f4283 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionChangeEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -11,19 +11,18 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.statusbar.phone.panelstate
+package com.android.systemui.shade.domain.model
import android.annotation.FloatRange
-data class PanelExpansionChangeEvent(
+/** Information about shade (NotificationPanel) expansion */
+data class ShadeModel(
/** 0 when collapsed, 1 when fully expanded. */
- @FloatRange(from = 0.0, to = 1.0) val fraction: Float,
+ @FloatRange(from = 0.0, to = 1.0) val expansionAmount: Float = 0f,
/** Whether the panel should be considered expanded */
- val expanded: Boolean,
+ val isExpanded: Boolean = false,
/** Whether the user is actively dragging the panel. */
- val tracking: Boolean,
- /** The amount of pixels that the user has dragged during the expansion. */
- val dragDownPxAmount: Float
+ val isUserDragging: Boolean = false,
)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
index 618c892..a77c21a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
@@ -7,12 +7,12 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.shade.PanelState
+import com.android.systemui.shade.STATE_OPENING
+import com.android.systemui.shade.ShadeExpansionChangeEvent
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
-import com.android.systemui.statusbar.phone.panelstate.PanelState
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.util.LargeScreenUtils
@@ -35,7 +35,7 @@
private var inSplitShade = false
private var splitShadeScrimTransitionDistance = 0
private var lastExpansionFraction: Float? = null
- private var lastExpansionEvent: PanelExpansionChangeEvent? = null
+ private var lastExpansionEvent: ShadeExpansionChangeEvent? = null
private var currentPanelState: Int? = null
init {
@@ -61,8 +61,8 @@
onStateChanged()
}
- fun onPanelExpansionChanged(panelExpansionChangeEvent: PanelExpansionChangeEvent) {
- lastExpansionEvent = panelExpansionChangeEvent
+ fun onPanelExpansionChanged(shadeExpansionChangeEvent: ShadeExpansionChangeEvent) {
+ lastExpansionEvent = shadeExpansionChangeEvent
onStateChanged()
}
@@ -75,7 +75,7 @@
}
private fun calculateScrimExpansionFraction(
- expansionEvent: PanelExpansionChangeEvent,
+ expansionEvent: ShadeExpansionChangeEvent,
@PanelState panelState: Int?
): Float {
return if (canUseCustomFraction(panelState)) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt
index 6c3a028..22e847d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt
@@ -1,6 +1,6 @@
package com.android.systemui.shade.transition
-import com.android.systemui.statusbar.phone.panelstate.PanelState
+import com.android.systemui.shade.PanelState
/** Represents an over scroller for the non-lockscreen shade. */
interface ShadeOverScroller {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
index 58acfb4..1e8208f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
@@ -7,13 +7,13 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.qs.QS
import com.android.systemui.shade.NotificationPanelViewController
+import com.android.systemui.shade.PanelState
+import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.panelStateToString
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
-import com.android.systemui.statusbar.phone.panelstate.PanelState
-import com.android.systemui.statusbar.phone.panelstate.panelStateToString
import com.android.systemui.statusbar.policy.ConfigurationController
import java.io.PrintWriter
import javax.inject.Inject
@@ -24,7 +24,7 @@
@Inject
constructor(
configurationController: ConfigurationController,
- panelExpansionStateManager: PanelExpansionStateManager,
+ shadeExpansionStateManager: ShadeExpansionStateManager,
dumpManager: DumpManager,
private val context: Context,
private val splitShadeOverScrollerFactory: SplitShadeOverScroller.Factory,
@@ -39,7 +39,7 @@
private var inSplitShade = false
private var currentPanelState: Int? = null
- private var lastPanelExpansionChangeEvent: PanelExpansionChangeEvent? = null
+ private var lastShadeExpansionChangeEvent: ShadeExpansionChangeEvent? = null
private val splitShadeOverScroller by lazy {
splitShadeOverScrollerFactory.create({ qs }, { notificationStackScrollLayoutController })
@@ -60,8 +60,8 @@
updateResources()
}
})
- panelExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged)
- panelExpansionStateManager.addStateListener(this::onPanelStateChanged)
+ shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged)
+ shadeExpansionStateManager.addStateListener(this::onPanelStateChanged)
dumpManager.registerDumpable("ShadeTransitionController") { printWriter, _ ->
dump(printWriter)
}
@@ -77,8 +77,8 @@
scrimShadeTransitionController.onPanelStateChanged(state)
}
- private fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) {
- lastPanelExpansionChangeEvent = event
+ private fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
+ lastShadeExpansionChangeEvent = event
shadeOverScroller.onDragDownAmountChanged(event.dragDownPxAmount)
scrimShadeTransitionController.onPanelExpansionChanged(event)
}
@@ -95,7 +95,7 @@
inSplitShade: $inSplitShade
isScreenUnlocked: ${isScreenUnlocked()}
currentPanelState: ${currentPanelState?.panelStateToString()}
- lastPanelExpansionChangeEvent: $lastPanelExpansionChangeEvent
+ lastPanelExpansionChangeEvent: $lastShadeExpansionChangeEvent
qs.isInitialized: ${this::qs.isInitialized}
npvc.isInitialized: ${this::notificationPanelViewController.isInitialized}
nssl.isInitialized: ${this::notificationStackScrollLayoutController.isInitialized}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt
index 204dd3c..8c57194 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt
@@ -10,11 +10,11 @@
import com.android.systemui.animation.Interpolators
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.qs.QS
+import com.android.systemui.shade.PanelState
+import com.android.systemui.shade.STATE_CLOSED
+import com.android.systemui.shade.STATE_OPENING
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.phone.panelstate.PanelState
-import com.android.systemui.statusbar.phone.panelstate.STATE_CLOSED
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING
import com.android.systemui.statusbar.policy.ConfigurationController
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -37,6 +37,7 @@
private var previousOverscrollAmount = 0
private var dragDownAmount: Float = 0f
@PanelState private var panelState: Int = STATE_CLOSED
+
private var releaseOverScrollAnimator: Animator? = null
private val qS: QS
diff --git a/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java b/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java
index 6abf339..ff26766 100644
--- a/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java
+++ b/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java
@@ -32,10 +32,10 @@
* Dispatches shortcut to System UI components
*/
@SysUISingleton
-public class ShortcutKeyDispatcher extends CoreStartable
- implements ShortcutKeyServiceProxy.Callbacks {
+public class ShortcutKeyDispatcher implements CoreStartable, ShortcutKeyServiceProxy.Callbacks {
private static final String TAG = "ShortcutKeyDispatcher";
+ private final Context mContext;
private ShortcutKeyServiceProxy mShortcutKeyServiceProxy = new ShortcutKeyServiceProxy(this);
private IWindowManager mWindowManagerService = WindowManagerGlobal.getWindowManagerService();
@@ -50,7 +50,7 @@
@Inject
public ShortcutKeyDispatcher(Context context) {
- super(context);
+ mContext = context;
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
index 7f7ff9cf..90c52bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
@@ -17,9 +17,9 @@
package com.android.systemui.statusbar
import android.app.PendingIntent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 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/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index c290ce2..184dc25 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -189,7 +189,6 @@
protected NotificationPresenter mPresenter;
protected ContentObserver mLockscreenSettingsObserver;
protected ContentObserver mSettingsObserver;
- private boolean mHideSilentNotificationsOnLockscreen;
@Inject
public NotificationLockscreenUserManagerImpl(Context context,
@@ -266,12 +265,6 @@
UserHandle.USER_ALL);
mContext.getContentResolver().registerContentObserver(
- mSecureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS),
- true,
- mLockscreenSettingsObserver,
- UserHandle.USER_ALL);
-
- mContext.getContentResolver().registerContentObserver(
Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false,
mSettingsObserver);
@@ -340,9 +333,6 @@
final boolean allowedByDpm = (dpmFlags
& DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS) == 0;
- mHideSilentNotificationsOnLockscreen = mSecureSettings.getIntForUser(
- Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 1, mCurrentUserId) == 0;
-
setShowLockscreenNotifications(show && allowedByDpm);
if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) {
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/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index cb13fcf..b5879ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -38,12 +38,12 @@
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
import com.android.systemui.statusbar.phone.BiometricUnlockController
import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.LargeScreenUtils
@@ -69,7 +69,7 @@
private val context: Context,
dumpManager: DumpManager,
configurationController: ConfigurationController
-) : PanelExpansionListener, Dumpable {
+) : ShadeExpansionListener, Dumpable {
companion object {
private const val WAKE_UP_ANIMATION_ENABLED = true
private const val VELOCITY_SCALE = 100f
@@ -338,7 +338,7 @@
/**
* Update blurs when pulling down the shade
*/
- override fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) {
+ override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
val rawFraction = event.fraction
val tracking = event.tracking
val timestamp = SystemClock.elapsedRealtimeNanos()
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 bbff046..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
@@ -52,7 +53,10 @@
import kotlin.math.max
/**
- * A utility class to enable the downward swipe on when pulsing.
+ * A utility class that handles notification panel expansion when a user swipes downward on a
+ * notification from the pulsing state.
+ * If face-bypass is enabled, the user can swipe down anywhere on the screen (not just from a
+ * notification) to trigger the notification panel expansion.
*/
@SysUISingleton
class PulseExpansionHandler @Inject
@@ -62,9 +66,10 @@
private val bypassController: KeyguardBypassController,
private val headsUpManager: HeadsUpManagerPhone,
private val roundnessManager: NotificationRoundnessManager,
- private val configurationController: ConfigurationController,
+ configurationController: ConfigurationController,
private val statusBarStateController: StatusBarStateController,
private val falsingManager: FalsingManager,
+ shadeExpansionStateManager: ShadeExpansionStateManager,
private val lockscreenShadeTransitionController: LockscreenShadeTransitionController,
private val falsingCollector: FalsingCollector,
dumpManager: DumpManager
@@ -123,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/QsFrameTranslateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java
index 7807738..59afb18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java
@@ -36,8 +36,7 @@
/**
* Calculate and translate the QS Frame on the Y-axis.
*/
- public abstract void translateQsFrame(View qsFrame, QS qs, float overExpansion,
- float qsTranslationForFullShadeTransition);
+ public abstract void translateQsFrame(View qsFrame, QS qs, int bottomInset);
/**
* Calculate the top padding for notifications panel. This could be the supplied
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java
index 33e2245..85b522c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java
@@ -27,6 +27,8 @@
/**
* Default implementation of QS Translation. This by default does not do much.
+ * This class can be subclassed to allow System UI variants the flexibility to change position of
+ * the Quick Settings frame.
*/
@SysUISingleton
public class QsFrameTranslateImpl extends QsFrameTranslateController {
@@ -37,8 +39,8 @@
}
@Override
- public void translateQsFrame(View qsFrame, QS qs, float overExpansion,
- float qsTranslationForFullShadeTransition) {
+ public void translateQsFrame(View qsFrame, QS qs, int bottomInset) {
+ // Empty implementation by default, meant to be overridden by subclasses.
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index b3dd853..402217d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -71,9 +71,9 @@
import com.android.systemui.demomode.DemoMode;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogLevel;
import com.android.systemui.log.dagger.StatusBarNetworkControllerLog;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogLevel;
import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
import com.android.systemui.settings.CurrentUserTracker;
import com.android.systemui.statusbar.policy.ConfigurationController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 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/gesture/SwipeStatusBarAwayGestureLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
index 17feaa8..9bdff92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.gesture
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.SwipeStatusBarAwayLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import javax.inject.Inject
/** Log messages for [SwipeStatusBarAwayGestureHandler]. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 8f8813b80..842204b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -118,6 +118,7 @@
regionSamplingEnabled,
updateFun
)
+ initializeTextColors(regionSamplingInstance)
regionSamplingInstance.startRegionSampler()
regionSamplingInstances.put(v, regionSamplingInstance)
connectSession()
@@ -361,18 +362,20 @@
}
}
+ private fun initializeTextColors(regionSamplingInstance: RegionSamplingInstance) {
+ val lightThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_LightWallpaper)
+ val darkColor = Utils.getColorAttrDefaultColor(lightThemeContext, R.attr.wallpaperTextColor)
+
+ val darkThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI)
+ val lightColor = Utils.getColorAttrDefaultColor(darkThemeContext, R.attr.wallpaperTextColor)
+
+ regionSamplingInstance.setForegroundColors(lightColor, darkColor)
+ }
+
private fun updateTextColorFromRegionSampler() {
smartspaceViews.forEach {
- val isRegionDark = regionSamplingInstances.getValue(it).currentRegionDarkness()
- val themeID = if (isRegionDark.isDark) {
- R.style.Theme_SystemUI
- } else {
- R.style.Theme_SystemUI_LightWallpaper
- }
- val themedContext = ContextThemeWrapper(context, themeID)
- val wallpaperTextColor =
- Utils.getColorAttrDefaultColor(themedContext, R.attr.wallpaperTextColor)
- it.setPrimaryTextColor(wallpaperTextColor)
+ val textColor = regionSamplingInstances.getValue(it).currentForegroundColor()
+ it.setPrimaryTextColor(textColor)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
index 1be4c04..b5c7ef5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.notification;
-import android.annotation.Nullable;
import android.util.ArraySet;
import androidx.annotation.VisibleForTesting;
@@ -25,7 +24,6 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import javax.inject.Inject;
@@ -43,7 +41,6 @@
private boolean mLastDynamicUnlocked;
private boolean mCacheInvalid;
- @Nullable private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@Inject
DynamicPrivacyController(NotificationLockscreenUserManager notificationLockscreenUserManager,
@@ -100,7 +97,7 @@
* contents aren't revealed yet?
*/
public boolean isInLockedDownShade() {
- if (!isStatusBarKeyguardShowing() || !mKeyguardStateController.isMethodSecure()) {
+ if (!mKeyguardStateController.isShowing() || !mKeyguardStateController.isMethodSecure()) {
return false;
}
int state = mStateController.getState();
@@ -113,16 +110,7 @@
return true;
}
- private boolean isStatusBarKeyguardShowing() {
- return mStatusBarKeyguardViewManager != null && mStatusBarKeyguardViewManager.isShowing();
- }
-
- public void setStatusBarKeyguardViewManager(
- StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
- mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
- }
-
public interface Listener {
void onDynamicPrivacyChanged();
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
index 59022c0f..0a5e986 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
@@ -66,11 +66,12 @@
* splitted screen.
*/
@SysUISingleton
-public class InstantAppNotifier extends CoreStartable
- implements CommandQueue.Callbacks, KeyguardStateController.Callback {
+public class InstantAppNotifier
+ implements CoreStartable, CommandQueue.Callbacks, KeyguardStateController.Callback {
private static final String TAG = "InstantAppNotifier";
public static final int NUM_TASKS_FOR_INSTANT_APP_INFO = 5;
+ private final Context mContext;
private final Handler mHandler = new Handler();
private final Executor mUiBgExecutor;
private final ArraySet<Pair<String, Integer>> mCurrentNotifs = new ArraySet<>();
@@ -83,7 +84,7 @@
CommandQueue commandQueue,
@UiBackground Executor uiBgExecutor,
KeyguardStateController keyguardStateController) {
- super(context);
+ mContext = context;
mCommandQueue = commandQueue;
mUiBgExecutor = uiBgExecutor;
mKeyguardStateController = keyguardStateController;
@@ -289,7 +290,7 @@
.setComponent(aiaComponent)
.setAction(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
- .addCategory("unique:" + System.currentTimeMillis())
+ .setIdentifier("unique:" + System.currentTimeMillis())
.putExtra(Intent.EXTRA_PACKAGE_NAME, appInfo.packageName)
.putExtra(
Intent.EXTRA_VERSION_CODE,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 7fbdd35..2734511 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -17,40 +17,27 @@
package com.android.systemui.statusbar.notification
import android.content.Context
-import android.util.Log
-import android.widget.Toast
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
-import com.android.systemui.util.Compile
import javax.inject.Inject
class NotifPipelineFlags @Inject constructor(
val context: Context,
val featureFlags: FeatureFlags
) {
- fun checkLegacyPipelineEnabled(): Boolean {
- if (Compile.IS_DEBUG) {
- Toast.makeText(context, "Old pipeline code running!", Toast.LENGTH_SHORT).show()
- }
- if (featureFlags.isEnabled(Flags.NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE)) {
- throw RuntimeException("Old pipeline code running with new pipeline enabled")
- } else {
- Log.d("NotifPipeline", "Old pipeline code running with new pipeline enabled",
- Exception())
- }
- return false
- }
-
fun isDevLoggingEnabled(): Boolean =
featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING)
- fun isSmartspaceDedupingEnabled(): Boolean =
- featureFlags.isEnabled(Flags.SMARTSPACE) &&
- featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING)
-
- fun removeUnrankedNotifs(): Boolean =
- featureFlags.isEnabled(Flags.REMOVE_UNRANKED_NOTIFICATIONS)
+ fun isSmartspaceDedupingEnabled(): Boolean = featureFlags.isEnabled(Flags.SMARTSPACE)
fun fullScreenIntentRequiresKeyguard(): Boolean =
featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD)
+
+ val isStabilityIndexFixEnabled: Boolean by lazy {
+ featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX)
+ }
+
+ val isSemiStableSortEnabled: Boolean by lazy {
+ featureFlags.isEnabled(Flags.SEMI_STABLE_SORT)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
index ad3dfed..3058fbb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.notification
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
index 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 126a986..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,9 +18,13 @@
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
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -28,21 +32,22 @@
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener
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, PanelExpansionListener {
+) : 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)) {
@@ -293,7 +301,7 @@
this.state = newState
}
- override fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) {
+ override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
val collapsedEnough = event.fraction <= 0.9f
if (collapsedEnough != this.collapsedEnoughToHide) {
val couldShowPulsingHuns = canShowPulsingHuns
@@ -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
@@ -426,4 +453,4 @@
*/
@JvmDefault fun onPulseExpansionChanged(expandingChanged: Boolean) {}
}
-}
\ No newline at end of file
+}
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/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
index f8449ae..84ab0d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
@@ -68,6 +68,9 @@
*/
var stableIndex: Int = -1
+ /** Access the index of the [section] or -1 if the entry does not have one */
+ val sectionIndex: Int get() = section?.index ?: -1
+
/** Copies the state of another instance. */
fun clone(other: ListAttachState) {
parent = other.parent
@@ -95,11 +98,13 @@
* This can happen if the entry is removed from a group that was broken up or if the entry was
* filtered out during any of the filtering steps.
*/
- fun detach() {
+ fun detach(includingStableIndex: Boolean) {
parent = null
section = null
promoter = null
- // stableIndex = -1 // TODO(b/241229236): Clear this once we fix the stability fragility
+ if (includingStableIndex) {
+ stableIndex = -1
+ }
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 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/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index e129ee4..3ae2545 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -54,6 +54,9 @@
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState;
+import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort;
+import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder;
+import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper;
import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator;
@@ -96,11 +99,14 @@
// used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated
// TODO replace temp with collection pool for readability
private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>();
+ private NotifPipelineFlags mFlags;
private final boolean mAlwaysLogList;
private List<ListEntry> mNotifList = new ArrayList<>();
private List<ListEntry> mNewNotifList = new ArrayList<>();
+ private final SemiStableSort mSemiStableSort = new SemiStableSort();
+ private final StableOrder<ListEntry> mStableOrder = this::getStableOrderRank;
private final PipelineState mPipelineState = new PipelineState();
private final Map<String, GroupEntry> mGroups = new ArrayMap<>();
private Collection<NotificationEntry> mAllEntries = Collections.emptyList();
@@ -141,6 +147,7 @@
) {
mSystemClock = systemClock;
mLogger = logger;
+ mFlags = flags;
mAlwaysLogList = flags.isDevLoggingEnabled();
mInteractionTracker = interactionTracker;
mChoreographer = pipelineChoreographer;
@@ -527,7 +534,7 @@
List<NotifFilter> filters) {
Trace.beginSection("ShadeListBuilder.filterNotifs");
final long now = mSystemClock.uptimeMillis();
- for (ListEntry entry : entries) {
+ for (ListEntry entry : entries) {
if (entry instanceof GroupEntry) {
final GroupEntry groupEntry = (GroupEntry) entry;
@@ -958,7 +965,8 @@
* filtered out during any of the filtering steps.
*/
private void annulAddition(ListEntry entry) {
- entry.getAttachState().detach();
+ // NOTE(b/241229236): Don't clear stableIndex until we fix stability fragility
+ entry.getAttachState().detach(/* includingStableIndex= */ mFlags.isSemiStableSortEnabled());
}
private void assignSections() {
@@ -978,7 +986,16 @@
private void sortListAndGroups() {
Trace.beginSection("ShadeListBuilder.sortListAndGroups");
- // Assign sections to top-level elements and sort their children
+ if (mFlags.isSemiStableSortEnabled()) {
+ sortWithSemiStableSort();
+ } else {
+ sortWithLegacyStability();
+ }
+ Trace.endSection();
+ }
+
+ private void sortWithLegacyStability() {
+ // Sort all groups and the top level list
for (ListEntry entry : mNotifList) {
if (entry instanceof GroupEntry) {
GroupEntry parent = (GroupEntry) entry;
@@ -991,16 +1008,15 @@
// Check for suppressed order changes
if (!getStabilityManager().isEveryChangeAllowed()) {
mForceReorderable = true;
- boolean isSorted = isShadeSorted();
+ boolean isSorted = isShadeSortedLegacy();
mForceReorderable = false;
if (!isSorted) {
getStabilityManager().onEntryReorderSuppressed();
}
}
- Trace.endSection();
}
- private boolean isShadeSorted() {
+ private boolean isShadeSortedLegacy() {
if (!isSorted(mNotifList, mTopLevelComparator)) {
return false;
}
@@ -1014,6 +1030,43 @@
return true;
}
+ private void sortWithSemiStableSort() {
+ // Sort each group's children
+ boolean allSorted = true;
+ for (ListEntry entry : mNotifList) {
+ if (entry instanceof GroupEntry) {
+ GroupEntry parent = (GroupEntry) entry;
+ allSorted &= sortGroupChildren(parent.getRawChildren());
+ }
+ }
+ // Sort each section within the top level list
+ mNotifList.sort(mTopLevelComparator);
+ if (!getStabilityManager().isEveryChangeAllowed()) {
+ for (List<ListEntry> subList : getSectionSubLists(mNotifList)) {
+ allSorted &= mSemiStableSort.stabilizeTo(subList, mStableOrder, mNewNotifList);
+ }
+ applyNewNotifList();
+ }
+ assignIndexes(mNotifList);
+ if (!allSorted) {
+ // Report suppressed order changes
+ getStabilityManager().onEntryReorderSuppressed();
+ }
+ }
+
+ private Iterable<List<ListEntry>> getSectionSubLists(List<ListEntry> entries) {
+ return ShadeListBuilderHelper.INSTANCE.getSectionSubLists(entries);
+ }
+
+ private boolean sortGroupChildren(List<NotificationEntry> entries) {
+ if (getStabilityManager().isEveryChangeAllowed()) {
+ entries.sort(mGroupChildrenComparator);
+ return true;
+ } else {
+ return mSemiStableSort.sort(entries, mStableOrder, mGroupChildrenComparator);
+ }
+ }
+
/** Determine whether the items in the list are sorted according to the comparator */
@VisibleForTesting
public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) {
@@ -1036,27 +1089,41 @@
/**
* Assign the index of each notification relative to the total order
*/
- private static void assignIndexes(List<ListEntry> notifList) {
+ private void assignIndexes(List<ListEntry> notifList) {
if (notifList.size() == 0) return;
NotifSection currentSection = requireNonNull(notifList.get(0).getSection());
int sectionMemberIndex = 0;
for (int i = 0; i < notifList.size(); i++) {
- ListEntry entry = notifList.get(i);
+ final ListEntry entry = notifList.get(i);
NotifSection section = requireNonNull(entry.getSection());
if (section.getIndex() != currentSection.getIndex()) {
sectionMemberIndex = 0;
currentSection = section;
}
- entry.getAttachState().setStableIndex(sectionMemberIndex);
- if (entry instanceof GroupEntry) {
- GroupEntry parent = (GroupEntry) entry;
- for (int j = 0; j < parent.getChildren().size(); j++) {
- entry = parent.getChildren().get(j);
- entry.getAttachState().setStableIndex(sectionMemberIndex);
- sectionMemberIndex++;
+ if (mFlags.isStabilityIndexFixEnabled()) {
+ entry.getAttachState().setStableIndex(sectionMemberIndex++);
+ if (entry instanceof GroupEntry) {
+ final GroupEntry parent = (GroupEntry) entry;
+ final NotificationEntry summary = parent.getSummary();
+ if (summary != null) {
+ summary.getAttachState().setStableIndex(sectionMemberIndex++);
+ }
+ for (NotificationEntry child : parent.getChildren()) {
+ child.getAttachState().setStableIndex(sectionMemberIndex++);
+ }
}
+ } else {
+ // This old implementation uses the same index number for the group as the first
+ // child, and fails to assign an index to the summary. Remove once tested.
+ entry.getAttachState().setStableIndex(sectionMemberIndex);
+ if (entry instanceof GroupEntry) {
+ final GroupEntry parent = (GroupEntry) entry;
+ for (NotificationEntry child : parent.getChildren()) {
+ child.getAttachState().setStableIndex(sectionMemberIndex++);
+ }
+ }
+ sectionMemberIndex++;
}
- sectionMemberIndex++;
}
}
@@ -1196,7 +1263,7 @@
o2.getSectionIndex());
if (cmp != 0) return cmp;
- cmp = Integer.compare(
+ cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
getStableOrderIndex(o1),
getStableOrderIndex(o2));
if (cmp != 0) return cmp;
@@ -1225,7 +1292,7 @@
private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> {
- int cmp = Integer.compare(
+ int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
getStableOrderIndex(o1),
getStableOrderIndex(o2));
if (cmp != 0) return cmp;
@@ -1256,9 +1323,25 @@
// let the stability manager constrain or allow reordering
return -1;
}
+ // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility
return entry.getPreviousAttachState().getStableIndex();
}
+ @Nullable
+ private Integer getStableOrderRank(ListEntry entry) {
+ if (getStabilityManager().isEntryReorderingAllowed(entry)) {
+ // let the stability manager constrain or allow reordering
+ return null;
+ }
+ if (entry.getAttachState().getSectionIndex()
+ != entry.getPreviousAttachState().getSectionIndex()) {
+ // stable index is only valid within the same section; otherwise we allow reordering
+ return null;
+ }
+ final int stableIndex = entry.getPreviousAttachState().getStableIndex();
+ return stableIndex == -1 ? null : stableIndex;
+ }
+
private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) {
final NotifFilter filter = findRejectingFilter(entry, now, filters);
entry.getAttachState().setExcludingFilter(filter);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
index 211e374..68d1319 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.notification.collection.coalescer
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import javax.inject.Inject
class GroupCoalescerLogger @Inject constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
index e8f352f..2919def 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
@@ -1,8 +1,8 @@
package com.android.systemui.statusbar.notification.collection.coordinator
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.statusbar.notification.row.NotificationGuts
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index ccf6fec..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)) {
@@ -440,6 +453,47 @@
override fun onEntryCleanUp(entry: NotificationEntry) {
mHeadsUpViewBinder.abortBindCallback(entry)
}
+
+ /**
+ * Identify notifications whose heads-up state changes when the notification rankings are
+ * updated, and have those changed notifications alert if necessary.
+ *
+ * This method will occur after any operations in onEntryAdded or onEntryUpdated, so any
+ * handling of ranking changes needs to take into account that we may have just made a
+ * PostedEntry for some of these notifications.
+ */
+ override fun onRankingApplied() {
+ // Because a ranking update may cause some notifications that are no longer (or were
+ // never) in mPostedEntries to need to alert, we need to check every notification
+ // known to the pipeline.
+ for (entry in mNotifPipeline.allNotifs) {
+ // 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.
+ if (entry.hasInterrupted()) continue
+
+ // The cases where we should consider this notification to be updated:
+ // - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp
+ // state
+ // - if it is present in PostedEntries and the previous state of shouldHeadsUp
+ // differs from the updated one
+ val shouldHeadsUpEver = mNotificationInterruptStateProvider.checkHeadsUp(entry,
+ /* log= */ false)
+ val postedShouldHeadsUpEver = mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false
+ val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver
+
+ if (shouldUpdateEntry) {
+ mLogger.logEntryUpdatedByRanking(entry.key, shouldHeadsUpEver)
+ onEntryUpdated(entry)
+ }
+ }
+ }
}
/**
@@ -450,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)) {
@@ -561,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/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index 204a494..dfaa291 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -1,9 +1,10 @@
package com.android.systemui.statusbar.notification.collection.coordinator
import android.util.Log
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+
import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import javax.inject.Inject
private const val TAG = "HeadsUpCoordinator"
@@ -59,4 +60,13 @@
" numPostedEntries=$int1 logicalGroupSize=$int2"
})
}
+
+ fun logEntryUpdatedByRanking(key: String, shouldHun: Boolean) {
+ buffer.log(TAG, LogLevel.DEBUG, {
+ str1 = key
+ bool1 = shouldHun
+ }, {
+ "updating entry via ranking applied: $str1 updated shouldHeadsUp=$bool1"
+ })
+ }
}
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/collection/coordinator/PreparationCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
index c4f4ed5..9558f47 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.notification.collection.coordinator
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
index c687e1b..d804454 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.notification.collection.coordinator
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import javax.inject.Inject
private const val TAG = "ShadeEventCoordinator"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java
index a7719d3..e71d80c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java
@@ -18,6 +18,8 @@
import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+
import android.os.SystemClock;
import android.service.notification.NotificationStats;
@@ -70,6 +72,8 @@
dismissalSurface = NotificationStats.DISMISSAL_PEEK;
} else if (mStatusBarStateController.isDozing()) {
dismissalSurface = NotificationStats.DISMISSAL_AOD;
+ } else if (mStatusBarStateController.getState() == KEYGUARD) {
+ dismissalSurface = NotificationStats.DISMISSAL_LOCKSCREEN;
}
return new DismissedByUserStats(
dismissalSurface,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt
new file mode 100644
index 0000000..9ec8e07
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import androidx.annotation.VisibleForTesting
+import kotlin.math.sign
+
+class SemiStableSort {
+ val preallocatedWorkspace by lazy { ArrayList<Any>() }
+ val preallocatedAdditions by lazy { ArrayList<Any>() }
+ val preallocatedMapToIndex by lazy { HashMap<Any, Int>() }
+ val preallocatedMapToIndexComparator: Comparator<Any> by lazy {
+ Comparator.comparingInt { item -> preallocatedMapToIndex[item] ?: -1 }
+ }
+
+ /**
+ * Sort the given [items] such that items which have a [stableOrder] will all be in that order,
+ * items without a [stableOrder] will be sorted according to the comparator, and the two sets of
+ * items will be combined to have the fewest elements out of order according to the [comparator]
+ * . The result will be placed into the original [items] list.
+ */
+ fun <T : Any> sort(
+ items: MutableList<T>,
+ stableOrder: StableOrder<in T>,
+ comparator: Comparator<in T>,
+ ): Boolean =
+ withWorkspace<T, Boolean> { workspace ->
+ val ordered =
+ sortTo(
+ items,
+ stableOrder,
+ comparator,
+ workspace,
+ )
+ items.clear()
+ items.addAll(workspace)
+ return ordered
+ }
+
+ /**
+ * Sort the given [items] such that items which have a [stableOrder] will all be in that order,
+ * items without a [stableOrder] will be sorted according to the comparator, and the two sets of
+ * items will be combined to have the fewest elements out of order according to the [comparator]
+ * . The result will be put into [output].
+ */
+ fun <T : Any> sortTo(
+ items: Iterable<T>,
+ stableOrder: StableOrder<in T>,
+ comparator: Comparator<in T>,
+ output: MutableList<T>,
+ ): Boolean {
+ if (DEBUG) println("\n> START from ${items.map { it to stableOrder.getRank(it) }}")
+ // If array already has elements, use subList to ensure we only append
+ val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size)
+ items.filterTo(result) { stableOrder.getRank(it) != null }
+ result.sortBy { stableOrder.getRank(it)!! }
+ val isOrdered = result.isSorted(comparator)
+ withAdditions<T> { additions ->
+ items.filterTo(additions) { stableOrder.getRank(it) == null }
+ additions.sortWith(comparator)
+ insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator)
+ }
+ return isOrdered
+ }
+
+ /**
+ * Rearrange the [sortedItems] to enforce that items are in the [stableOrder], and store the
+ * result in [output]. Items with a [stableOrder] will be in that order, items without a
+ * [stableOrder] will remain in same relative order as the input, and the two sets of items will
+ * be combined to have the fewest elements moved from their locations in the original.
+ */
+ fun <T : Any> stabilizeTo(
+ sortedItems: Iterable<T>,
+ stableOrder: StableOrder<in T>,
+ output: MutableList<T>,
+ ): Boolean {
+ // Append to the output array if present
+ val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size)
+ sortedItems.filterTo(result) { stableOrder.getRank(it) != null }
+ val stableRankComparator = compareBy<T> { stableOrder.getRank(it)!! }
+ val isOrdered = result.isSorted(stableRankComparator)
+ if (!isOrdered) {
+ result.sortWith(stableRankComparator)
+ }
+ if (result.isEmpty()) {
+ sortedItems.filterTo(result) { stableOrder.getRank(it) == null }
+ return isOrdered
+ }
+ withAdditions<T> { additions ->
+ sortedItems.filterTo(additions) { stableOrder.getRank(it) == null }
+ if (additions.isNotEmpty()) {
+ withIndexOfComparator(sortedItems) { comparator ->
+ insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator)
+ }
+ }
+ }
+ return isOrdered
+ }
+
+ private inline fun <T : Any, R> withWorkspace(block: (ArrayList<T>) -> R): R {
+ preallocatedWorkspace.clear()
+ val result = block(preallocatedWorkspace as ArrayList<T>)
+ preallocatedWorkspace.clear()
+ return result
+ }
+
+ private inline fun <T : Any> withAdditions(block: (ArrayList<T>) -> Unit) {
+ preallocatedAdditions.clear()
+ block(preallocatedAdditions as ArrayList<T>)
+ preallocatedAdditions.clear()
+ }
+
+ private inline fun <T : Any> withIndexOfComparator(
+ sortedItems: Iterable<T>,
+ block: (Comparator<in T>) -> Unit
+ ) {
+ preallocatedMapToIndex.clear()
+ sortedItems.forEachIndexed { i, item -> preallocatedMapToIndex[item] = i }
+ block(preallocatedMapToIndexComparator as Comparator<in T>)
+ preallocatedMapToIndex.clear()
+ }
+
+ companion object {
+
+ /**
+ * This is the core of the algorithm.
+ *
+ * Insert [preSortedAdditions] (the elements to be inserted) into [existing] without
+ * changing the relative order of any elements already in [existing], even though those
+ * elements may be mis-ordered relative to the [comparator], such that the total number of
+ * elements which are ordered incorrectly according to the [comparator] is fewest.
+ */
+ private fun <T> insertPreSortedElementsWithFewestMisOrderings(
+ existing: MutableList<T>,
+ preSortedAdditions: Iterable<T>,
+ comparator: Comparator<in T>,
+ ) {
+ if (DEBUG) println(" To $existing insert $preSortedAdditions with fewest misordering")
+ var iStart = 0
+ preSortedAdditions.forEach { toAdd ->
+ if (DEBUG) println(" need to add $toAdd to $existing, starting at $iStart")
+ var cmpSum = 0
+ var cmpSumMax = 0
+ var iCmpSumMax = iStart
+ if (DEBUG) print(" ")
+ for (i in iCmpSumMax until existing.size) {
+ val cmp = comparator.compare(toAdd, existing[i]).sign
+ cmpSum += cmp
+ if (cmpSum > cmpSumMax) {
+ cmpSumMax = cmpSum
+ iCmpSumMax = i + 1
+ }
+ if (DEBUG) print("sum[$i]=$cmpSum, ")
+ }
+ if (DEBUG) println("inserting $toAdd at $iCmpSumMax")
+ existing.add(iCmpSumMax, toAdd)
+ iStart = iCmpSumMax + 1
+ }
+ }
+
+ /** Determines if a list is correctly sorted according to the given comparator */
+ @VisibleForTesting
+ fun <T> List<T>.isSorted(comparator: Comparator<T>): Boolean {
+ if (this.size <= 1) {
+ return true
+ }
+ val iterator = this.iterator()
+ var previous = iterator.next()
+ var current: T?
+ while (iterator.hasNext()) {
+ current = iterator.next()
+ if (comparator.compare(previous, current) > 0) {
+ return false
+ }
+ previous = current
+ }
+ return true
+ }
+ }
+
+ fun interface StableOrder<T> {
+ fun getRank(item: T): Int?
+ }
+}
+
+val DEBUG = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt
new file mode 100644
index 0000000..d8f75f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import com.android.systemui.statusbar.notification.collection.ListEntry
+
+object ShadeListBuilderHelper {
+ fun getSectionSubLists(entries: List<ListEntry>): Iterable<List<ListEntry>> =
+ getContiguousSubLists(entries, minLength = 1) { it.sectionIndex }
+
+ inline fun <T : Any, K : Any> getContiguousSubLists(
+ itemList: List<T>,
+ minLength: Int = 1,
+ key: (T) -> K,
+ ): Iterable<List<T>> {
+ val subLists = mutableListOf<List<T>>()
+ val numEntries = itemList.size
+ var currentSectionStartIndex = 0
+ var currentSectionKey: K? = null
+ for (i in 0 until numEntries) {
+ val sectionKey = key(itemList[i])
+ if (currentSectionKey == null) {
+ currentSectionKey = sectionKey
+ } else if (currentSectionKey != sectionKey) {
+ val length = i - currentSectionStartIndex
+ if (length >= minLength) {
+ subLists.add(itemList.subList(currentSectionStartIndex, i))
+ }
+ currentSectionStartIndex = i
+ currentSectionKey = sectionKey
+ }
+ }
+ val length = numEntries - currentSectionStartIndex
+ if (length >= minLength) {
+ subLists.add(itemList.subList(currentSectionStartIndex, numEntries))
+ }
+ return subLists
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
index d8dae5d..8e052c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
@@ -16,11 +16,11 @@
package com.android.systemui.statusbar.notification.collection.listbuilder
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
index aa27e1e..911a2d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
@@ -20,13 +20,13 @@
import android.service.notification.NotificationListenerService
import android.service.notification.NotificationListenerService.RankingMap
import android.service.notification.StatusBarNotification
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
-import com.android.systemui.log.LogLevel.WTF
import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
+import com.android.systemui.plugins.log.LogLevel.WTF
import com.android.systemui.statusbar.notification.collection.NotifCollection
import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason
import com.android.systemui.statusbar.notification.collection.NotifCollection.FutureDismissal
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
index 38e3d49..9c71e5c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.notification.collection.render
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
import com.android.systemui.util.Compile
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
index b6278d1..fde4ecb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.collection.render
+import javax.inject.Inject
+
/** An interface by which the pipeline can make updates to the notification root view. */
interface NotifStackController {
/** Provides stats about the list of notifications attached to the shade */
@@ -42,6 +44,6 @@
* methods, rather than forcing us to add no-op implementations in their implementation every time
* a method is added.
*/
-open class DefaultNotifStackController : NotifStackController {
+open class DefaultNotifStackController @Inject constructor() : NotifStackController {
override fun setNotifStats(stats: NotifStats) {}
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
index 6d1071c..b4b9438 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.notification.collection.render
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import java.lang.RuntimeException
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt
index 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/HeadsUpViewBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java
index 6f41425..9a7610d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java
@@ -114,7 +114,18 @@
*/
public void unbindHeadsUpView(NotificationEntry entry) {
abortBindCallback(entry);
- mStage.getStageParams(entry).markContentViewsFreeable(FLAG_CONTENT_VIEW_HEADS_UP);
+
+ // params may be null if the notification was already removed from the collection but we let
+ // it stick around during a launch animation. In this case, the heads up view has already
+ // been unbound, so we don't need to unbind it.
+ // TODO(b/253081345): Change this back to getStageParams and remove null check.
+ RowContentBindParams params = mStage.tryGetStageParams(entry);
+ if (params == null) {
+ mLogger.entryBindStageParamsNullOnUnbind(entry);
+ return;
+ }
+
+ params.markContentViewsFreeable(FLAG_CONTENT_VIEW_HEADS_UP);
mLogger.entryContentViewMarkedFreeable(entry);
mStage.requestRebind(entry, e -> mLogger.entryUnbound(e));
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
index d1feaa0..d4f11fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
@@ -1,8 +1,8 @@
package com.android.systemui.statusbar.notification.interruption
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
import javax.inject.Inject
@@ -47,6 +47,14 @@
"start unbinding heads up entry $str1 "
})
}
+
+ fun entryBindStageParamsNullOnUnbind(entry: NotificationEntry) {
+ buffer.log(TAG, INFO, {
+ str1 = entry.logKey
+ }, {
+ "heads up entry bind stage params null on unbind $str1 "
+ })
+ }
}
private const val TAG = "HeadsUpViewBinder"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
index c956a2e..e6dbcee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
@@ -72,7 +72,6 @@
@SysUISingleton
private class KeyguardNotificationVisibilityProviderImpl @Inject constructor(
- context: Context,
@Main private val handler: Handler,
private val keyguardStateController: KeyguardStateController,
private val lockscreenUserManager: NotificationLockscreenUserManager,
@@ -82,7 +81,7 @@
private val broadcastDispatcher: BroadcastDispatcher,
private val secureSettings: SecureSettings,
private val globalSettings: GlobalSettings
-) : CoreStartable(context), KeyguardNotificationVisibilityProvider {
+) : CoreStartable, KeyguardNotificationVisibilityProvider {
private val showSilentNotifsUri =
secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS)
private val onStateChangedListeners = ListenerSet<Consumer<String>>()
@@ -232,7 +231,7 @@
private fun readShowSilentNotificationSetting() {
val showSilentNotifs =
secureSettings.getBoolForUser(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS,
- true, UserHandle.USER_CURRENT)
+ false, UserHandle.USER_CURRENT)
hideSilentNotificationsOnLockscreen = !showSilentNotifs
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
index 99d320d..073b6b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
@@ -16,11 +16,11 @@
package com.android.systemui.statusbar.notification.interruption
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
import com.android.systemui.log.dagger.NotificationInterruptLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
index 3292a8f..6cf4bf3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
@@ -37,6 +37,19 @@
boolean shouldHeadsUp(NotificationEntry entry);
/**
+ * Returns the value of whether this entry should peek (from shouldHeadsUp(entry)), but only
+ * optionally logs the status.
+ *
+ * This method should be used in cases where the caller needs to check whether a notification
+ * qualifies for a heads up, but is not necessarily guaranteed to make the heads-up happen.
+ *
+ * @param entry the entry to check
+ * @param log whether or not to log the results of this check
+ * @return true if the entry should heads up, false otherwise
+ */
+ boolean checkHeadsUp(NotificationEntry entry, boolean log);
+
+ /**
* Whether the notification should appear as a bubble with a fly-out on top of the screen.
*
* @param entry the entry to check
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 558fd62..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) {
@@ -137,11 +163,11 @@
public boolean shouldBubbleUp(NotificationEntry entry) {
final StatusBarNotification sbn = entry.getSbn();
- if (!canAlertCommon(entry)) {
+ if (!canAlertCommon(entry, true)) {
return false;
}
- if (!canAlertAwakeCommon(entry)) {
+ if (!canAlertAwakeCommon(entry, true)) {
return false;
}
@@ -163,10 +189,15 @@
@Override
public boolean shouldHeadsUp(NotificationEntry entry) {
+ return checkHeadsUp(entry, true);
+ }
+
+ @Override
+ public boolean checkHeadsUp(NotificationEntry entry, boolean log) {
if (mStatusBarStateController.isDozing()) {
- return shouldHeadsUpWhenDozing(entry);
+ return shouldHeadsUpWhenDozing(entry, log);
} else {
- return shouldHeadsUpWhenAwake(entry);
+ return shouldHeadsUpWhenAwake(entry, log);
}
}
@@ -198,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;
}
@@ -244,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;
}
@@ -263,61 +298,61 @@
}
}
- private boolean shouldHeadsUpWhenAwake(NotificationEntry entry) {
+ private boolean shouldHeadsUpWhenAwake(NotificationEntry entry, boolean log) {
StatusBarNotification sbn = entry.getSbn();
if (!mUseHeadsUp) {
- mLogger.logNoHeadsUpFeatureDisabled();
+ if (log) mLogger.logNoHeadsUpFeatureDisabled();
return false;
}
- if (!canAlertCommon(entry)) {
+ if (!canAlertCommon(entry, log)) {
return false;
}
- if (!canAlertHeadsUpCommon(entry)) {
+ if (!canAlertHeadsUpCommon(entry, log)) {
return false;
}
- if (!canAlertAwakeCommon(entry)) {
+ if (!canAlertAwakeCommon(entry, log)) {
return false;
}
if (isSnoozedPackage(sbn)) {
- mLogger.logNoHeadsUpPackageSnoozed(entry);
+ if (log) mLogger.logNoHeadsUpPackageSnoozed(entry);
return false;
}
boolean inShade = mStatusBarStateController.getState() == SHADE;
if (entry.isBubble() && inShade) {
- mLogger.logNoHeadsUpAlreadyBubbled(entry);
+ if (log) mLogger.logNoHeadsUpAlreadyBubbled(entry);
return false;
}
if (entry.shouldSuppressPeek()) {
- mLogger.logNoHeadsUpSuppressedByDnd(entry);
+ if (log) mLogger.logNoHeadsUpSuppressedByDnd(entry);
return false;
}
if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) {
- mLogger.logNoHeadsUpNotImportant(entry);
+ if (log) mLogger.logNoHeadsUpNotImportant(entry);
return false;
}
boolean inUse = mPowerManager.isScreenOn() && !isDreaming();
if (!inUse) {
- mLogger.logNoHeadsUpNotInUse(entry);
+ if (log) mLogger.logNoHeadsUpNotInUse(entry);
return false;
}
for (int i = 0; i < mSuppressors.size(); i++) {
if (mSuppressors.get(i).suppressAwakeHeadsUp(entry)) {
- mLogger.logNoHeadsUpSuppressedBy(entry, mSuppressors.get(i));
+ if (log) mLogger.logNoHeadsUpSuppressedBy(entry, mSuppressors.get(i));
return false;
}
}
- mLogger.logHeadsUp(entry);
+ if (log) mLogger.logHeadsUp(entry);
return true;
}
@@ -328,37 +363,37 @@
* @param entry the entry to check
* @return true if the entry should ambient pulse, false otherwise
*/
- private boolean shouldHeadsUpWhenDozing(NotificationEntry entry) {
+ private boolean shouldHeadsUpWhenDozing(NotificationEntry entry, boolean log) {
if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) {
- mLogger.logNoPulsingSettingDisabled(entry);
+ if (log) mLogger.logNoPulsingSettingDisabled(entry);
return false;
}
if (mBatteryController.isAodPowerSave()) {
- mLogger.logNoPulsingBatteryDisabled(entry);
+ if (log) mLogger.logNoPulsingBatteryDisabled(entry);
return false;
}
- if (!canAlertCommon(entry)) {
- mLogger.logNoPulsingNoAlert(entry);
+ if (!canAlertCommon(entry, log)) {
+ if (log) mLogger.logNoPulsingNoAlert(entry);
return false;
}
- if (!canAlertHeadsUpCommon(entry)) {
- mLogger.logNoPulsingNoAlert(entry);
+ if (!canAlertHeadsUpCommon(entry, log)) {
+ if (log) mLogger.logNoPulsingNoAlert(entry);
return false;
}
if (entry.shouldSuppressAmbient()) {
- mLogger.logNoPulsingNoAmbientEffect(entry);
+ if (log) mLogger.logNoPulsingNoAmbientEffect(entry);
return false;
}
if (entry.getImportance() < NotificationManager.IMPORTANCE_DEFAULT) {
- mLogger.logNoPulsingNotImportant(entry);
+ if (log) mLogger.logNoPulsingNotImportant(entry);
return false;
}
- mLogger.logPulsing(entry);
+ if (log) mLogger.logPulsing(entry);
return true;
}
@@ -366,18 +401,22 @@
* Common checks between regular & AOD heads up and bubbles.
*
* @param entry the entry to check
+ * @param log whether or not to log the results of these checks
* @return true if these checks pass, false if the notification should not alert
*/
- private boolean canAlertCommon(NotificationEntry entry) {
+ private boolean canAlertCommon(NotificationEntry entry, boolean log) {
for (int i = 0; i < mSuppressors.size(); i++) {
if (mSuppressors.get(i).suppressInterruptions(entry)) {
- mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i), /* awake */ false);
+ if (log) {
+ mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i),
+ /* awake */ false);
+ }
return false;
}
}
if (mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry)) {
- mLogger.keyguardHideNotification(entry);
+ if (log) mLogger.keyguardHideNotification(entry);
return false;
}
@@ -388,19 +427,20 @@
* Common checks for heads up notifications on regular and AOD displays.
*
* @param entry the entry to check
+ * @param log whether or not to log the results of these checks
* @return true if these checks pass, false if the notification should not alert
*/
- private boolean canAlertHeadsUpCommon(NotificationEntry entry) {
+ private boolean canAlertHeadsUpCommon(NotificationEntry entry, boolean log) {
StatusBarNotification sbn = entry.getSbn();
// Don't alert notifications that are suppressed due to group alert behavior
if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
- mLogger.logNoAlertingGroupAlertBehavior(entry);
+ if (log) mLogger.logNoAlertingGroupAlertBehavior(entry);
return false;
}
if (entry.hasJustLaunchedFullScreenIntent()) {
- mLogger.logNoAlertingRecentFullscreen(entry);
+ if (log) mLogger.logNoAlertingRecentFullscreen(entry);
return false;
}
@@ -413,12 +453,14 @@
* @param entry the entry to check
* @return true if these checks pass, false if the notification should not alert
*/
- private boolean canAlertAwakeCommon(NotificationEntry entry) {
+ private boolean canAlertAwakeCommon(NotificationEntry entry, boolean log) {
StatusBarNotification sbn = entry.getSbn();
for (int i = 0; i < mSuppressors.size(); i++) {
if (mSuppressors.get(i).suppressAwakeInterruptions(entry)) {
- mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i), /* awake */ true);
+ if (log) {
+ mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i), /* awake */ true);
+ }
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/logging/NotificationRoundnessLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
index fe03b2a..10197a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.notification.logging
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
import com.android.systemui.log.dagger.NotificationRenderLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.notification.stack.NotificationSection
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 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/BindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java
index 7c41800..d626c18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java
@@ -21,6 +21,7 @@
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -64,7 +65,7 @@
* Get the stage parameters for the entry. Clients should use this to modify how the stage
* handles the notification content.
*/
- public final Params getStageParams(@NonNull NotificationEntry entry) {
+ public final @NonNull Params getStageParams(@NonNull NotificationEntry entry) {
Params params = mContentParams.get(entry);
if (params == null) {
// TODO: This should throw an exception but there are some cases of re-entrant calls
@@ -79,6 +80,17 @@
return params;
}
+ // TODO(b/253081345): Remove this method.
+ /**
+ * Get the stage parameters for the entry, or null if there are no stage parameters for the
+ * entry.
+ *
+ * @see #getStageParams(NotificationEntry)
+ */
+ public final @Nullable Params tryGetStageParams(@NonNull NotificationEntry entry) {
+ return mContentParams.get(entry);
+ }
+
/**
* Create a params entry for the notification for this stage.
*/
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/NotifBindPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
index ab91926..46fef3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.notification.row
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 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/RowContentBindStageLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
index f9923b2..8a5d29a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.notification.row
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index 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/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index ce465bc..b2628e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -44,7 +44,8 @@
import javax.inject.Inject;
/**
- * A global state to track all input states for the algorithm.
+ * Global state to track all input states for
+ * {@link com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm}.
*/
@SysUISingleton
public class AmbientState implements Dumpable {
@@ -141,6 +142,11 @@
*/
private boolean mIsFlingRequiredAfterLockScreenSwipeUp = false;
+ /**
+ * Whether the shade is currently closing.
+ */
+ private boolean mIsClosing;
+
@VisibleForTesting
public boolean isFlingRequiredAfterLockScreenSwipeUp() {
return mIsFlingRequiredAfterLockScreenSwipeUp;
@@ -716,6 +722,20 @@
&& mStatusBarKeyguardViewManager.isBouncerInTransit();
}
+ /**
+ * @param isClosing Whether the shade is currently closing.
+ */
+ public void setIsClosing(boolean isClosing) {
+ mIsClosing = isClosing;
+ }
+
+ /**
+ * @return Whether the shade is currently closing.
+ */
+ public boolean isClosing() {
+ return mIsClosing;
+ }
+
@Override
public void dump(PrintWriter pw, String[] args) {
pw.println("mTopPadding=" + mTopPadding);
@@ -760,5 +780,6 @@
+ mIsFlingRequiredAfterLockScreenSwipeUp);
pw.println("mZDistanceBetweenElements=" + mZDistanceBetweenElements);
pw.println("mBaseZHeight=" + mBaseZHeight);
+ pw.println("mIsClosing=" + mIsClosing);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 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/NotificationSectionsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
index cb7dfe8..b61c55e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
@@ -17,9 +17,9 @@
package com.android.systemui.statusbar.notification.stack
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.NotificationSectionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import javax.inject.Inject
private const val TAG = "NotifSections"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index 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 836cacc..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
@@ -255,7 +255,6 @@
private boolean mClearAllInProgress;
private FooterClearAllListener mFooterClearAllListener;
private boolean mFlingAfterUpEvent;
-
/**
* Was the scroller scrolled to the top when the down motion was observed?
*/
@@ -1115,6 +1114,10 @@
updateAlgorithmLayoutMinHeight();
updateOwnTranslationZ();
+ // Give The Algorithm information regarding the QS height so it can layout notifications
+ // properly. Needed for some devices that grows notifications down-to-top
+ mStackScrollAlgorithm.updateQSFrameTop(mQsHeader == null ? 0 : mQsHeader.getHeight());
+
// Once the layout has finished, we don't need to animate any scrolling clampings anymore.
mAnimateStackYForContentHeightChange = false;
}
@@ -1185,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;
@@ -1655,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;
@@ -1688,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]);
@@ -2273,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++;
}
@@ -2493,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;
@@ -2506,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;
@@ -2520,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) {
@@ -2879,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) {
@@ -2933,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();
}
@@ -3965,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);
@@ -3978,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());
@@ -4016,8 +4023,9 @@
setOwnScrollY(0);
}
+ @VisibleForTesting
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
- private void setIsExpanded(boolean isExpanded) {
+ void setIsExpanded(boolean isExpanded) {
boolean changed = isExpanded != mIsExpanded;
mIsExpanded = isExpanded;
mStackScrollAlgorithm.setIsExpanded(isExpanded);
@@ -4226,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);
@@ -4261,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();
}
@@ -4281,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);
}
@@ -4508,7 +4516,7 @@
public void setClearAllInProgress(boolean clearAllInProgress) {
mClearAllInProgress = clearAllInProgress;
mAmbientState.setClearAllInProgress(clearAllInProgress);
- mController.getNoticationRoundessManager().setClearAllInProgress(clearAllInProgress);
+ mController.getNotificationRoundnessManager().setClearAllInProgress(clearAllInProgress);
}
boolean getClearAllInProgress() {
@@ -4551,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;
}
@@ -4582,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) {
@@ -4838,13 +4846,21 @@
}
}
+ @VisibleForTesting
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
- private void setOwnScrollY(int ownScrollY) {
+ void setOwnScrollY(int ownScrollY) {
setOwnScrollY(ownScrollY, false /* animateScrollChangeListener */);
}
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
private void setOwnScrollY(int ownScrollY, boolean animateStackYChangeListener) {
+ // Avoid Flicking during clear all
+ // when the shade finishes closing, onExpansionStopped will call
+ // resetScrollPosition to setOwnScrollY to 0
+ if (mAmbientState.isClosing()) {
+ return;
+ }
+
if (ownScrollY != mOwnScrollY) {
// We still want to call the normal scrolled changed for accessibility reasons
onScrollChanged(mScrollX, ownScrollY, mScrollX, mOwnScrollY);
@@ -5040,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();
}
@@ -5329,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;
}
@@ -5368,7 +5384,7 @@
public void setController(
NotificationStackScrollLayoutController notificationStackScrollLayoutController) {
mController = notificationStackScrollLayoutController;
- mController.getNoticationRoundessManager().setAnimatedChildren(mChildrenToAddAnimated);
+ mController.getNotificationRoundnessManager().setAnimatedChildren(mChildrenToAddAnimated);
}
void addSwipedOutView(View v) {
@@ -5379,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);
@@ -5414,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/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
index 5f79c0e..4c52db7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
@@ -1,8 +1,8 @@
package com.android.systemui.statusbar.notification.stack
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
new file mode 100644
index 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 8d28f75..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;
@@ -417,12 +418,19 @@
}
/**
+ * Update the position of QS Frame.
+ */
+ public void updateQSFrameTop(int qsHeight) {
+ // Intentionally empty for sub-classes in other device form factors to override
+ }
+
+ /**
* Determine the positions for the views. This is the main part of the algorithm.
*
* @param algorithmState The state in which the current pass of the algorithm is currently in
* @param ambientState The current ambient state
*/
- private void updatePositionsForState(StackScrollAlgorithmState algorithmState,
+ protected void updatePositionsForState(StackScrollAlgorithmState algorithmState,
AmbientState ambientState) {
if (!ambientState.isOnKeyguard()
|| (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) {
@@ -448,7 +456,7 @@
* @return Fraction to apply to view height and gap between views.
* Does not include shelf height even if shelf is showing.
*/
- private float getExpansionFractionWithoutShelf(
+ protected float getExpansionFractionWithoutShelf(
StackScrollAlgorithmState algorithmState,
AmbientState ambientState) {
@@ -797,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/notification/stack/StackStateLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
index cb4a088..f5de678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
@@ -1,8 +1,8 @@
package com.android.systemui.statusbar.notification.stack
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.statusbar.notification.logKey
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index fe43137..a2798f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -163,7 +163,6 @@
private PowerManager.WakeLock mWakeLock;
private final com.android.systemui.shade.ShadeController mShadeController;
private final KeyguardUpdateMonitor mUpdateMonitor;
- private final DozeParameters mDozeParameters;
private final KeyguardStateController mKeyguardStateController;
private final NotificationShadeWindowController mNotificationShadeWindowController;
private final SessionTracker mSessionTracker;
@@ -278,7 +277,7 @@
KeyguardStateController keyguardStateController, Handler handler,
KeyguardUpdateMonitor keyguardUpdateMonitor,
@Main Resources resources,
- KeyguardBypassController keyguardBypassController, DozeParameters dozeParameters,
+ KeyguardBypassController keyguardBypassController,
MetricsLogger metricsLogger, DumpManager dumpManager,
PowerManager powerManager,
NotificationMediaManager notificationMediaManager,
@@ -294,7 +293,6 @@
mPowerManager = powerManager;
mShadeController = shadeController;
mUpdateMonitor = keyguardUpdateMonitor;
- mDozeParameters = dozeParameters;
mUpdateMonitor.registerCallback(this);
mMediaManager = notificationMediaManager;
mLatencyTracker = latencyTracker;
@@ -552,7 +550,7 @@
boolean deviceDreaming = mUpdateMonitor.isDreaming();
if (!mUpdateMonitor.isDeviceInteractive()) {
- if (!mKeyguardViewController.isShowing()
+ if (!mKeyguardStateController.isShowing()
&& !mScreenOffAnimationController.isKeyguardShowDelayed()) {
if (mKeyguardStateController.isUnlocked()) {
return MODE_WAKE_AND_UNLOCK;
@@ -569,7 +567,7 @@
if (unlockingAllowed && deviceDreaming) {
return MODE_WAKE_AND_UNLOCK_FROM_DREAM;
}
- if (mKeyguardViewController.isShowing()) {
+ if (mKeyguardStateController.isShowing()) {
if (mKeyguardViewController.bouncerIsOrWillBeShowing() && unlockingAllowed) {
return MODE_DISMISS_BOUNCER;
} else if (unlockingAllowed) {
@@ -588,7 +586,7 @@
boolean bypass = mKeyguardBypassController.getBypassEnabled()
|| mAuthController.isUdfpsFingerDown();
if (!mUpdateMonitor.isDeviceInteractive()) {
- if (!mKeyguardViewController.isShowing()) {
+ if (!mKeyguardStateController.isShowing()) {
return bypass ? MODE_WAKE_AND_UNLOCK : MODE_ONLY_WAKE;
} else if (!unlockingAllowed) {
return bypass ? MODE_SHOW_BOUNCER : MODE_NONE;
@@ -612,7 +610,7 @@
if (unlockingAllowed && mKeyguardStateController.isOccluded()) {
return MODE_UNLOCK_COLLAPSING;
}
- if (mKeyguardViewController.isShowing()) {
+ if (mKeyguardStateController.isShowing()) {
if ((mKeyguardViewController.bouncerIsOrWillBeShowing()
|| mKeyguardBypassController.getAltBouncerShowing()) && unlockingAllowed) {
return MODE_DISMISS_BOUNCER;
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/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 52a45d6..1e95dad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -363,7 +363,7 @@
mKeyguardUpdateMonitor.onCameraLaunched();
}
- if (!mStatusBarKeyguardViewManager.isShowing()) {
+ if (!mKeyguardStateController.isShowing()) {
final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext);
mCentralSurfaces.startActivityDismissingKeyguard(cameraIntent,
false /* onlyProvisioned */, true /* dismissShade */,
@@ -420,7 +420,7 @@
// TODO(b/169087248) Possibly add haptics here for emergency action. Currently disabled for
// app-side haptic experimentation.
- if (!mStatusBarKeyguardViewManager.isShowing()) {
+ if (!mKeyguardStateController.isShowing()) {
mCentralSurfaces.startActivityDismissingKeyguard(emergencyIntent,
false /* onlyProvisioned */, true /* dismissShade */,
true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
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 01a92b8..db8aac95 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -91,7 +91,6 @@
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.MathUtils;
-import android.util.Slog;
import android.view.Display;
import android.view.IRemoteAnimationRunner;
import android.view.IWindowManager;
@@ -183,6 +182,8 @@
import com.android.systemui.shade.NotificationShadeWindowView;
import com.android.systemui.shade.NotificationShadeWindowViewController;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.shared.plugins.PluginManager;
import com.android.systemui.statusbar.AutoHideUiElement;
import com.android.systemui.statusbar.BackDropView;
@@ -221,8 +222,6 @@
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
import com.android.systemui.statusbar.phone.dagger.StatusBarPhoneModule;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -270,8 +269,7 @@
* </b>
*/
@SysUISingleton
-public class CentralSurfacesImpl extends CoreStartable implements
- CentralSurfaces {
+public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
private static final String BANNER_ACTION_CANCEL =
"com.android.systemui.statusbar.banner_action_cancel";
@@ -307,6 +305,7 @@
ONLY_CORE_APPS = onlyCoreApps;
}
+ private final Context mContext;
private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
private CentralSurfacesCommandQueueCallbacks mCommandQueueCallbacks;
private float mTransitionToFullShadeProgress = 0f;
@@ -476,7 +475,6 @@
private final KeyguardStateController mKeyguardStateController;
private final HeadsUpManagerPhone mHeadsUpManager;
private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
- private final DynamicPrivacyController mDynamicPrivacyController;
private final FalsingCollector mFalsingCollector;
private final FalsingManager mFalsingManager;
private final BroadcastDispatcher mBroadcastDispatcher;
@@ -514,7 +512,7 @@
private final NotificationGutsManager mGutsManager;
private final NotificationLogger mNotificationLogger;
- private final PanelExpansionStateManager mPanelExpansionStateManager;
+ private final ShadeExpansionStateManager mShadeExpansionStateManager;
private final KeyguardViewMediator mKeyguardViewMediator;
protected final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
private final BrightnessSliderController.Factory mBrightnessSliderFactory;
@@ -700,7 +698,7 @@
NotificationGutsManager notificationGutsManager,
NotificationLogger notificationLogger,
NotificationInterruptStateProvider notificationInterruptStateProvider,
- PanelExpansionStateManager panelExpansionStateManager,
+ ShadeExpansionStateManager shadeExpansionStateManager,
KeyguardViewMediator keyguardViewMediator,
DisplayMetrics displayMetrics,
MetricsLogger metricsLogger,
@@ -765,7 +763,7 @@
DeviceStateManager deviceStateManager,
WiredChargingRippleController wiredChargingRippleController,
IDreamManager dreamManager) {
- super(context);
+ mContext = context;
mNotificationsController = notificationsController;
mFragmentService = fragmentService;
mLightBarController = lightBarController;
@@ -779,14 +777,13 @@
mHeadsUpManager = headsUpManagerPhone;
mKeyguardIndicationController = keyguardIndicationController;
mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
- mDynamicPrivacyController = dynamicPrivacyController;
mFalsingCollector = falsingCollector;
mFalsingManager = falsingManager;
mBroadcastDispatcher = broadcastDispatcher;
mGutsManager = notificationGutsManager;
mNotificationLogger = notificationLogger;
mNotificationInterruptStateProvider = notificationInterruptStateProvider;
- mPanelExpansionStateManager = panelExpansionStateManager;
+ mShadeExpansionStateManager = shadeExpansionStateManager;
mKeyguardViewMediator = keyguardViewMediator;
mDisplayMetrics = displayMetrics;
mMetricsLogger = metricsLogger;
@@ -851,7 +848,7 @@
mScreenOffAnimationController = screenOffAnimationController;
- mPanelExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged);
+ mShadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged);
mBubbleExpandListener = (isExpanding, key) ->
mContext.getMainExecutor().execute(this::updateScrimController);
@@ -888,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();
@@ -1139,9 +1141,8 @@
// 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);
- mPanelExpansionStateManager.addExpansionListener(mWakeUpCoordinator);
+ mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator);
mUserSwitcherController.init(mNotificationShadeWindowView);
// Allow plugins to reference DarkIconDispatcher and StatusBarStateController
@@ -1377,7 +1378,7 @@
}
}
- private void onPanelExpansionChanged(PanelExpansionChangeEvent event) {
+ private void onPanelExpansionChanged(ShadeExpansionChangeEvent event) {
float fraction = event.getFraction();
boolean tracking = event.getTracking();
dispatchPanelExpansionForKeyguardDismiss(fraction, tracking);
@@ -1424,6 +1425,7 @@
mStackScrollerController.setNotificationActivityStarter(mNotificationActivityStarter);
mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);
mNotificationsController.initialize(
+ this,
mPresenter,
mNotifListContainer,
mStackScrollerController.getNotifStackController(),
@@ -1561,7 +1563,7 @@
mKeyguardViewMediator.registerCentralSurfaces(
/* statusBar= */ this,
mNotificationPanelViewController,
- mPanelExpansionStateManager,
+ mShadeExpansionStateManager,
mBiometricUnlockController,
mStackScroller,
mKeyguardBypassController);
@@ -1570,7 +1572,6 @@
.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
mRemoteInputManager.addControllerCallback(mStatusBarKeyguardViewManager);
- mDynamicPrivacyController.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
mLightBarController.setBiometricUnlockController(mBiometricUnlockController);
mMediaManager.setBiometricUnlockController(mBiometricUnlockController);
@@ -1794,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;
}
@@ -2082,7 +2071,7 @@
// Trimming will happen later if Keyguard is showing - doing it here might cause a jank in
// the bouncer appear animation.
- if (!mStatusBarKeyguardViewManager.isShowing()) {
+ if (!mKeyguardStateController.isShowing()) {
WindowManagerGlobal.getInstance().trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
}
}
@@ -2519,8 +2508,8 @@
};
// Do not deferKeyguard when occluded because, when keyguard is occluded,
// we do not launch the activity until keyguard is done.
- boolean occluded = mStatusBarKeyguardViewManager.isShowing()
- && mStatusBarKeyguardViewManager.isOccluded();
+ boolean occluded = mKeyguardStateController.isShowing()
+ && mKeyguardStateController.isOccluded();
boolean deferred = !occluded;
executeRunnableDismissingKeyguard(runnable, cancelRunnable, dismissShadeDirectly,
willLaunchResolverActivity, deferred /* deferred */, animate);
@@ -2590,8 +2579,8 @@
@Override
public boolean onDismiss() {
if (runnable != null) {
- if (mStatusBarKeyguardViewManager.isShowing()
- && mStatusBarKeyguardViewManager.isOccluded()) {
+ if (mKeyguardStateController.isShowing()
+ && mKeyguardStateController.isOccluded()) {
mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable);
} else {
mMainExecutor.execute(runnable);
@@ -2685,7 +2674,7 @@
private void executeWhenUnlocked(OnDismissAction action, boolean requiresShadeOpen,
boolean afterKeyguardGone) {
- if (mStatusBarKeyguardViewManager.isShowing() && requiresShadeOpen) {
+ if (mKeyguardStateController.isShowing() && requiresShadeOpen) {
mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
}
dismissKeyguardThenExecute(action, null /* cancelAction */,
@@ -2709,7 +2698,7 @@
mBiometricUnlockController.startWakeAndUnlock(
BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING);
}
- if (mStatusBarKeyguardViewManager.isShowing()) {
+ if (mKeyguardStateController.isShowing()) {
mStatusBarKeyguardViewManager.dismissWithAction(action, cancelAction,
afterKeyguardGone);
} else {
@@ -2845,8 +2834,8 @@
}
private void logStateToEventlog() {
- boolean isShowing = mStatusBarKeyguardViewManager.isShowing();
- boolean isOccluded = mStatusBarKeyguardViewManager.isOccluded();
+ boolean isShowing = mKeyguardStateController.isShowing();
+ boolean isOccluded = mKeyguardStateController.isOccluded();
boolean isBouncerShowing = mStatusBarKeyguardViewManager.isBouncerShowing();
boolean isSecure = mKeyguardStateController.isMethodSecure();
boolean unlocked = mKeyguardStateController.canDismissLockScreen();
@@ -3242,18 +3231,17 @@
Trace.traceCounter(Trace.TRACE_TAG_APP, "dozing", mDozing ? 1 : 0);
Trace.beginSection("CentralSurfaces#updateDozingState");
- boolean visibleNotOccluded = mStatusBarKeyguardViewManager.isShowing()
- && !mStatusBarKeyguardViewManager.isOccluded();
+ boolean keyguardVisible = mKeyguardStateController.isVisible();
// If we're dozing and we'll be animating the screen off, the keyguard isn't currently
// visible but will be shortly for the animation, so we should proceed as if it's visible.
- boolean visibleNotOccludedOrWillBe =
- visibleNotOccluded || (mDozing && mDozeParameters.shouldDelayKeyguardShow());
+ boolean keyguardVisibleOrWillBe =
+ keyguardVisible || (mDozing && mDozeParameters.shouldDelayKeyguardShow());
boolean wakeAndUnlock = mBiometricUnlockController.getMode()
== BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
boolean animate = (!mDozing && mDozeServiceHost.shouldAnimateWakeup() && !wakeAndUnlock)
|| (mDozing && mDozeParameters.shouldControlScreenOff()
- && visibleNotOccludedOrWillBe);
+ && keyguardVisibleOrWillBe);
mNotificationPanelViewController.setDozing(mDozing, animate);
updateQsExpansionEnabled();
@@ -3328,7 +3316,7 @@
@Override
public boolean onBackPressed() {
if (mStatusBarKeyguardViewManager.canHandleBackPressed()) {
- mStatusBarKeyguardViewManager.onBackPressed(false /* unused */);
+ mStatusBarKeyguardViewManager.onBackPressed();
return true;
}
if (mNotificationPanelViewController.isQsCustomizing()) {
@@ -3554,7 +3542,7 @@
public void setBouncerShowingOverDream(boolean bouncerShowingOverDream) {
mBouncerShowingOverDream = bouncerShowingOverDream;
}
-
+
/**
* Propagate the bouncer state to status bar components.
*
@@ -3585,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
@@ -3816,8 +3811,7 @@
if (mDevicePolicyManager.getCameraDisabled(null,
mLockscreenUserManager.getCurrentUserId())) {
return false;
- } else if (mStatusBarKeyguardViewManager == null
- || (isKeyguardShowing() && isKeyguardSecure())) {
+ } else if (isKeyguardShowing() && isKeyguardSecure()) {
// Check if the admin has disabled the camera specifically for the keyguard
return (mDevicePolicyManager.getKeyguardDisabledFeatures(null,
mLockscreenUserManager.getCurrentUserId())
@@ -3934,11 +3928,7 @@
@Override
public boolean isKeyguardShowing() {
- if (mStatusBarKeyguardViewManager == null) {
- Slog.i(TAG, "isKeyguardShowing() called before startKeyguard(), returning true");
- return true;
- }
- return mStatusBarKeyguardViewManager.isShowing();
+ return mKeyguardStateController.isShowing();
}
@Override
@@ -4229,14 +4219,6 @@
@Override
public boolean isKeyguardSecure() {
- if (mStatusBarKeyguardViewManager == null) {
- // startKeyguard() hasn't been called yet, so we don't know.
- // Make sure anything that needs to know isKeyguardSecure() checks and re-checks this
- // value onVisibilityChanged().
- Slog.w(TAG, "isKeyguardSecure() called before startKeyguard(), returning false",
- new Throwable());
- return false;
- }
return mStatusBarKeyguardViewManager.isSecure();
}
@Override
@@ -4296,11 +4278,6 @@
.Callback() {
@Override
public void onFinished() {
- if (mStatusBarKeyguardViewManager == null) {
- Log.w(TAG, "Tried to notify keyguard visibility when "
- + "mStatusBarKeyguardViewManager was null");
- return;
- }
if (mKeyguardStateController.isKeyguardFadingAway()) {
mStatusBarKeyguardViewManager.onKeyguardFadedAway();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index d61c51e..9bb4132 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -91,6 +91,11 @@
mBouncerPromptReason = mCallback.getBouncerPromptReason();
}
}
+
+ @Override
+ public void onNonStrongBiometricAllowedChanged(int userId) {
+ mBouncerPromptReason = mCallback.getBouncerPromptReason();
+ }
};
private final Runnable mRemoveViewRunnable = this::removeView;
private final KeyguardBypassController mKeyguardBypassController;
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/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
index e3e8572..5e26cf0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
@@ -47,7 +47,7 @@
private val asyncSensorManager: AsyncSensorManager,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val dumpManager: DumpManager
-) : Dumpable, CoreStartable(context) {
+) : Dumpable, CoreStartable {
private val pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
private var isListening = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
index 02b2354..4839fe6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
@@ -19,9 +19,9 @@
import android.util.DisplayMetrics
import android.view.View
import com.android.internal.logging.nano.MetricsProto.MetricsEvent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.LSShadeTransitionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
index 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/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 9f93223..8490768 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -249,6 +249,7 @@
private Callback mCallback;
private boolean mWallpaperSupportsAmbientMode;
private boolean mScreenOn;
+ private boolean mTransparentScrimBackground;
// Scrim blanking callbacks
private Runnable mPendingFrameCallback;
@@ -341,6 +342,8 @@
mScrimBehind.setDefaultFocusHighlightEnabled(false);
mNotificationsScrim.setDefaultFocusHighlightEnabled(false);
mScrimInFront.setDefaultFocusHighlightEnabled(false);
+ mTransparentScrimBackground = notificationsScrim.getResources()
+ .getBoolean(R.bool.notification_scrim_transparent);
updateScrims();
mKeyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback);
}
@@ -777,13 +780,16 @@
float behindFraction = getInterpolatedFraction();
behindFraction = (float) Math.pow(behindFraction, 0.8f);
if (mClipsQsScrim) {
- mBehindAlpha = 1;
- mNotificationsAlpha = behindFraction * mDefaultScrimAlpha;
+ mBehindAlpha = mTransparentScrimBackground ? 0 : 1;
+ mNotificationsAlpha =
+ mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha;
} else {
- mBehindAlpha = behindFraction * mDefaultScrimAlpha;
+ mBehindAlpha =
+ mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha;
// Delay fade-in of notification scrim a bit further, to coincide with the
// view fade in. Otherwise the empty panel can be quite jarring.
- mNotificationsAlpha = MathUtils.constrainedMap(0f, 1f, 0.3f, 0.75f,
+ mNotificationsAlpha = mTransparentScrimBackground
+ ? 0 : MathUtils.constrainedMap(0f, 1f, 0.3f, 0.75f,
mPanelExpansionFraction);
}
mBehindTint = mState.getBehindTint();
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 6e1cf13..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;
@@ -68,6 +67,9 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.shade.ShadeExpansionListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.NotificationMediaManager;
@@ -77,9 +79,6 @@
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
import com.android.systemui.statusbar.phone.KeyguardBouncer.BouncerExpansionCallback;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.unfold.FoldAodAnimationController;
@@ -103,7 +102,7 @@
@SysUISingleton
public class StatusBarKeyguardViewManager implements RemoteInputController.Callback,
StatusBarStateController.StateListener, ConfigurationController.ConfigurationListener,
- PanelExpansionListener, NavigationModeController.ModeChangedListener,
+ ShadeExpansionListener, NavigationModeController.ModeChangedListener,
KeyguardViewController, FoldAodAnimationController.FoldAodAnimationStatus {
// When hiding the Keyguard with timing supplied from WindowManager, better be early than late.
@@ -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() {
@@ -203,7 +202,7 @@
if (DEBUG) {
Log.d(TAG, "onBackInvokedCallback() called, invoking onBackPressed()");
}
- onBackPressed(false /* unused */);
+ onBackPressed();
};
private boolean mIsBackCallbackRegistered = false;
@@ -225,12 +224,11 @@
protected CentralSurfaces mCentralSurfaces;
private NotificationPanelViewController mNotificationPanelViewController;
private BiometricUnlockController mBiometricUnlockController;
+ private boolean mCentralSurfacesRegistered;
private View mNotificationContainer;
@Nullable protected KeyguardBouncer mBouncer;
- protected boolean mShowing;
- protected boolean mOccluded;
protected boolean mRemoteInputActive;
private boolean mGlobalActionsVisible = false;
private boolean mLastGlobalActionsVisible = false;
@@ -277,10 +275,9 @@
new KeyguardUpdateMonitorCallback() {
@Override
public void onEmergencyCallAction() {
-
// Since we won't get a setOccluded call we have to reset the view manually such that
// the bouncer goes away.
- if (mOccluded) {
+ if (mKeyguardStateController.isOccluded()) {
reset(true /* hideBouncerWhenShowing */);
}
}
@@ -329,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);
@@ -338,7 +335,7 @@
@Override
public void registerCentralSurfaces(CentralSurfaces centralSurfaces,
NotificationPanelViewController notificationPanelViewController,
- PanelExpansionStateManager panelExpansionStateManager,
+ ShadeExpansionStateManager shadeExpansionStateManager,
BiometricUnlockController biometricUnlockController,
View notificationContainer,
KeyguardBypassController bypassController) {
@@ -352,13 +349,14 @@
mBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
}
mNotificationPanelViewController = notificationPanelViewController;
- if (panelExpansionStateManager != null) {
- panelExpansionStateManager.addExpansionListener(this);
+ if (shadeExpansionStateManager != null) {
+ shadeExpansionStateManager.addExpansionListener(this);
}
mBypassController = bypassController;
mNotificationContainer = notificationContainer;
mKeyguardMessageAreaController = mKeyguardMessageAreaFactory.create(
centralSurfaces.getKeyguardMessageArea());
+ mCentralSurfacesRegistered = true;
registerListeners();
}
@@ -445,7 +443,7 @@
}
@Override
- public void onPanelExpansionChanged(PanelExpansionChangeEvent event) {
+ public void onPanelExpansionChanged(ShadeExpansionChangeEvent event) {
float fraction = event.getFraction();
boolean tracking = event.getTracking();
// Avoid having the shade and the bouncer open at the same time over a dream.
@@ -479,7 +477,7 @@
} else {
mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
}
- } else if (mShowing && !hideBouncerOverDream) {
+ } else if (mKeyguardStateController.isShowing() && !hideBouncerOverDream) {
if (!isWakeAndUnlocking()
&& !(mBiometricUnlockController.getMode() == MODE_DISMISS_BOUNCER)
&& !mCentralSurfaces.isInLaunchTransition()
@@ -500,7 +498,7 @@
mBouncerInteractor.show(/* isScrimmed= */false);
}
}
- } else if (!mShowing && isBouncerInTransit()) {
+ } else if (!mKeyguardStateController.isShowing() && isBouncerInTransit()) {
// Keyguard is not visible anymore, but expansion animation was still running.
// We need to hide the bouncer, otherwise it will be stuck in transit.
if (mBouncer != null) {
@@ -533,9 +531,8 @@
@Override
public void show(Bundle options) {
Trace.beginSection("StatusBarKeyguardViewManager#show");
- mShowing = true;
mNotificationShadeWindowController.setKeyguardShowing(true);
- mKeyguardStateController.notifyKeyguardState(mShowing,
+ mKeyguardStateController.notifyKeyguardState(true,
mKeyguardStateController.isOccluded());
reset(true /* hideBouncerWhenShowing */);
SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
@@ -599,7 +596,7 @@
} else {
mBouncerInteractor.hide();
}
- if (mShowing) {
+ if (mKeyguardStateController.isShowing()) {
// If we were showing the bouncer and then aborting, we need to also clear out any
// potential actions unless we actually unlocked.
cancelPostAuthActions();
@@ -616,7 +613,7 @@
public void showBouncer(boolean scrimmed) {
resetAlternateAuth(false);
- if (mShowing && !isBouncerShowing()) {
+ if (mKeyguardStateController.isShowing() && !isBouncerShowing()) {
if (mBouncer != null) {
mBouncer.show(false /* resetSecuritySelection */, scrimmed);
} else {
@@ -633,7 +630,7 @@
public void dismissWithAction(OnDismissAction r, Runnable cancelAction,
boolean afterKeyguardGone, String message) {
- if (mShowing) {
+ if (mKeyguardStateController.isShowing()) {
try {
Trace.beginSection("StatusBarKeyguardViewManager#dismissWithAction");
cancelPendingWakeupAction();
@@ -719,11 +716,12 @@
@Override
public void reset(boolean hideBouncerWhenShowing) {
- if (mShowing) {
+ if (mKeyguardStateController.isShowing()) {
+ final boolean isOccluded = mKeyguardStateController.isOccluded();
// Hide quick settings.
- mNotificationPanelViewController.resetViews(/* animate= */ !mOccluded);
+ mNotificationPanelViewController.resetViews(/* animate= */ !isOccluded);
// Hide bouncer and quick-quick settings.
- if (mOccluded && !mDozing) {
+ if (isOccluded && !mDozing) {
mCentralSurfaces.hideKeyguard();
if (hideBouncerWhenShowing || needsFullscreenBouncer()) {
hideBouncer(false /* destroyView */);
@@ -805,7 +803,8 @@
private void setDozing(boolean dozing) {
if (mDozing != dozing) {
mDozing = dozing;
- if (dozing || mBouncer.needsFullscreenBouncer() || mOccluded) {
+ if (dozing || needsFullscreenBouncer()
+ || mKeyguardStateController.isOccluded()) {
reset(dozing /* hideBouncerWhenShowing */);
}
updateStates();
@@ -838,18 +837,23 @@
@Override
public void setOccluded(boolean occluded, boolean animate) {
- final boolean isOccluding = !mOccluded && occluded;
- final boolean isUnOccluding = mOccluded && !occluded;
- setOccludedAndUpdateStates(occluded);
+ final boolean wasOccluded = mKeyguardStateController.isOccluded();
+ final boolean isOccluding = !wasOccluded && occluded;
+ final boolean isUnOccluding = wasOccluded && !occluded;
+ mKeyguardStateController.notifyKeyguardState(
+ mKeyguardStateController.isShowing(), occluded);
+ updateStates();
+ final boolean isShowing = mKeyguardStateController.isShowing();
+ final boolean isOccluded = mKeyguardStateController.isOccluded();
- if (mShowing && isOccluding) {
+ if (isShowing && isOccluding) {
SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__OCCLUDED);
if (mCentralSurfaces.isInLaunchTransition()) {
final Runnable endRunnable = new Runnable() {
@Override
public void run() {
- mNotificationShadeWindowController.setKeyguardOccluded(mOccluded);
+ mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
reset(true /* hideBouncerWhenShowing */);
}
};
@@ -864,19 +868,19 @@
// When isLaunchingActivityOverLockscreen() is true, we know for sure that the post
// collapse runnables will be run.
mShadeController.get().addPostCollapseAction(() -> {
- mNotificationShadeWindowController.setKeyguardOccluded(mOccluded);
+ mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
reset(true /* hideBouncerWhenShowing */);
});
return;
}
- } else if (mShowing && isUnOccluding) {
+ } else if (isShowing && isUnOccluding) {
SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__SHOWN);
}
- if (mShowing) {
- mMediaManager.updateMediaMetaData(false, animate && !mOccluded);
+ if (isShowing) {
+ mMediaManager.updateMediaMetaData(false, animate && !isOccluded);
}
- mNotificationShadeWindowController.setKeyguardOccluded(mOccluded);
+ mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
// setDozing(false) will call reset once we stop dozing. Also, if we're going away, there's
// no need to reset the keyguard views as we'll be gone shortly. Resetting now could cause
@@ -886,20 +890,11 @@
// by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
reset(isOccluding /* hideBouncerWhenShowing*/);
}
- if (animate && !mOccluded && mShowing && !bouncerIsShowing()) {
+ if (animate && !isOccluded && isShowing && !bouncerIsShowing()) {
mCentralSurfaces.animateKeyguardUnoccluding();
}
}
- private void setOccludedAndUpdateStates(boolean occluded) {
- mOccluded = occluded;
- updateStates();
- }
-
- public boolean isOccluded() {
- return mOccluded;
- }
-
@Override
public void startPreHideAnimation(Runnable finishRunnable) {
if (bouncerIsShowing()) {
@@ -930,8 +925,7 @@
@Override
public void hide(long startTime, long fadeoutDuration) {
Trace.beginSection("StatusBarKeyguardViewManager#hide");
- mShowing = false;
- mKeyguardStateController.notifyKeyguardState(mShowing,
+ mKeyguardStateController.notifyKeyguardState(false,
mKeyguardStateController.isOccluded());
launchPendingWakeupAction();
@@ -1080,11 +1074,6 @@
KeyguardUpdateMonitor.getCurrentUser()) != KeyguardSecurityModel.SecurityMode.None;
}
- @Override
- public boolean isShowing() {
- return mShowing;
- }
-
/**
* Returns whether a back invocation can be handled, which depends on whether the keyguard
* is currently showing (which itself is derived from multiple states).
@@ -1092,27 +1081,20 @@
* @return whether a back press can be handled right now.
*/
public boolean canHandleBackPressed() {
- return mBouncer.isShowing();
+ return bouncerIsShowing();
}
/**
* Notifies this manager that the back button has been pressed.
*/
- // TODO(b/244635782): This "accept boolean and ignore it, and always return false" was done
- // to make it possible to check this in *and* allow merging to master,
- // where ArcStatusBarKeyguardViewManager inherits this class, and its
- // build will break if we change this interface.
- // So, overall, while this function refactors the behavior of onBackPressed,
- // (it now handles the back press, and no longer returns *whether* it did so)
- // its interface is not changing right now (but will, in a follow-up CL).
- public boolean onBackPressed(boolean ignored) {
+ public void onBackPressed() {
if (!canHandleBackPressed()) {
- return false;
+ return;
}
mCentralSurfaces.endAffordanceLaunch();
// The second condition is for SIM card locked bouncer
- if (bouncerIsScrimmed() && needsFullscreenBouncer()) {
+ if (bouncerIsScrimmed() && !needsFullscreenBouncer()) {
hideBouncer(false);
updateStates();
} else {
@@ -1128,7 +1110,7 @@
mNotificationPanelViewController.expandWithoutQs();
}
}
- return false;
+ return;
}
@Override
@@ -1142,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();
}
@@ -1187,8 +1169,11 @@
};
protected void updateStates() {
- boolean showing = mShowing;
- boolean occluded = mOccluded;
+ if (!mCentralSurfacesRegistered) {
+ return;
+ }
+ boolean showing = mKeyguardStateController.isShowing();
+ boolean occluded = mKeyguardStateController.isOccluded();
boolean bouncerShowing = bouncerIsShowing();
boolean bouncerIsOrWillBeShowing = bouncerIsOrWillBeShowing();
boolean bouncerDismissible = !isFullscreenBouncer();
@@ -1222,13 +1207,6 @@
mNotificationShadeWindowController.setBouncerShowing(bouncerShowing);
mCentralSurfaces.setBouncerShowing(bouncerShowing);
}
-
- if (occluded != mLastOccluded || mFirstUpdate) {
- mKeyguardStateController.notifyKeyguardState(showing, occluded);
- }
- if (occluded != mLastOccluded || mShowing != showing || mFirstUpdate) {
- mKeyguardUpdateManager.setKeyguardShowing(showing, occluded);
- }
if (bouncerIsOrWillBeShowing != mLastBouncerIsOrWillBeShowing || mFirstUpdate
|| bouncerShowing != mLastBouncerShowing) {
mKeyguardUpdateManager.sendKeyguardBouncerChanged(bouncerIsOrWillBeShowing,
@@ -1279,12 +1257,12 @@
public boolean isNavBarVisible() {
boolean isWakeAndUnlockPulsing = mBiometricUnlockController != null
&& mBiometricUnlockController.getMode() == MODE_WAKE_AND_UNLOCK_PULSING;
- boolean keyguardShowing = mShowing && !mOccluded;
+ boolean keyguardVisible = mKeyguardStateController.isVisible();
boolean hideWhileDozing = mDozing && !isWakeAndUnlockPulsing;
- boolean keyguardWithGestureNav = (keyguardShowing && !mDozing && !mScreenOffAnimationPlaying
+ boolean keyguardWithGestureNav = (keyguardVisible && !mDozing && !mScreenOffAnimationPlaying
|| mPulsing && !mIsDocked)
&& mGesturalNav;
- return (!keyguardShowing && !hideWhileDozing && !mScreenOffAnimationPlaying
+ return (!keyguardVisible && !hideWhileDozing && !mScreenOffAnimationPlaying
|| bouncerIsShowing()
|| mRemoteInputActive
|| keyguardWithGestureNav
@@ -1306,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);
}
@@ -1323,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();
}
@@ -1416,7 +1394,7 @@
DismissWithActionRequest request = mPendingWakeupAction;
mPendingWakeupAction = null;
if (request != null) {
- if (mShowing) {
+ if (mKeyguardStateController.isShowing()) {
dismissWithAction(request.dismissAction, request.cancelAction,
request.afterKeyguardGone, request.message);
} else if (request.dismissAction != null) {
@@ -1435,10 +1413,10 @@
public boolean bouncerNeedsScrimming() {
// When a dream overlay is active, scrimming will cause any expansion to immediately expand.
- return (mOccluded && !mDreamOverlayStateController.isOverlayActive())
+ return (mKeyguardStateController.isOccluded()
+ && !mDreamOverlayStateController.isOverlayActive())
|| bouncerWillDismissWithAction()
- || (bouncerIsShowing()
- && bouncerIsScrimmed())
+ || (bouncerIsShowing() && bouncerIsScrimmed())
|| isFullscreenBouncer();
}
@@ -1457,8 +1435,6 @@
public void dump(PrintWriter pw) {
pw.println("StatusBarKeyguardViewManager:");
- pw.println(" mShowing: " + mShowing);
- pw.println(" mOccluded: " + mOccluded);
pw.println(" mRemoteInputActive: " + mRemoteInputActive);
pw.println(" mDozing: " + mDozing);
pw.println(" mAfterKeyguardGoneAction: " + mAfterKeyguardGoneAction);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
index b9a1413..81edff4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
@@ -17,12 +17,12 @@
package com.android.systemui.statusbar.phone
import android.app.PendingIntent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 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/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 627cfb7..dc90266 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -41,6 +41,7 @@
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.NotificationShadeWindowView;
import com.android.systemui.shade.NotificationsQuickSettingsContainer;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.NotificationShelfController;
@@ -64,7 +65,6 @@
import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragmentLogger;
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -284,7 +284,7 @@
SystemStatusAnimationScheduler animationScheduler,
StatusBarLocationPublisher locationPublisher,
NotificationIconAreaController notificationIconAreaController,
- PanelExpansionStateManager panelExpansionStateManager,
+ ShadeExpansionStateManager shadeExpansionStateManager,
FeatureFlags featureFlags,
StatusBarIconController statusBarIconController,
StatusBarIconController.DarkIconManager.Factory darkIconManagerFactory,
@@ -306,7 +306,7 @@
animationScheduler,
locationPublisher,
notificationIconAreaController,
- panelExpansionStateManager,
+ shadeExpansionStateManager,
featureFlags,
statusBarIconController,
darkIconManagerFactory,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index e1215ee..9f3fd72 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -53,6 +53,7 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.DisableFlagsLogger.DisableState;
import com.android.systemui.statusbar.OperatorNameView;
@@ -74,7 +75,6 @@
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent.Startable;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.EncryptionHelper;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.CarrierConfigTracker;
@@ -127,7 +127,7 @@
private final StatusBarLocationPublisher mLocationPublisher;
private final FeatureFlags mFeatureFlags;
private final NotificationIconAreaController mNotificationIconAreaController;
- private final PanelExpansionStateManager mPanelExpansionStateManager;
+ private final ShadeExpansionStateManager mShadeExpansionStateManager;
private final StatusBarIconController mStatusBarIconController;
private final CarrierConfigTracker mCarrierConfigTracker;
private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
@@ -184,7 +184,7 @@
SystemStatusAnimationScheduler animationScheduler,
StatusBarLocationPublisher locationPublisher,
NotificationIconAreaController notificationIconAreaController,
- PanelExpansionStateManager panelExpansionStateManager,
+ ShadeExpansionStateManager shadeExpansionStateManager,
FeatureFlags featureFlags,
StatusBarIconController statusBarIconController,
StatusBarIconController.DarkIconManager.Factory darkIconManagerFactory,
@@ -206,7 +206,7 @@
mAnimationScheduler = animationScheduler;
mLocationPublisher = locationPublisher;
mNotificationIconAreaController = notificationIconAreaController;
- mPanelExpansionStateManager = panelExpansionStateManager;
+ mShadeExpansionStateManager = shadeExpansionStateManager;
mFeatureFlags = featureFlags;
mStatusBarIconController = statusBarIconController;
mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager;
@@ -490,7 +490,7 @@
}
private boolean shouldHideNotificationIcons() {
- if (!mPanelExpansionStateManager.isClosed()
+ if (!mShadeExpansionStateManager.isClosed()
&& mNotificationPanelViewController.hideStatusBarIconsWhenExpanded()) {
return true;
}
@@ -536,7 +536,7 @@
* don't set the clock GONE otherwise it'll mess up the animation.
*/
private int clockHiddenMode() {
- if (!mPanelExpansionStateManager.isClosed() && !mKeyguardStateController.isShowing()
+ if (!mShadeExpansionStateManager.isClosed() && !mKeyguardStateController.isShowing()
&& !mStatusBarStateController.isDozing()) {
return View.INVISIBLE;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
index 9ae378f..0e6b7f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.phone.fragment
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.CollapsedSbFragmentLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.statusbar.DisableFlagsLogger
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
index 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..fcd1b8a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -16,10 +16,16 @@
package com.android.systemui.statusbar.pipeline.dagger
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryImpl
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
@@ -30,16 +36,25 @@
@Module
abstract class StatusBarPipelineModule {
@Binds
+ abstract fun airplaneModeRepository(impl: AirplaneModeRepositoryImpl): AirplaneModeRepository
+
+ @Binds
abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
@Binds
abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
@Binds
- abstract fun mobileSubscriptionRepository(
- impl: MobileSubscriptionRepositoryImpl
- ): MobileSubscriptionRepository
+ abstract fun mobileConnectionsRepository(
+ impl: MobileConnectionsRepositoryImpl
+ ): MobileConnectionsRepository
@Binds
abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
+
+ @Binds
+ abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy
+
+ @Binds
+ abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
index 46ccf32c..eaba0e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
@@ -27,6 +27,7 @@
import android.telephony.TelephonyCallback.SignalStrengthsListener
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
/**
* Data class containing all of the relevant information for a particular line of service, known as
@@ -57,6 +58,11 @@
/** From [CarrierNetworkListener.onCarrierNetworkChange] */
val carrierNetworkChangeActive: Boolean? = null,
- /** From [DisplayInfoListener.onDisplayInfoChanged] */
- val displayInfo: TelephonyDisplayInfo? = null
+ /**
+ * From [DisplayInfoListener.onDisplayInfoChanged].
+ *
+ * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or
+ * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon
+ */
+ val resolvedNetworkType: ResolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN),
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
new file mode 100644
index 0000000..f385806
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.model
+
+import android.telephony.Annotation.NetworkType
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+
+/**
+ * A SysUI type to represent the [NetworkType] that we pull out of [TelephonyDisplayInfo]. Depending
+ * on whether or not the display info contains an override type, we may have to call different
+ * methods on [MobileMappingsProxy] to generate an icon lookup key.
+ */
+sealed interface ResolvedNetworkType {
+ @NetworkType val type: Int
+}
+
+data class DefaultNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType
+
+data class OverrideNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
new file mode 100644
index 0000000..45284cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.CellSignalStrength
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
+import android.telephony.TelephonyManager
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import java.lang.IllegalStateException
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a
+ * repository for each individual, tracked subscription via [MobileConnectionsRepository], and this
+ * repository is responsible for setting up a [TelephonyManager] object tied to its subscriptionId
+ *
+ * There should only ever be one [MobileConnectionRepository] per subscription, since
+ * [TelephonyManager] limits the number of callbacks that can be registered per process.
+ *
+ * This repository should have all of the relevant information for a single line of service, which
+ * eventually becomes a single icon in the status bar.
+ */
+interface MobileConnectionRepository {
+ /**
+ * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single
+ * listener + model.
+ */
+ val subscriptionModelFlow: Flow<MobileSubscriptionModel>
+}
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class MobileConnectionRepositoryImpl(
+ private val subId: Int,
+ telephonyManager: TelephonyManager,
+ bgDispatcher: CoroutineDispatcher,
+ logger: ConnectivityPipelineLogger,
+ scope: CoroutineScope,
+) : MobileConnectionRepository {
+ init {
+ if (telephonyManager.subscriptionId != subId) {
+ throw IllegalStateException(
+ "TelephonyManager should be created with subId($subId). " +
+ "Found ${telephonyManager.subscriptionId} instead."
+ )
+ }
+ }
+
+ override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
+ var state = MobileSubscriptionModel()
+ conflatedCallbackFlow {
+ // TODO (b/240569788): log all of these into the connectivity logger
+ val callback =
+ object :
+ TelephonyCallback(),
+ TelephonyCallback.ServiceStateListener,
+ TelephonyCallback.SignalStrengthsListener,
+ TelephonyCallback.DataConnectionStateListener,
+ TelephonyCallback.DataActivityListener,
+ TelephonyCallback.CarrierNetworkListener,
+ TelephonyCallback.DisplayInfoListener {
+ override fun onServiceStateChanged(serviceState: ServiceState) {
+ state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
+ trySend(state)
+ }
+
+ override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
+ val cdmaLevel =
+ signalStrength
+ .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
+ .let { strengths ->
+ if (!strengths.isEmpty()) {
+ strengths[0].level
+ } else {
+ CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
+ }
+ }
+
+ val primaryLevel = signalStrength.level
+
+ state =
+ state.copy(
+ cdmaLevel = cdmaLevel,
+ primaryLevel = primaryLevel,
+ isGsm = signalStrength.isGsm,
+ )
+ trySend(state)
+ }
+
+ override fun onDataConnectionStateChanged(
+ dataState: Int,
+ networkType: Int
+ ) {
+ state = state.copy(dataConnectionState = dataState)
+ trySend(state)
+ }
+
+ override fun onDataActivity(direction: Int) {
+ state = state.copy(dataActivityDirection = direction)
+ trySend(state)
+ }
+
+ override fun onCarrierNetworkChange(active: Boolean) {
+ state = state.copy(carrierNetworkChangeActive = active)
+ trySend(state)
+ }
+
+ override fun onDisplayInfoChanged(
+ telephonyDisplayInfo: TelephonyDisplayInfo
+ ) {
+ val networkType =
+ if (
+ telephonyDisplayInfo.overrideNetworkType ==
+ OVERRIDE_NETWORK_TYPE_NONE
+ ) {
+ DefaultNetworkType(telephonyDisplayInfo.networkType)
+ } else {
+ OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType)
+ }
+ state = state.copy(resolvedNetworkType = networkType)
+ trySend(state)
+ }
+ }
+ telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+ awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+ }
+ .onEach { logger.logOutputChange("mobileSubscriptionModel", it.toString()) }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), state)
+ }
+
+ class Factory
+ @Inject
+ constructor(
+ private val telephonyManager: TelephonyManager,
+ private val logger: ConnectivityPipelineLogger,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ @Application private val scope: CoroutineScope,
+ ) {
+ fun build(subId: Int): MobileConnectionRepository {
+ return MobileConnectionRepositoryImpl(
+ subId,
+ telephonyManager.createForSubscriptionId(subId),
+ bgDispatcher,
+ logger,
+ scope,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
new file mode 100644
index 0000000..0e2428a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.content.Context
+import android.content.IntentFilter
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyManager
+import androidx.annotation.VisibleForTesting
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/**
+ * Repo for monitoring the complete active subscription info list, to be consumed and filtered based
+ * on various policy
+ */
+interface MobileConnectionsRepository {
+ /** Observable list of current mobile subscriptions */
+ val subscriptionsFlow: Flow<List<SubscriptionInfo>>
+
+ /** Observable for the subscriptionId of the current mobile data connection */
+ val activeMobileDataSubscriptionId: Flow<Int>
+
+ /** Observable for [MobileMappings.Config] tracking the defaults */
+ val defaultDataSubRatConfig: StateFlow<Config>
+
+ /** Get or create a repository for the line of service for the given subscription ID */
+ fun getRepoForSubId(subId: Int): MobileConnectionRepository
+}
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MobileConnectionsRepositoryImpl
+@Inject
+constructor(
+ private val subscriptionManager: SubscriptionManager,
+ private val telephonyManager: TelephonyManager,
+ private val logger: ConnectivityPipelineLogger,
+ broadcastDispatcher: BroadcastDispatcher,
+ private val context: Context,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ @Application private val scope: CoroutineScope,
+ private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
+) : MobileConnectionsRepository {
+ private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+
+ /**
+ * State flow that emits the set of mobile data subscriptions, each represented by its own
+ * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
+ * info object, but for now we keep track of the infos themselves.
+ */
+ override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
+ conflatedCallbackFlow {
+ val callback =
+ object : SubscriptionManager.OnSubscriptionsChangedListener() {
+ override fun onSubscriptionsChanged() {
+ trySend(Unit)
+ }
+ }
+
+ subscriptionManager.addOnSubscriptionsChangedListener(
+ bgDispatcher.asExecutor(),
+ callback,
+ )
+
+ awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
+ }
+ .mapLatest { fetchSubscriptionsList() }
+ .onEach { infos -> dropUnusedReposFromCache(infos) }
+ .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
+
+ /** StateFlow that keeps track of the current active mobile data subscription */
+ override val activeMobileDataSubscriptionId: StateFlow<Int> =
+ conflatedCallbackFlow {
+ val callback =
+ object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
+ override fun onActiveDataSubscriptionIdChanged(subId: Int) {
+ trySend(subId)
+ }
+ }
+
+ telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+ awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+ }
+ .stateIn(
+ scope,
+ started = SharingStarted.WhileSubscribed(),
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ )
+
+ private val defaultDataSubChangedEvent =
+ broadcastDispatcher.broadcastFlow(
+ IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+ )
+
+ private val carrierConfigChangedEvent =
+ broadcastDispatcher.broadcastFlow(
+ IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
+ )
+
+ /**
+ * [Config] is an object that tracks relevant configuration flags for a given subscription ID.
+ * In the case of [MobileMappings], it's hard-coded to check the default data subscription's
+ * config, so this will apply to every icon that we care about.
+ *
+ * Relevant bits in the config are things like
+ * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL]
+ *
+ * This flow will produce whenever the default data subscription or the carrier config changes.
+ */
+ override val defaultDataSubRatConfig: StateFlow<Config> =
+ combine(defaultDataSubChangedEvent, carrierConfigChangedEvent) { _, _ ->
+ Config.readConfig(context)
+ }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ initialValue = Config.readConfig(context)
+ )
+
+ override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+ if (!isValidSubId(subId)) {
+ throw IllegalArgumentException(
+ "subscriptionId $subId is not in the list of valid subscriptions"
+ )
+ }
+
+ return subIdRepositoryCache[subId]
+ ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
+ }
+
+ private fun isValidSubId(subId: Int): Boolean {
+ subscriptionsFlow.value.forEach {
+ if (it.subscriptionId == subId) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
+
+ private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
+ return mobileConnectionRepositoryFactory.build(subId)
+ }
+
+ private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) {
+ // Remove any connection repository from the cache that isn't in the new set of IDs. They
+ // will get garbage collected once their subscribers go away
+ val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
+
+ subIdRepositoryCache.keys.forEach {
+ if (!currentValidSubscriptionIds.contains(it)) {
+ subIdRepositoryCache.remove(it)
+ }
+ }
+ }
+
+ private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
+ withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt
deleted file mode 100644
index 36de2a2..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt
+++ /dev/null
@@ -1,210 +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.statusbar.pipeline.mobile.data.repository
-
-import android.telephony.CellSignalStrength
-import android.telephony.CellSignalStrengthCdma
-import android.telephony.ServiceState
-import android.telephony.SignalStrength
-import android.telephony.SubscriptionInfo
-import android.telephony.SubscriptionManager
-import android.telephony.TelephonyCallback
-import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
-import android.telephony.TelephonyCallback.CarrierNetworkListener
-import android.telephony.TelephonyCallback.DataActivityListener
-import android.telephony.TelephonyCallback.DataConnectionStateListener
-import android.telephony.TelephonyCallback.DisplayInfoListener
-import android.telephony.TelephonyCallback.ServiceStateListener
-import android.telephony.TelephonyCallback.SignalStrengthsListener
-import android.telephony.TelephonyDisplayInfo
-import android.telephony.TelephonyManager
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.asExecutor
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
-
-/**
- * Repo for monitoring the complete active subscription info list, to be consumed and filtered based
- * on various policy
- */
-interface MobileSubscriptionRepository {
- /** Observable list of current mobile subscriptions */
- val subscriptionsFlow: Flow<List<SubscriptionInfo>>
-
- /** Observable for the subscriptionId of the current mobile data connection */
- val activeMobileDataSubscriptionId: Flow<Int>
-
- /** Get or create an observable for the given subscription ID */
- fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel>
-}
-
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
-@SysUISingleton
-class MobileSubscriptionRepositoryImpl
-@Inject
-constructor(
- private val subscriptionManager: SubscriptionManager,
- private val telephonyManager: TelephonyManager,
- @Background private val bgDispatcher: CoroutineDispatcher,
- @Application private val scope: CoroutineScope,
-) : MobileSubscriptionRepository {
- private val subIdFlowCache: MutableMap<Int, StateFlow<MobileSubscriptionModel>> = mutableMapOf()
-
- /**
- * State flow that emits the set of mobile data subscriptions, each represented by its own
- * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
- * info object, but for now we keep track of the infos themselves.
- */
- override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
- conflatedCallbackFlow {
- val callback =
- object : SubscriptionManager.OnSubscriptionsChangedListener() {
- override fun onSubscriptionsChanged() {
- trySend(Unit)
- }
- }
-
- subscriptionManager.addOnSubscriptionsChangedListener(
- bgDispatcher.asExecutor(),
- callback,
- )
-
- awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
- }
- .mapLatest { fetchSubscriptionsList() }
- .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
-
- /** StateFlow that keeps track of the current active mobile data subscription */
- override val activeMobileDataSubscriptionId: StateFlow<Int> =
- conflatedCallbackFlow {
- val callback =
- object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
- override fun onActiveDataSubscriptionIdChanged(subId: Int) {
- trySend(subId)
- }
- }
-
- telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
- awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
- }
- .stateIn(
- scope,
- started = SharingStarted.WhileSubscribed(),
- SubscriptionManager.INVALID_SUBSCRIPTION_ID
- )
-
- /**
- * Each mobile subscription needs its own flow, which comes from registering listeners on the
- * system. Use this method to create those flows and cache them for reuse
- */
- override fun getFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> {
- return subIdFlowCache[subId]
- ?: createFlowForSubId(subId).also { subIdFlowCache[subId] = it }
- }
-
- @VisibleForTesting fun getSubIdFlowCache() = subIdFlowCache
-
- private fun createFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> = run {
- var state = MobileSubscriptionModel()
- conflatedCallbackFlow {
- val phony = telephonyManager.createForSubscriptionId(subId)
- // TODO (b/240569788): log all of these into the connectivity logger
- val callback =
- object :
- TelephonyCallback(),
- ServiceStateListener,
- SignalStrengthsListener,
- DataConnectionStateListener,
- DataActivityListener,
- CarrierNetworkListener,
- DisplayInfoListener {
- override fun onServiceStateChanged(serviceState: ServiceState) {
- state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
- trySend(state)
- }
- override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
- val cdmaLevel =
- signalStrength
- .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
- .let { strengths ->
- if (!strengths.isEmpty()) {
- strengths[0].level
- } else {
- CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
- }
- }
-
- val primaryLevel = signalStrength.level
-
- state =
- state.copy(
- cdmaLevel = cdmaLevel,
- primaryLevel = primaryLevel,
- isGsm = signalStrength.isGsm,
- )
- trySend(state)
- }
- override fun onDataConnectionStateChanged(
- dataState: Int,
- networkType: Int
- ) {
- state = state.copy(dataConnectionState = dataState)
- trySend(state)
- }
- override fun onDataActivity(direction: Int) {
- state = state.copy(dataActivityDirection = direction)
- trySend(state)
- }
- override fun onCarrierNetworkChange(active: Boolean) {
- state = state.copy(carrierNetworkChangeActive = active)
- trySend(state)
- }
- override fun onDisplayInfoChanged(
- telephonyDisplayInfo: TelephonyDisplayInfo
- ) {
- state = state.copy(displayInfo = telephonyDisplayInfo)
- trySend(state)
- }
- }
- phony.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
- awaitClose {
- phony.unregisterTelephonyCallback(callback)
- // Release the cached flow
- subIdFlowCache.remove(subId)
- }
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), state)
- }
-
- private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
- withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 40fe0f3..15f4acc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -17,32 +17,58 @@
package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
import android.telephony.CarrierConfigManager
-import com.android.settingslib.SignalIcon
-import com.android.settingslib.mobile.TelephonyIcons
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
interface MobileIconInteractor {
- /** Identifier for RAT type indicator */
- val iconGroup: Flow<SignalIcon.MobileIconGroup>
+ /** Observable for RAT type (network type) indicator */
+ val networkTypeIconGroup: Flow<MobileIconGroup>
+
/** True if this line of service is emergency-only */
val isEmergencyOnly: Flow<Boolean>
+
/** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */
val level: Flow<Int>
+
/** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */
val numberOfLevels: Flow<Int>
+
/** True when we want to draw an icon that makes room for the exclamation mark */
val cutOut: Flow<Boolean>
}
/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
class MobileIconInteractorImpl(
- mobileStatusInfo: Flow<MobileSubscriptionModel>,
+ defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>,
+ defaultMobileIconGroup: Flow<MobileIconGroup>,
+ mobileMappingsProxy: MobileMappingsProxy,
+ connectionRepository: MobileConnectionRepository,
) : MobileIconInteractor {
- override val iconGroup: Flow<SignalIcon.MobileIconGroup> = flowOf(TelephonyIcons.THREE_G)
+ private val mobileStatusInfo = connectionRepository.subscriptionModelFlow
+
+ /** Observable for the current RAT indicator icon ([MobileIconGroup]) */
+ override val networkTypeIconGroup: Flow<MobileIconGroup> =
+ combine(
+ mobileStatusInfo,
+ defaultMobileIconMapping,
+ defaultMobileIconGroup,
+ ) { info, mapping, defaultGroup ->
+ val lookupKey =
+ when (val resolved = info.resolvedNetworkType) {
+ is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type)
+ is OverrideNetworkType -> mobileMappingsProxy.toIconKeyOverride(resolved.type)
+ }
+ mapping[lookupKey] ?: defaultGroup
+ }
+
override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly }
override val level: Flow<Int> =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index 8e67e19..cd411a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -19,29 +19,51 @@
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
/**
- * Business layer logic for mobile subscription icons
+ * Business layer logic for the set of mobile subscription icons.
*
- * Mobile indicators represent the UI for the (potentially filtered) list of [SubscriptionInfo]s
- * that the system knows about. They obey policy that depends on OEM, carrier, and locale configs
+ * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]).
+ * The list of subscriptions is filtered based on the opportunistic flags on the infos.
+ *
+ * It provides the default mapping between the telephony display info and the icon group that
+ * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual
+ * icon
*/
+interface MobileIconsInteractor {
+ val filteredSubscriptions: Flow<List<SubscriptionInfo>>
+ val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>
+ val defaultMobileIconGroup: Flow<MobileIconGroup>
+ val isUserSetup: Flow<Boolean>
+ fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor
+}
+
@SysUISingleton
-class MobileIconsInteractor
+class MobileIconsInteractorImpl
@Inject
constructor(
- private val mobileSubscriptionRepo: MobileSubscriptionRepository,
+ private val mobileSubscriptionRepo: MobileConnectionsRepository,
private val carrierConfigTracker: CarrierConfigTracker,
+ private val mobileMappingsProxy: MobileMappingsProxy,
userSetupRepo: UserSetupRepository,
-) {
+ @Application private val scope: CoroutineScope,
+) : MobileIconsInteractor {
private val activeMobileDataSubscriptionId =
mobileSubscriptionRepo.activeMobileDataSubscriptionId
@@ -61,7 +83,7 @@
* [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
* and by checking which subscription is opportunistic, or which one is active.
*/
- val filteredSubscriptions: Flow<List<SubscriptionInfo>> =
+ override val filteredSubscriptions: Flow<List<SubscriptionInfo>> =
combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId
->
// Based on the old logic,
@@ -92,15 +114,29 @@
}
}
- val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow
+ /**
+ * Mapping from network type to [MobileIconGroup] using the config generated for the default
+ * subscription Id. This mapping is the same for every subscription.
+ */
+ override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> =
+ mobileSubscriptionRepo.defaultDataSubRatConfig
+ .map { mobileMappingsProxy.mapIconSets(it) }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
+
+ /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
+ override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
+ mobileSubscriptionRepo.defaultDataSubRatConfig
+ .map { mobileMappingsProxy.getDefaultIcons(it) }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G)
+
+ override val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow
/** Vends out new [MobileIconInteractor] for a particular subId */
- fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
- MobileIconInteractorImpl(mobileSubscriptionFlowForSubId(subId))
-
- /**
- * Create a new flow for a given subscription ID, which usually maps 1:1 with mobile connections
- */
- private fun mobileSubscriptionFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> =
- mobileSubscriptionRepo.getFlowForSubId(subId)
+ override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
+ MobileIconInteractorImpl(
+ defaultMobileIconMapping,
+ defaultMobileIconGroup,
+ mobileMappingsProxy,
+ mobileSubscriptionRepo.getRepoForSubId(subId),
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index 1405b05..67ea139 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -17,6 +17,8 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.binder
import android.content.res.ColorStateList
+import android.view.View.GONE
+import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.view.isVisible
@@ -24,6 +26,7 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.settingslib.graph.SignalDrawable
import com.android.systemui.R
+import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
import kotlinx.coroutines.flow.collect
@@ -37,6 +40,7 @@
view: ViewGroup,
viewModel: MobileIconViewModel,
) {
+ val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
@@ -52,10 +56,20 @@
}
}
+ // Set the network type icon
+ launch {
+ viewModel.networkTypeIcon.distinctUntilChanged().collect { dataTypeId ->
+ dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) }
+ networkTypeView.visibility = if (dataTypeId != null) VISIBLE else GONE
+ }
+ }
+
// Set the tint
launch {
viewModel.tint.collect { tint ->
- iconView.imageTintList = ColorStateList.valueOf(tint)
+ val tintList = ColorStateList.valueOf(tint)
+ iconView.imageTintList = tintList
+ networkTypeView.imageTintList = tintList
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index cfabeba..cc8f6dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -18,6 +18,8 @@
import android.graphics.Color
import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
@@ -26,6 +28,7 @@
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
/**
* View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
@@ -54,5 +57,15 @@
.distinctUntilChanged()
.logOutputChange(logger, "iconId($subscriptionId)")
+ /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
+ var networkTypeIcon: Flow<Icon?> =
+ iconInteractor.networkTypeIconGroup.map {
+ val desc =
+ if (it.dataContentDescription != 0)
+ ContentDescription.Resource(it.dataContentDescription)
+ else null
+ Icon.Resource(it.dataType, desc)
+ }
+
var tint: Flow<Int> = flowOf(Color.CYAN)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt
new file mode 100644
index 0000000..60bd038
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.util
+
+import android.telephony.Annotation.NetworkType
+import android.telephony.TelephonyDisplayInfo
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.MobileMappings.Config
+import javax.inject.Inject
+
+/**
+ * [MobileMappings] owns the logic on creating the map from [TelephonyDisplayInfo] to
+ * [MobileIconGroup]. It creates that hash map and also manages the creation of lookup keys. This
+ * interface allows us to proxy those calls to the static java methods in SettingsLib and also fake
+ * them out in tests
+ */
+interface MobileMappingsProxy {
+ fun mapIconSets(config: Config): Map<String, MobileIconGroup>
+ fun getDefaultIcons(config: Config): MobileIconGroup
+ fun toIconKey(@NetworkType networkType: Int): String
+ fun toIconKeyOverride(@NetworkType networkType: Int): String
+}
+
+/** Injectable wrapper class for [MobileMappings] */
+class MobileMappingsProxyImpl @Inject constructor() : MobileMappingsProxy {
+ override fun mapIconSets(config: Config): Map<String, MobileIconGroup> =
+ MobileMappings.mapIconSets(config)
+
+ override fun getDefaultIcons(config: Config): MobileIconGroup =
+ MobileMappings.getDefaultIcons(config)
+
+ override fun toIconKey(@NetworkType networkType: Int): String =
+ MobileMappings.toIconKey(networkType)
+
+ override fun toIconKeyOverride(networkType: Int): String =
+ MobileMappings.toDisplayIconKey(networkType)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
index dbb1aa5..d3cf32f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -18,10 +18,10 @@
import android.net.Network
import android.net.NetworkCapabilities
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.StatusBarConnectivityLog
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
index c96faab..062c3d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
@@ -21,7 +21,9 @@
/** Provides information about the current wifi network. */
sealed class WifiNetworkModel {
/** A model representing that we have no active wifi network. */
- object Inactive : WifiNetworkModel()
+ object Inactive : WifiNetworkModel() {
+ override fun toString() = "WifiNetwork.Inactive"
+ }
/**
* A model representing that our wifi network is actually a carrier merged network, meaning it's
@@ -29,7 +31,9 @@
*
* See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
*/
- object CarrierMerged : WifiNetworkModel()
+ object CarrierMerged : WifiNetworkModel() {
+ override fun toString() = "WifiNetwork.CarrierMerged"
+ }
/** Provides information about an active wifi network. */
data class Active(
@@ -72,6 +76,24 @@
}
}
+ override fun toString(): String {
+ // Only include the passpoint-related values in the string if we have them. (Most
+ // networks won't have them so they'll be mostly clutter.)
+ val passpointString =
+ if (isPasspointAccessPoint ||
+ isOnlineSignUpForPasspointAccessPoint ||
+ passpointProviderFriendlyName != null) {
+ ", isPasspointAp=$isPasspointAccessPoint, " +
+ "isOnlineSignUpForPasspointAp=$isOnlineSignUpForPasspointAccessPoint, " +
+ "passpointName=$passpointProviderFriendlyName"
+ } else {
+ ""
+ }
+
+ return "WifiNetworkModel.Active(networkId=$networkId, isValidated=$isValidated, " +
+ "level=$level, ssid=$ssid$passpointString)"
+ }
+
companion object {
@VisibleForTesting
internal const val MIN_VALID_LEVEL = 0
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/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
index 5b2d695..cf4106c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -35,19 +35,15 @@
protected val controller: UserSwitcherController,
) : BaseAdapter() {
- protected open val users: ArrayList<UserRecord>
- get() = controller.users
+ protected open val users: List<UserRecord>
+ get() = controller.users.filter { !controller.isKeyguardShowing || !it.isRestricted }
init {
controller.addAdapter(WeakReference(this))
}
override fun getCount(): Int {
- return if (controller.isKeyguardShowing) {
- users.count { !it.isRestricted }
- } else {
- users.size
- }
+ return users.size
}
override fun getItem(position: Int): UserRecord {
@@ -65,7 +61,7 @@
* animation to and from the parent dialog.
*/
@JvmOverloads
- fun onUserListItemClicked(
+ open fun onUserListItemClicked(
record: UserRecord,
dialogShower: DialogShower? = null,
) {
@@ -112,6 +108,7 @@
item.isGuest,
item.isAddSupervisedUser,
isTablet,
+ item.isManageUsers,
)
return checkNotNull(context.getDrawable(iconRes))
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index d7c81af..df1e80b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -16,10 +16,10 @@
package com.android.systemui.statusbar.policy
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.VERBOSE
import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
index 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/KeyguardStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
index 250d9d4..1ae1eae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
@@ -35,9 +35,16 @@
}
/**
- * If the lock screen is visible.
- * The keyguard is also visible when the device is asleep or in always on mode, except when
- * the screen timed out and the user can unlock by quickly pressing power.
+ * If the keyguard is visible. This is unrelated to being locked or not.
+ */
+ default boolean isVisible() {
+ return isShowing() && !isOccluded();
+ }
+
+ /**
+ * If the keyguard is showing. This includes when it's occluded by an activity, and when
+ * the device is asleep or in always on mode, except when the screen timed out and the user
+ * can unlock by quickly pressing power.
*
* This is unrelated to being locked or not.
*
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
index 437d4d4..cc6fdcc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -181,6 +181,7 @@
if (mShowing == showing && mOccluded == occluded) return;
mShowing = showing;
mOccluded = occluded;
+ mKeyguardUpdateMonitor.setKeyguardShowing(showing, occluded);
Trace.instantForTrack(Trace.TRACE_TAG_APP, "UI Events",
"Keyguard showing: " + showing + " occluded: " + occluded);
notifyKeyguardChanged();
@@ -387,6 +388,8 @@
@Override
public void dump(PrintWriter pw, String[] args) {
pw.println("KeyguardStateController:");
+ pw.println(" mShowing: " + mShowing);
+ pw.println(" mOccluded: " + mOccluded);
pw.println(" mSecure: " + mSecure);
pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen);
pw.println(" mTrustManaged: " + mTrustManaged);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
index 494a4bb..c150654 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
@@ -53,6 +53,7 @@
import com.android.systemui.util.ViewController;
import java.util.ArrayList;
+import java.util.List;
import javax.inject.Inject;
@@ -456,7 +457,7 @@
}
void refreshUserOrder() {
- ArrayList<UserRecord> users = super.getUsers();
+ List<UserRecord> users = super.getUsers();
mUsersOrdered = new ArrayList<>(users.size());
for (int i = 0; i < users.size(); i++) {
UserRecord record = users.get(i);
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/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
index af39eee..935fc7f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
@@ -249,7 +249,7 @@
override fun startActivity(intent: Intent) {
if (useInteractor) {
- activityStarter.startActivity(intent, /* dismissShade= */ false)
+ activityStarter.startActivity(intent, /* dismissShade= */ true)
} else {
_oldImpl.startActivity(intent)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
index 46d2f3a..c294c37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
@@ -49,6 +49,7 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.LatencyTracker;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.settingslib.users.UserCreatingDialog;
import com.android.systemui.GuestResetOrExitSessionReceiver;
import com.android.systemui.GuestResumeSessionReceiver;
@@ -399,6 +400,16 @@
records.add(userRecord);
}
+ if (canManageUsers()) {
+ records.add(LegacyUserDataHelper.createRecord(
+ mContext,
+ KeyguardUpdateMonitor.getCurrentUser(),
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ /* isRestricted= */ false,
+ /* isSwitchToEnabled= */ true
+ ));
+ }
+
mUiExecutor.execute(() -> {
if (records != null) {
mUsers = records;
@@ -438,6 +449,14 @@
&& mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY);
}
+ @VisibleForTesting
+ boolean canManageUsers() {
+ UserInfo currentUser = mUserTracker.getUserInfo();
+ return mUserSwitcherEnabled
+ && ((currentUser != null && currentUser.isAdmin())
+ || mAddUsersFromLockScreen.getValue());
+ }
+
private boolean createIsRestricted() {
return !mAddUsersFromLockScreen.getValue();
}
@@ -525,6 +544,8 @@
showAddUserDialog(dialogShower);
} else if (record.isAddSupervisedUser) {
startSupervisedUserActivity();
+ } else if (record.isManageUsers) {
+ startActivity(new Intent(Settings.ACTION_USER_SETTINGS));
} else {
onUserListItemClicked(record.info.id, record, dialogShower);
}
@@ -984,7 +1005,7 @@
@Override
public void startActivity(Intent intent) {
- mActivityStarter.startActivity(intent, true);
+ mActivityStarter.startActivity(intent, /* dismissShade= */ true);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
index 09298b6..b1b8341 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
@@ -37,19 +37,20 @@
* Serves as a collection of UI components, rather than showing its own UI.
*/
@SysUISingleton
-public class TvStatusBar extends CoreStartable implements CommandQueue.Callbacks {
+public class TvStatusBar implements CoreStartable, CommandQueue.Callbacks {
private static final String ACTION_SHOW_PIP_MENU =
"com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU";
private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
+ private final Context mContext;
private final CommandQueue mCommandQueue;
private final Lazy<AssistManager> mAssistManagerLazy;
@Inject
public TvStatusBar(Context context, CommandQueue commandQueue,
Lazy<AssistManager> assistManagerLazy) {
- super(context);
+ mContext = context;
mCommandQueue = commandQueue;
mAssistManagerLazy = assistManagerLazy;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt b/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt
index c199744..b938c90 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt
@@ -35,9 +35,9 @@
*/
@SysUISingleton
class VpnStatusObserver @Inject constructor(
- context: Context,
+ private val context: Context,
private val securityController: SecurityController
-) : CoreStartable(context),
+) : CoreStartable,
SecurityController.SecurityControllerCallback {
private var vpnConnected = false
@@ -102,7 +102,7 @@
.apply {
vpnName?.let {
setContentText(
- mContext.getString(
+ context.getString(
R.string.notification_disclosure_vpn_text, it
)
)
@@ -111,23 +111,23 @@
.build()
private fun createVpnConnectedNotificationBuilder() =
- Notification.Builder(mContext, NOTIFICATION_CHANNEL_TV_VPN)
+ Notification.Builder(context, NOTIFICATION_CHANNEL_TV_VPN)
.setSmallIcon(vpnIconId)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setCategory(Notification.CATEGORY_SYSTEM)
.extend(Notification.TvExtender())
.setOngoing(true)
- .setContentTitle(mContext.getString(R.string.notification_vpn_connected))
- .setContentIntent(VpnConfig.getIntentForStatusPanel(mContext))
+ .setContentTitle(context.getString(R.string.notification_vpn_connected))
+ .setContentIntent(VpnConfig.getIntentForStatusPanel(context))
private fun createVpnDisconnectedNotification() =
- Notification.Builder(mContext, NOTIFICATION_CHANNEL_TV_VPN)
+ Notification.Builder(context, NOTIFICATION_CHANNEL_TV_VPN)
.setSmallIcon(vpnIconId)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setCategory(Notification.CATEGORY_SYSTEM)
.extend(Notification.TvExtender())
.setTimeoutAfter(VPN_DISCONNECTED_NOTIFICATION_TIMEOUT_MS)
- .setContentTitle(mContext.getString(R.string.notification_vpn_disconnected))
+ .setContentTitle(context.getString(R.string.notification_vpn_disconnected))
.build()
companion object {
@@ -137,4 +137,4 @@
private const val TAG = "TvVpnNotification"
private const val VPN_DISCONNECTED_NOTIFICATION_TIMEOUT_MS = 5_000L
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java
index 8026ba5..b92725b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java
@@ -18,13 +18,13 @@
import android.annotation.Nullable;
import android.app.Notification;
-import android.content.Context;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.util.SparseArray;
import com.android.systemui.CoreStartable;
+import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.NotificationListener;
import javax.inject.Inject;
@@ -32,7 +32,8 @@
/**
* Keeps track of the notifications on TV.
*/
-public class TvNotificationHandler extends CoreStartable implements
+@SysUISingleton
+public class TvNotificationHandler implements CoreStartable,
NotificationListener.NotificationHandler {
private static final String TAG = "TvNotificationHandler";
private final NotificationListener mNotificationListener;
@@ -41,8 +42,7 @@
private Listener mUpdateListener;
@Inject
- public TvNotificationHandler(Context context, NotificationListener notificationListener) {
- super(context);
+ public TvNotificationHandler(NotificationListener notificationListener) {
mNotificationListener = notificationListener;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java
index 892fedc..dbbd0b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java
@@ -35,14 +35,15 @@
* Offers control methods for the notification panel handler on TV devices.
*/
@SysUISingleton
-public class TvNotificationPanel extends CoreStartable implements CommandQueue.Callbacks {
+public class TvNotificationPanel implements CoreStartable, CommandQueue.Callbacks {
private static final String TAG = "TvNotificationPanel";
+ private final Context mContext;
private final CommandQueue mCommandQueue;
private final String mNotificationHandlerPackage;
@Inject
public TvNotificationPanel(Context context, CommandQueue commandQueue) {
- super(context);
+ mContext = context;
mCommandQueue = commandQueue;
mNotificationHandlerPackage = mContext.getResources().getString(
com.android.internal.R.string.config_notificationHandlerPackage);
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index 4450b76..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
@@ -62,16 +61,15 @@
@LayoutRes private val viewLayoutRes: Int,
private val windowTitle: String,
private val wakeReason: String,
-) : CoreStartable(context) {
+) : CoreStartable {
/**
* Window layout params that will be used as a starting point for the [windowLayoutParams] of
* all subclasses.
*/
- @SuppressLint("WrongConstant") // We're allowed to use TYPE_VOLUME_OVERLAY
internal val commonWindowLayoutParams = WindowManager.LayoutParams().apply {
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
- type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY
+ type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
title = windowTitle
@@ -131,7 +129,7 @@
)
cancelViewTimeout?.run()
cancelViewTimeout = mainExecutor.executeDelayed(
- { removeView(TemporaryDisplayRemovalReason.REASON_TIMEOUT) },
+ { removeView(REMOVAL_REASON_TIMEOUT) },
timeout.toLong()
)
}
@@ -175,9 +173,6 @@
*/
fun removeView(removalReason: String) {
val currentDisplayInfo = displayInfo ?: return
- if (shouldIgnoreViewRemoval(currentDisplayInfo.info, removalReason)) {
- return
- }
val currentView = currentDisplayInfo.view
animateViewOut(currentView) { windowManager.removeView(currentView) }
@@ -193,13 +188,6 @@
}
/**
- * Returns true if a view removal request should be ignored and false otherwise.
- *
- * Allows subclasses to keep the view visible for longer in certain circumstances.
- */
- open fun shouldIgnoreViewRemoval(info: T, removalReason: String): Boolean = false
-
- /**
* A method implemented by subclasses to update [currentView] based on [newInfo].
*/
abstract fun updateView(newInfo: T, currentView: ViewGroup)
@@ -236,10 +224,7 @@
)
}
-object TemporaryDisplayRemovalReason {
- const val REASON_TIMEOUT = "TIMEOUT"
- const val REASON_SCREEN_TAP = "SCREEN_TAP"
-}
+private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT"
private data class IconInfo(
val iconName: String,
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
index 606a11a..a7185cb 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -16,8 +16,8 @@
package com.android.systemui.temporarydisplay
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
/** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */
open class TemporaryViewLogger(
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
new file mode 100644
index 0000000..b8930a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.temporarydisplay.chipbar
+
+import android.content.Context
+import android.graphics.Rect
+import android.os.PowerManager
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.view.accessibility.AccessibilityManager
+import android.widget.TextView
+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.MediaTttSenderLogger
+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.util.concurrency.DelayableExecutor
+import com.android.systemui.util.view.ViewUtil
+import javax.inject.Inject
+
+/**
+ * A coordinator for showing/hiding the chipbar.
+ *
+ * The chipbar is a UI element that displays on top of all content. It appears at the top of the
+ * screen and consists of an icon, one line of text, and an optional end icon or action. It will
+ * auto-dismiss after some amount of seconds. The user is *not* able to manually dismiss the
+ * chipbar.
+ *
+ * It should be only be used for critical and temporary information that the user *must* be aware
+ * of. In general, prefer using heads-up notifications, since they are dismissable and will remain
+ * in the list of notifications until the user dismisses them.
+ *
+ * Only one chipbar may be shown at a time.
+ * TODO(b/245610654): Should we just display whichever chipbar was most recently requested, or do we
+ * need to maintain a priority ordering?
+ *
+ * TODO(b/245610654): Remove all media-related items from this class so it's just for generic
+ * chipbars.
+ */
+@SysUISingleton
+open class ChipbarCoordinator @Inject constructor(
+ context: Context,
+ @MediaTttSenderLogger logger: MediaTttLogger,
+ windowManager: WindowManager,
+ @Main mainExecutor: DelayableExecutor,
+ accessibilityManager: AccessibilityManager,
+ configurationController: ConfigurationController,
+ powerManager: PowerManager,
+ private val falsingManager: FalsingManager,
+ private val falsingCollector: FalsingCollector,
+ private val viewUtil: ViewUtil,
+ private val vibratorHelper: VibratorHelper,
+) : TemporaryViewDisplayController<ChipbarInfo, MediaTttLogger>(
+ context,
+ logger,
+ windowManager,
+ mainExecutor,
+ accessibilityManager,
+ configurationController,
+ powerManager,
+ R.layout.chipbar,
+ MediaTttUtils.WINDOW_TITLE,
+ MediaTttUtils.WAKE_REASON,
+) {
+
+ private lateinit var parent: ChipbarRootView
+
+ override val windowLayoutParams = commonWindowLayoutParams.apply {
+ gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
+ }
+
+ override fun start() {}
+
+ override fun updateView(
+ newInfo: ChipbarInfo,
+ currentView: ViewGroup
+ ) {
+ // TODO(b/245610654): Adding logging here.
+
+ // Detect falsing touches on the chip.
+ parent = currentView.requireViewById(R.id.chipbar_root_view)
+ parent.touchHandler = object : Gefingerpoken {
+ override fun onTouchEvent(ev: MotionEvent?): Boolean {
+ falsingCollector.onTouchEvent(ev)
+ return false
+ }
+ }
+
+ // ---- Start icon ----
+ val iconView = currentView.requireViewById<CachingIconView>(R.id.start_icon)
+ IconViewBinder.bind(newInfo.startIcon, iconView)
+
+ // ---- 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 =
+ (newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue()
+
+ // Error
+ currentView.requireViewById<View>(R.id.error).visibility =
+ (newInfo.endItem == ChipbarEndItem.Error).visibleIfTrue()
+
+ // Button
+ val buttonView = currentView.requireViewById<TextView>(R.id.end_button)
+ if (newInfo.endItem is ChipbarEndItem.Button) {
+ TextViewBinder.bind(buttonView, newInfo.endItem.text)
+
+ 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.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.chipbar_inner)
+ ViewHierarchyAnimator.animateAddition(
+ chipInnerView,
+ ViewHierarchyAnimator.Hotspot.TOP,
+ Interpolators.EMPHASIZED_DECELERATE,
+ duration = ANIMATION_DURATION,
+ includeMargins = true,
+ includeFadeIn = true,
+ // We can only request focus once the animation finishes.
+ onAnimationEnd = { chipInnerView.requestAccessibilityFocus() },
+ )
+ }
+
+ override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ ViewHierarchyAnimator.animateRemoval(
+ view.requireViewById<ViewGroup>(R.id.chipbar_inner),
+ ViewHierarchyAnimator.Hotspot.TOP,
+ Interpolators.EMPHASIZED_ACCELERATE,
+ ANIMATION_DURATION,
+ includeMargins = true,
+ onAnimationEnd,
+ )
+ }
+
+ override fun getTouchableRegion(view: View, outRect: Rect) {
+ viewUtil.setRectToViewWindowLocation(view, outRect)
+ }
+
+ private fun Boolean.visibleIfTrue(): Int {
+ return if (this) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ }
+}
+
+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/media/taptotransfer/sender/MediaTttChipRootView.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarRootView.kt
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt
rename to packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarRootView.kt
index 3373159..edec420 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarRootView.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.media.taptotransfer.sender
+package com.android.systemui.temporarydisplay.chipbar
import android.content.Context
import android.util.AttributeSet
@@ -22,8 +22,8 @@
import android.widget.FrameLayout
import com.android.systemui.Gefingerpoken
-/** A simple subclass that allows for observing touch events on chip. */
-class MediaTttChipRootView(
+/** A simple subclass that allows for observing touch events on chipbar. */
+class ChipbarRootView(
context: Context,
attrs: AttributeSet?
) : FrameLayout(context, attrs) {
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index a345d99..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;
@@ -100,7 +101,7 @@
* associated work profiles
*/
@SysUISingleton
-public class ThemeOverlayController extends CoreStartable implements Dumpable {
+public class ThemeOverlayController implements CoreStartable, Dumpable {
protected static final String TAG = "ThemeOverlayController";
private static final boolean DEBUG = true;
@@ -114,6 +115,8 @@
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;
private final DeviceProvisionedController mDeviceProvisionedController;
@@ -361,8 +364,8 @@
UserManager userManager, DeviceProvisionedController deviceProvisionedController,
UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags,
@Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) {
- super(context);
-
+ 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/toast/ToastLogger.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
index 51541bd..fda5114 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
@@ -16,11 +16,11 @@
package com.android.systemui.toast
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogMessage
import com.android.systemui.log.dagger.ToastLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogMessage
import javax.inject.Inject
private const val TAG = "ToastLog"
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
index 9eb34a4..ed14c8a 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
@@ -50,13 +50,14 @@
* Controls display of text toasts.
*/
@SysUISingleton
-public class ToastUI extends CoreStartable implements CommandQueue.Callbacks {
+public class ToastUI implements CoreStartable, CommandQueue.Callbacks {
// values from NotificationManagerService#LONG_DELAY and NotificationManagerService#SHORT_DELAY
private static final int TOAST_LONG_TIME = 3500; // 3.5 seconds
private static final int TOAST_SHORT_TIME = 2000; // 2 seconds
private static final String TAG = "ToastUI";
+ private final Context mContext;
private final CommandQueue mCommandQueue;
private final INotificationManager mNotificationManager;
private final IAccessibilityManager mIAccessibilityManager;
@@ -90,7 +91,7 @@
@Nullable IAccessibilityManager accessibilityManager,
ToastFactory toastFactory, ToastLogger toastLogger
) {
- super(context);
+ mContext = context;
mCommandQueue = commandQueue;
mNotificationManager = notificationManager;
mIAccessibilityManager = accessibilityManager;
@@ -179,7 +180,7 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(Configuration newConfig) {
if (newConfig.orientation != mOrientation) {
mOrientation = newConfig.orientation;
if (mToast != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 3ce5ca3..10a09dd1 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -203,9 +203,9 @@
@Provides
@SysUISingleton
- static TvNotificationHandler provideTvNotificationHandler(Context context,
+ static TvNotificationHandler provideTvNotificationHandler(
NotificationListener notificationListener) {
- return new TvNotificationHandler(context, notificationListener);
+ return new TvNotificationHandler(notificationListener);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index fc20ac2..6ed3a09 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -26,8 +26,6 @@
import android.view.Choreographer
import android.view.Display
import android.view.DisplayInfo
-import android.view.IRotationWatcher
-import android.view.IWindowManager
import android.view.Surface
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
@@ -40,6 +38,7 @@
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.LinearLightRevealEffect
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.util.traceSection
import com.android.wm.shell.displayareahelper.DisplayAreaHelper
import java.util.Optional
@@ -58,7 +57,7 @@
private val displayAreaHelper: Optional<DisplayAreaHelper>,
@Main private val executor: Executor,
@UiBackground private val backgroundExecutor: Executor,
- private val windowManagerInterface: IWindowManager
+ private val rotationChangeProvider: RotationChangeProvider,
) {
private val transitionListener = TransitionListener()
@@ -78,7 +77,7 @@
fun init() {
deviceStateManager.registerCallback(executor, FoldListener())
unfoldTransitionProgressProvider.addCallback(transitionListener)
- windowManagerInterface.watchRotation(rotationWatcher, context.display.displayId)
+ rotationChangeProvider.addCallback(rotationWatcher)
val containerBuilder =
SurfaceControl.Builder(SurfaceSession())
@@ -86,7 +85,9 @@
.setName("unfold-overlay-container")
displayAreaHelper.get().attachToRootDisplayArea(
- Display.DEFAULT_DISPLAY, containerBuilder) { builder ->
+ Display.DEFAULT_DISPLAY,
+ containerBuilder
+ ) { builder ->
executor.execute {
overlayContainer = builder.build()
@@ -244,8 +245,8 @@
}
}
- private inner class RotationWatcher : IRotationWatcher.Stub() {
- override fun onRotationChanged(newRotation: Int) =
+ private inner class RotationWatcher : RotationChangeProvider.RotationListener {
+ override fun onRotationChanged(newRotation: Int) {
traceSection("UnfoldLightRevealOverlayAnimation#onRotationChanged") {
if (currentRotation != newRotation) {
currentRotation = newRotation
@@ -253,6 +254,7 @@
root?.relayout(getLayoutParams())
}
}
+ }
}
private inner class FoldListener :
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
index eea6ac0..59ad24a 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
@@ -17,11 +17,11 @@
package com.android.systemui.unfold
import android.content.Context
-import android.view.IWindowManager
import com.android.systemui.keyguard.LifecycleScreenStatusProvider
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.system.SystemUnfoldSharedModule
import com.android.systemui.unfold.updates.FoldStateProvider
+import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
@@ -65,11 +65,11 @@
@Singleton
fun provideNaturalRotationProgressProvider(
context: Context,
- windowManager: IWindowManager,
+ rotationChangeProvider: RotationChangeProvider,
unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider>
): Optional<NaturalRotationUnfoldProgressProvider> =
unfoldTransitionProgressProvider.map { provider ->
- NaturalRotationUnfoldProgressProvider(context, windowManager, provider)
+ NaturalRotationUnfoldProgressProvider(context, rotationChangeProvider, provider)
}
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
index 4dc78f9..bf70673 100644
--- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
+++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
@@ -21,7 +21,6 @@
import android.app.Notification.Action;
import android.app.NotificationManager;
import android.app.PendingIntent;
-import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -56,11 +55,12 @@
/** */
@SysUISingleton
-public class StorageNotification extends CoreStartable {
+public class StorageNotification implements CoreStartable {
private static final String TAG = "StorageNotification";
private static final String ACTION_SNOOZE_VOLUME = "com.android.systemui.action.SNOOZE_VOLUME";
private static final String ACTION_FINISH_WIZARD = "com.android.systemui.action.FINISH_WIZARD";
+ private final Context mContext;
// TODO: delay some notifications to avoid bumpy fast operations
@@ -69,7 +69,7 @@
@Inject
public StorageNotification(Context context) {
- super(context);
+ mContext = context;
}
private static class MoveInfo {
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
index 108ab43..7da2d47 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
@@ -16,426 +16,42 @@
package com.android.systemui.user
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.graphics.drawable.BitmapDrawable
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.GradientDrawable
-import android.graphics.drawable.LayerDrawable
import android.os.Bundle
-import android.os.UserManager
-import android.provider.Settings
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.MotionEvent
-import android.view.View
-import android.view.ViewGroup
-import android.widget.AdapterView
-import android.widget.ArrayAdapter
-import android.widget.ImageView
-import android.widget.TextView
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
+import android.view.WindowInsets.Type
+import android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
import androidx.activity.ComponentActivity
-import androidx.constraintlayout.helper.widget.Flow
import androidx.lifecycle.ViewModelProvider
-import com.android.internal.annotations.VisibleForTesting
-import com.android.internal.util.UserIcons
-import com.android.settingslib.Utils
-import com.android.systemui.Gefingerpoken
import com.android.systemui.R
-import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter
-import com.android.systemui.statusbar.policy.UserSwitcherController
-import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.ui.binder.UserSwitcherViewBinder
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
import dagger.Lazy
import javax.inject.Inject
-import kotlin.math.ceil
-
-private const val USER_VIEW = "user_view"
/** Support a fullscreen user switcher */
open class UserSwitcherActivity
@Inject
constructor(
- private val userSwitcherController: UserSwitcherController,
- private val broadcastDispatcher: BroadcastDispatcher,
private val falsingCollector: FalsingCollector,
- private val falsingManager: FalsingManager,
- private val userManager: UserManager,
- private val userTracker: UserTracker,
- private val flags: FeatureFlags,
private val viewModelFactory: Lazy<UserSwitcherViewModel.Factory>,
) : ComponentActivity() {
- private lateinit var parent: UserSwitcherRootView
- private lateinit var broadcastReceiver: BroadcastReceiver
- private var popupMenu: UserSwitcherPopupMenu? = null
- private lateinit var addButton: View
- private var addUserRecords = mutableListOf<UserRecord>()
- private val onBackCallback = OnBackInvokedCallback { finish() }
- private val userSwitchedCallback: UserTracker.Callback =
- object : UserTracker.Callback {
- override fun onUserChanged(newUser: Int, userContext: Context) {
- finish()
- }
- }
- // When the add users options become available, insert another option to manage users
- private val manageUserRecord =
- UserRecord(
- null /* info */,
- null /* picture */,
- false /* isGuest */,
- false /* isCurrent */,
- false /* isAddUser */,
- false /* isRestricted */,
- false /* isSwitchToEnabled */,
- false /* isAddSupervisedUser */
- )
-
- private val adapter: UserAdapter by lazy { UserAdapter() }
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- createActivity()
- }
-
- @VisibleForTesting
- fun createActivity() {
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)
- if (isUsingModernArchitecture()) {
- Log.d(TAG, "Using modern architecture.")
- val viewModel =
- ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java]
- UserSwitcherViewBinder.bind(
- view = requireViewById(R.id.user_switcher_root),
- viewModel = viewModel,
- lifecycleOwner = this,
- layoutInflater = layoutInflater,
- falsingCollector = falsingCollector,
- onFinish = this::finish,
- )
- return
- } else {
- Log.d(TAG, "Not using modern architecture.")
+ window.decorView.getWindowInsetsController().apply {
+ setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE)
+ hide(Type.systemBars())
}
-
- parent = requireViewById<UserSwitcherRootView>(R.id.user_switcher_root)
-
- parent.touchHandler =
- object : Gefingerpoken {
- override fun onTouchEvent(ev: MotionEvent?): Boolean {
- falsingCollector.onTouchEvent(ev)
- return false
- }
- }
-
- requireViewById<View>(R.id.cancel).apply { setOnClickListener { _ -> finish() } }
-
- addButton =
- requireViewById<View>(R.id.add).apply { setOnClickListener { _ -> showPopupMenu() } }
-
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- onBackCallback
+ val viewModel =
+ ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java]
+ UserSwitcherViewBinder.bind(
+ view = requireViewById(R.id.user_switcher_root),
+ viewModel = viewModel,
+ lifecycleOwner = this,
+ layoutInflater = layoutInflater,
+ falsingCollector = falsingCollector,
+ onFinish = this::finish,
)
-
- userSwitcherController.init(parent)
- initBroadcastReceiver()
-
- parent.post { buildUserViews() }
- userTracker.addCallback(userSwitchedCallback, mainExecutor)
- }
-
- private fun showPopupMenu() {
- val items = mutableListOf<UserRecord>()
- addUserRecords.forEach { items.add(it) }
-
- var popupMenuAdapter =
- ItemAdapter(
- this,
- R.layout.user_switcher_fullscreen_popup_item,
- layoutInflater,
- { item: UserRecord -> adapter.getName(this@UserSwitcherActivity, item, true) },
- { item: UserRecord ->
- adapter.findUserIcon(item, true).mutate().apply {
- setTint(
- resources.getColor(
- R.color.user_switcher_fullscreen_popup_item_tint,
- getTheme()
- )
- )
- }
- }
- )
- popupMenuAdapter.addAll(items)
-
- popupMenu =
- UserSwitcherPopupMenu(this).apply {
- setAnchorView(addButton)
- setAdapter(popupMenuAdapter)
- setOnItemClickListener { parent: AdapterView<*>, view: View, pos: Int, id: Long ->
- if (falsingManager.isFalseTap(LOW_PENALTY) || !view.isEnabled()) {
- return@setOnItemClickListener
- }
- // -1 for the header
- val item = popupMenuAdapter.getItem(pos - 1)
- if (item == manageUserRecord) {
- val i = Intent().setAction(Settings.ACTION_USER_SETTINGS)
- this@UserSwitcherActivity.startActivity(i)
- } else {
- adapter.onUserListItemClicked(item)
- }
-
- dismiss()
- popupMenu = null
-
- if (!item.isAddUser) {
- this@UserSwitcherActivity.finish()
- }
- }
-
- show()
- }
- }
-
- private fun buildUserViews() {
- var count = 0
- var start = 0
- for (i in 0 until parent.getChildCount()) {
- if (parent.getChildAt(i).getTag() == USER_VIEW) {
- if (count == 0) start = i
- count++
- }
- }
- parent.removeViews(start, count)
- addUserRecords.clear()
- val flow = requireViewById<Flow>(R.id.flow)
- val totalWidth = parent.width
- val userViewCount = adapter.getTotalUserViews()
- val maxColumns = getMaxColumns(userViewCount)
- val horizontalGap =
- resources.getDimensionPixelSize(R.dimen.user_switcher_fullscreen_horizontal_gap)
- val totalWidthOfHorizontalGap = (maxColumns - 1) * horizontalGap
- val maxWidgetDiameter = (totalWidth - totalWidthOfHorizontalGap) / maxColumns
-
- flow.setMaxElementsWrap(maxColumns)
-
- for (i in 0 until adapter.getCount()) {
- val item = adapter.getItem(i)
- if (adapter.doNotRenderUserView(item)) {
- addUserRecords.add(item)
- } else {
- val userView = adapter.getView(i, null, parent)
- userView.requireViewById<ImageView>(R.id.user_switcher_icon).apply {
- val lp = layoutParams
- if (maxWidgetDiameter < lp.width) {
- lp.width = maxWidgetDiameter
- lp.height = maxWidgetDiameter
- layoutParams = lp
- }
- }
-
- userView.setId(View.generateViewId())
- parent.addView(userView)
-
- // Views must have an id and a parent in order for Flow to lay them out
- flow.addView(userView)
-
- userView.setOnClickListener { v ->
- if (falsingManager.isFalseTap(LOW_PENALTY) || !v.isEnabled()) {
- return@setOnClickListener
- }
-
- if (!item.isCurrent || item.isGuest) {
- adapter.onUserListItemClicked(item)
- }
- }
- }
- }
-
- if (!addUserRecords.isEmpty()) {
- addUserRecords.add(manageUserRecord)
- addButton.visibility = View.VISIBLE
- } else {
- addButton.visibility = View.GONE
- }
- }
-
- override fun onBackPressed() {
- if (isUsingModernArchitecture()) {
- return super.onBackPressed()
- }
-
- finish()
- }
-
- override fun onDestroy() {
- super.onDestroy()
- if (isUsingModernArchitecture()) {
- return
- }
- destroyActivity()
- }
-
- @VisibleForTesting
- fun destroyActivity() {
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(onBackCallback)
- broadcastDispatcher.unregisterReceiver(broadcastReceiver)
- userTracker.removeCallback(userSwitchedCallback)
- }
-
- private fun initBroadcastReceiver() {
- broadcastReceiver =
- object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- val action = intent.getAction()
- if (Intent.ACTION_SCREEN_OFF.equals(action)) {
- finish()
- }
- }
- }
-
- val filter = IntentFilter()
- filter.addAction(Intent.ACTION_SCREEN_OFF)
- broadcastDispatcher.registerReceiver(broadcastReceiver, filter)
- }
-
- @VisibleForTesting
- fun getMaxColumns(userCount: Int): Int {
- return if (userCount < 5) 4 else ceil(userCount / 2.0).toInt()
- }
-
- private fun isUsingModernArchitecture(): Boolean {
- return flags.isEnabled(Flags.MODERN_USER_SWITCHER_ACTIVITY)
- }
-
- /** Provides views to populate the option menu. */
- private class ItemAdapter(
- val parentContext: Context,
- val resource: Int,
- val layoutInflater: LayoutInflater,
- val textGetter: (UserRecord) -> String,
- val iconGetter: (UserRecord) -> Drawable
- ) : ArrayAdapter<UserRecord>(parentContext, resource) {
-
- override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
- val item = getItem(position)
- val view = convertView ?: layoutInflater.inflate(resource, parent, false)
-
- view.requireViewById<ImageView>(R.id.icon).apply { setImageDrawable(iconGetter(item)) }
- view.requireViewById<TextView>(R.id.text).apply { setText(textGetter(item)) }
-
- return view
- }
- }
-
- private inner class UserAdapter : BaseUserSwitcherAdapter(userSwitcherController) {
- override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
- val item = getItem(position)
- var view = convertView as ViewGroup?
- if (view == null) {
- view =
- layoutInflater.inflate(R.layout.user_switcher_fullscreen_item, parent, false)
- as ViewGroup
- }
- (view.getChildAt(0) as ImageView).apply { setImageDrawable(getDrawable(item)) }
- (view.getChildAt(1) as TextView).apply { setText(getName(getContext(), item)) }
-
- view.setEnabled(item.isSwitchToEnabled)
- UserSwitcherController.setSelectableAlpha(view)
- view.setTag(USER_VIEW)
- return view
- }
-
- override fun getName(context: Context, item: UserRecord, isTablet: Boolean): String {
- return if (item == manageUserRecord) {
- getString(R.string.manage_users)
- } else {
- super.getName(context, item, isTablet)
- }
- }
-
- fun findUserIcon(item: UserRecord, isTablet: Boolean = false): Drawable {
- if (item == manageUserRecord) {
- return getDrawable(R.drawable.ic_manage_users)
- }
- if (item.info == null) {
- return getIconDrawable(this@UserSwitcherActivity, item, isTablet)
- }
- val userIcon = userManager.getUserIcon(item.info.id)
- if (userIcon != null) {
- return BitmapDrawable(userIcon)
- }
- return UserIcons.getDefaultUserIcon(resources, item.info.id, false)
- }
-
- fun getTotalUserViews(): Int {
- return users.count { item -> !doNotRenderUserView(item) }
- }
-
- fun doNotRenderUserView(item: UserRecord): Boolean {
- return item.isAddUser || item.isAddSupervisedUser || item.isGuest && item.info == null
- }
-
- private fun getDrawable(item: UserRecord): Drawable {
- var drawable =
- if (item.isGuest) {
- getDrawable(R.drawable.ic_account_circle)
- } else {
- findUserIcon(item)
- }
- drawable.mutate()
-
- if (!item.isCurrent && !item.isSwitchToEnabled) {
- drawable.setTint(
- resources.getColor(
- R.color.kg_user_switcher_restricted_avatar_icon_color,
- getTheme()
- )
- )
- }
-
- val ld = getDrawable(R.drawable.user_switcher_icon_large).mutate() as LayerDrawable
- if (item == userSwitcherController.currentUserRecord) {
- (ld.findDrawableByLayerId(R.id.ring) as GradientDrawable).apply {
- val stroke =
- resources.getDimensionPixelSize(R.dimen.user_switcher_icon_selected_width)
- val color =
- Utils.getColorAttrDefaultColor(
- this@UserSwitcherActivity,
- com.android.internal.R.attr.colorAccentPrimary
- )
-
- setStroke(stroke, color)
- }
- }
-
- ld.setDrawableByLayerId(R.id.user_avatar, drawable)
- return ld
- }
-
- override fun notifyDataSetChanged() {
- super.notifyDataSetChanged()
- buildUserViews()
- }
- }
-
- companion object {
- private const val TAG = "UserSwitcherActivity"
}
}
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 919e699..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 }
}
}
}
@@ -321,6 +326,7 @@
return when {
isAddUser -> false
isAddSupervisedUser -> false
+ isManageUsers -> false
isGuest -> info != null
else -> true
}
@@ -346,6 +352,7 @@
isAddUser -> UserActionModel.ADD_USER
isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
isGuest -> UserActionModel.ENTER_GUEST_MODE
+ isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
else -> error("Don't know how to convert to UserActionModel: $this")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
index 9370286..d4fb563 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
@@ -47,6 +47,9 @@
* If not disabled, this is `null`.
*/
@JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null,
+
+ /** Whether this record is to go to the Settings page to manage users. */
+ @JvmField val isManageUsers: Boolean = false
) {
/** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */
fun copyWithIsCurrent(isCurrent: Boolean): UserRecord {
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
index 1b4746a..dc004f3 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
@@ -82,6 +82,15 @@
)
}
+ fun canManageUsers(
+ repository: UserRepository,
+ isUserSwitcherEnabled: Boolean,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ ): Boolean {
+ return isUserSwitcherEnabled &&
+ (repository.getSelectedUserInfo().isAdmin || isAddUsersFromLockScreenEnabled)
+ }
+
/**
* Returns `true` if the current user is allowed to add users to the device; `false` otherwise.
*/
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 142a328..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
@@ -102,6 +102,7 @@
interface UserCallback {
/** Returns `true` if this callback can be cleaned-up. */
fun isEvictable(): Boolean = false
+
/** Notifies that the state of users on the device has changed. */
fun onUserStateChanged()
}
@@ -164,10 +165,11 @@
get() =
if (isNewImpl) {
combine(
+ repository.selectedUserInfo,
repository.userInfos,
repository.userSwitcherSettings,
keyguardInteractor.isKeyguardShowing,
- ) { userInfos, settings, isDeviceLocked ->
+ ) { _, userInfos, settings, isDeviceLocked ->
buildList {
val hasGuestUser = userInfos.any { it.isGuest }
if (
@@ -183,35 +185,45 @@
add(UserActionModel.ENTER_GUEST_MODE)
}
- if (isDeviceLocked && !settings.isAddUsersFromLockscreen) {
+ if (!isDeviceLocked || settings.isAddUsersFromLockscreen) {
// The device is locked and our setting to allow actions that add users
// from the lock-screen is not enabled. The guest action from above is
// always allowed, even when the device is locked, but the various "add
// user" actions below are not. We can finish building the list here.
- return@buildList
+
+ val canCreateUsers =
+ UserActionsUtil.canCreateUser(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ )
+
+ if (canCreateUsers) {
+ add(UserActionModel.ADD_USER)
+ }
+
+ if (
+ UserActionsUtil.canCreateSupervisedUser(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ supervisedUserPackageName,
+ )
+ ) {
+ add(UserActionModel.ADD_SUPERVISED_USER)
+ }
}
if (
- UserActionsUtil.canCreateUser(
- manager,
+ UserActionsUtil.canManageUsers(
repository,
settings.isUserSwitcherEnabled,
settings.isAddUsersFromLockscreen,
)
) {
- add(UserActionModel.ADD_USER)
- }
-
- if (
- UserActionsUtil.canCreateSupervisedUser(
- manager,
- repository,
- settings.isUserSwitcherEnabled,
- settings.isAddUsersFromLockscreen,
- supervisedUserPackageName,
- )
- ) {
- add(UserActionModel.ADD_SUPERVISED_USER)
+ add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
}
}
}
@@ -224,18 +236,7 @@
}
.flatMapLatest { isActionable ->
if (isActionable) {
- repository.actions.map { actions ->
- actions +
- if (actions.isNotEmpty()) {
- // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT
- // because that's a user switcher specific action that is
- // not known to the our data source or other features.
- listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
- } else {
- // If no actions, don't add the navigate action.
- emptyList()
- }
- }
+ repository.actions
} else {
// If not actionable it means that we're not allowed to show actions
// when
@@ -264,7 +265,10 @@
toRecord(
action = it,
selectedUserId = selectedUserInfo.id,
- isAddFromLockscreenEnabled = settings.isAddUsersFromLockscreen,
+ isRestricted =
+ it != UserActionModel.ENTER_GUEST_MODE &&
+ it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT &&
+ !settings.isAddUsersFromLockscreen,
)
}
)
@@ -425,6 +429,7 @@
isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
onExitGuestUser = this::exitGuestUser,
+ dialogShower = dialogShower,
)
)
return
@@ -439,6 +444,7 @@
isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
onExitGuestUser = this::exitGuestUser,
+ dialogShower = dialogShower,
)
)
return
@@ -473,6 +479,7 @@
userHandle = currentUser.userHandle,
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
+ dialogShower = dialogShower,
)
)
}
@@ -482,12 +489,12 @@
.setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
.setPackage(supervisedUserPackageName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
- /* dismissShade= */ false,
+ /* dismissShade= */ true,
)
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
activityStarter.startActivity(
Intent(Settings.ACTION_USER_SETTINGS),
- /* dismissShade= */ false,
+ /* dismissShade= */ true,
)
}
} else {
@@ -575,20 +582,13 @@
private suspend fun toRecord(
action: UserActionModel,
selectedUserId: Int,
- isAddFromLockscreenEnabled: Boolean,
+ isRestricted: Boolean,
): UserRecord {
return LegacyUserDataHelper.createRecord(
context = applicationContext,
selectedUserId = selectedUserId,
actionType = action,
- isRestricted =
- if (action == UserActionModel.ENTER_GUEST_MODE) {
- // Entering guest mode is never restricted, so it's allowed to happen from the
- // lockscreen even if the "add from lockscreen" system setting is off.
- false
- } else {
- !isAddFromLockscreenEnabled
- },
+ isRestricted = isRestricted,
isSwitchToEnabled =
canSwitchUsers(selectedUserId) &&
// If the user is auto-created is must not be currently resetting.
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/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
index 137de15..03a7470 100644
--- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
@@ -80,6 +80,7 @@
context = context,
selectedUserId = selectedUserId,
),
+ isManageUsers = actionType == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
}
@@ -90,6 +91,7 @@
record.isAddUser -> UserActionModel.ADD_USER
record.isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
record.isGuest -> UserActionModel.ENTER_GUEST_MODE
+ record.isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
else -> error("Not a known action: $record")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
index 15fdc35..e74232d 100644
--- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
@@ -22,7 +22,6 @@
import androidx.annotation.StringRes
import com.android.systemui.R
import com.android.systemui.user.data.source.UserRecord
-import kotlin.math.ceil
/**
* Defines utility functions for helping with legacy UI code for users.
@@ -33,16 +32,6 @@
*/
object LegacyUserUiHelper {
- /** Returns the maximum number of columns for user items in the user switcher. */
- fun getMaxUserSwitcherItemColumns(userCount: Int): Int {
- // TODO(b/243844097): remove this once we remove the old user switcher implementation.
- return if (userCount < 5) {
- 4
- } else {
- ceil(userCount / 2.0).toInt()
- }
- }
-
@JvmStatic
@DrawableRes
fun getUserSwitcherActionIconResourceId(
@@ -50,6 +39,7 @@
isGuest: Boolean,
isAddSupervisedUser: Boolean,
isTablet: Boolean = false,
+ isManageUsers: Boolean,
): Int {
return if (isAddUser && isTablet) {
R.drawable.ic_account_circle_filled
@@ -59,6 +49,8 @@
R.drawable.ic_account_circle
} else if (isAddSupervisedUser) {
R.drawable.ic_add_supervised_user
+ } else if (isManageUsers) {
+ R.drawable.ic_manage_users
} else {
R.drawable.ic_avatar_user
}
@@ -85,6 +77,7 @@
isAddUser = record.isAddUser,
isAddSupervisedUser = record.isAddSupervisedUser,
isTablet = isTablet,
+ isManageUsers = record.isManageUsers,
)
)
}
@@ -114,8 +107,9 @@
isAddUser: Boolean,
isAddSupervisedUser: Boolean,
isTablet: Boolean = false,
+ isManageUsers: Boolean,
): Int {
- check(isGuest || isAddUser || isAddSupervisedUser)
+ check(isGuest || isAddUser || isAddSupervisedUser || isManageUsers)
return when {
isGuest && isGuestUserAutoCreated && isGuestUserResetting ->
@@ -125,6 +119,7 @@
isGuest -> com.android.internal.R.string.guest_name
isAddUser -> com.android.settingslib.R.string.user_add_user
isAddSupervisedUser -> R.string.add_user_supervised
+ isManageUsers -> R.string.manage_users
else -> error("This should never happen!")
}
}
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 6e7b523..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
@@ -30,6 +32,7 @@
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
@@ -41,19 +44,19 @@
class UserSwitcherDialogCoordinator
@Inject
constructor(
- @Application private val context: Context,
- @Application private val applicationScope: CoroutineScope,
- private val falsingManager: FalsingManager,
- private val broadcastSender: BroadcastSender,
- private val dialogLaunchAnimator: DialogLaunchAnimator,
- private val interactor: UserInteractor,
- private val featureFlags: FeatureFlags,
-) : CoreStartable(context) {
+ @Application private val context: Lazy<Context>,
+ @Application private val applicationScope: Lazy<CoroutineScope>,
+ private val falsingManager: Lazy<FalsingManager>,
+ private val broadcastSender: Lazy<BroadcastSender>,
+ private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>,
+ private val interactor: Lazy<UserInteractor>,
+ private val featureFlags: Lazy<FeatureFlags>,
+) : CoreStartable {
private var currentDialog: Dialog? = null
override fun start() {
- if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
+ if (featureFlags.get().isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
return
}
@@ -62,61 +65,87 @@
}
private fun startHandlingDialogShowRequests() {
- applicationScope.launch {
- interactor.dialogShowRequests.filterNotNull().collect { request ->
+ applicationScope.get().launch {
+ interactor.get().dialogShowRequests.filterNotNull().collect { request ->
currentDialog?.let {
if (it.isShowing) {
it.cancel()
}
}
- currentDialog =
+ val (dialog, dialogCuj) =
when (request) {
is ShowDialogRequestModel.ShowAddUserDialog ->
- AddUserDialog(
- context = context,
- userHandle = request.userHandle,
- isKeyguardShowing = request.isKeyguardShowing,
- showEphemeralMessage = request.showEphemeralMessage,
- falsingManager = falsingManager,
- broadcastSender = broadcastSender,
- dialogLaunchAnimator = dialogLaunchAnimator,
+ Pair(
+ AddUserDialog(
+ context = context.get(),
+ userHandle = request.userHandle,
+ isKeyguardShowing = request.isKeyguardShowing,
+ showEphemeralMessage = request.showEphemeralMessage,
+ falsingManager = falsingManager.get(),
+ broadcastSender = broadcastSender.get(),
+ dialogLaunchAnimator = dialogLaunchAnimator.get(),
+ ),
+ DialogCuj(
+ InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
+ INTERACTION_JANK_ADD_NEW_USER_TAG,
+ ),
)
is ShowDialogRequestModel.ShowUserCreationDialog ->
- UserCreatingDialog(
- context,
- request.isGuest,
+ Pair(
+ UserCreatingDialog(
+ context.get(),
+ request.isGuest,
+ ),
+ null,
)
is ShowDialogRequestModel.ShowExitGuestDialog ->
- ExitGuestDialog(
- context = context,
- guestUserId = request.guestUserId,
- isGuestEphemeral = request.isGuestEphemeral,
- targetUserId = request.targetUserId,
- isKeyguardShowing = request.isKeyguardShowing,
- falsingManager = falsingManager,
- dialogLaunchAnimator = dialogLaunchAnimator,
- onExitGuestUserListener = request.onExitGuestUser,
+ Pair(
+ ExitGuestDialog(
+ context = context.get(),
+ guestUserId = request.guestUserId,
+ isGuestEphemeral = request.isGuestEphemeral,
+ targetUserId = request.targetUserId,
+ isKeyguardShowing = request.isKeyguardShowing,
+ falsingManager = falsingManager.get(),
+ dialogLaunchAnimator = dialogLaunchAnimator.get(),
+ onExitGuestUserListener = request.onExitGuestUser,
+ ),
+ DialogCuj(
+ InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
+ INTERACTION_JANK_EXIT_GUEST_MODE_TAG,
+ ),
)
}
+ currentDialog = dialog
- currentDialog?.show()
- interactor.onDialogShown()
+ if (request.dialogShower != null && dialogCuj != null) {
+ request.dialogShower?.showDialog(dialog, dialogCuj)
+ } else {
+ dialog.show()
+ }
+
+ interactor.get().onDialogShown()
}
}
}
private fun startHandlingDialogDismissRequests() {
- applicationScope.launch {
- interactor.dialogDismissRequests.filterNotNull().collect {
+ applicationScope.get().launch {
+ interactor.get().dialogDismissRequests.filterNotNull().collect {
currentDialog?.let {
if (it.isShowing) {
it.cancel()
}
}
- interactor.onDialogDismissed()
+ interactor.get().onDialogDismissed()
}
}
}
+
+ companion object {
+ private const val INTERACTION_JANK_ADD_NEW_USER_TAG = "add_new_user"
+ private const val INTERACTION_JANK_EXIT_GUEST_MODE_TAG = "exit_guest_mode"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 5b83df7..d857e85 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -19,7 +19,6 @@
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
-import com.android.systemui.R
import com.android.systemui.common.ui.drawable.CircularDrawable
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
@@ -30,6 +29,7 @@
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
import javax.inject.Inject
+import kotlin.math.ceil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
@@ -52,8 +52,7 @@
userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
/** The maximum number of columns that the user selection grid should use. */
- val maximumUserColumns: Flow<Int> =
- users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) }
+ val maximumUserColumns: Flow<Int> = users.map { getMaxUserSwitcherItemColumns(it.size) }
private val _isMenuVisible = MutableStateFlow(false)
/**
@@ -63,17 +62,7 @@
val isMenuVisible: Flow<Boolean> = _isMenuVisible
/** The user action menu. */
val menu: Flow<List<UserActionViewModel>> =
- userInteractor.actions.map { actions ->
- if (isNewImpl && actions.isNotEmpty()) {
- // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user
- // switcher specific action that is not known to the our data source or other
- // features.
- actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
- } else {
- actions
- }
- .map { action -> toViewModel(action) }
- }
+ userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
/** Whether the button to open the user action menu is visible. */
val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() }
@@ -118,6 +107,15 @@
_isMenuVisible.value = false
}
+ /** Returns the maximum number of columns for user items in the user switcher. */
+ private fun getMaxUserSwitcherItemColumns(userCount: Int): Int {
+ return if (userCount < 5) {
+ 4
+ } else {
+ ceil(userCount / 2.0).toInt()
+ }
+ }
+
private fun createFinishRequestedFlow(): Flow<Boolean> {
var mostRecentSelectedUserId: Int? = null
var mostRecentIsInteractive: Boolean? = null
@@ -171,27 +169,23 @@
return UserActionViewModel(
viewKey = model.ordinal.toLong(),
iconResourceId =
- if (model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) {
- R.drawable.ic_manage_users
- } else {
- LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
- isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
- isAddUser = model == UserActionModel.ADD_USER,
- isGuest = model == UserActionModel.ENTER_GUEST_MODE,
- )
- },
+ LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
+ isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
+ isAddUser = model == UserActionModel.ADD_USER,
+ isGuest = model == UserActionModel.ENTER_GUEST_MODE,
+ isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ isTablet = true,
+ ),
textResourceId =
- if (model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) {
- R.string.manage_users
- } else {
- LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
- isGuest = model == UserActionModel.ENTER_GUEST_MODE,
- isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
- isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
- isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
- isAddUser = model == UserActionModel.ADD_USER,
- )
- },
+ LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
+ isGuest = model == UserActionModel.ENTER_GUEST_MODE,
+ isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
+ isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
+ isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
+ isAddUser = model == UserActionModel.ADD_USER,
+ isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ isTablet = true,
+ ),
onClicked = {
userInteractor.executeAction(action = model)
// We don't finish because we want to show a dialog over the full-screen UI and
diff --git a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java
index 53da213..2efeda9 100644
--- a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java
+++ b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java
@@ -32,7 +32,8 @@
import javax.inject.Inject;
// NOT Singleton. Started per-user.
-public class NotificationChannels extends CoreStartable {
+/** */
+public class NotificationChannels implements CoreStartable {
public static String ALERTS = "ALR";
public static String SCREENSHOTS_HEADSUP = "SCN_HEADSUP";
// Deprecated. Please use or create a more specific channel that users will better understand
@@ -45,9 +46,11 @@
public static String INSTANT = "INS";
public static String SETUP = "STP";
+ private final Context mContext;
+
@Inject
public NotificationChannels(Context context) {
- super(context);
+ mContext = context;
}
public static void createAll(Context context) {
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/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
index 619e50b..a0a0372 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
@@ -564,12 +564,13 @@
/** */
@SysUISingleton
- public static class Service extends CoreStartable implements Dumpable {
+ public static class Service implements CoreStartable, Dumpable {
+ private final Context mContext;
private final GarbageMonitor mGarbageMonitor;
@Inject
public Service(Context context, GarbageMonitor garbageMonitor) {
- super(context);
+ mContext = context;
mGarbageMonitor = garbageMonitor;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto
similarity index 61%
copy from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
copy to packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto
index ca667dd..b7166d9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,10 +14,13 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone.panelstate
+syntax = "proto3";
-/** A listener interface to be notified of state change events for the notification panel. */
-fun interface PanelStateListener {
- /** Called when the panel's expansion state has changed. */
- fun onPanelStateChanged(@PanelState state: Int)
+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/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
index 87fb2a6..0b3521b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
@@ -31,18 +31,19 @@
import javax.inject.Inject;
@SysUISingleton
-public class VolumeUI extends CoreStartable {
+public class VolumeUI implements CoreStartable {
private static final String TAG = "VolumeUI";
private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
private final Handler mHandler = new Handler();
private boolean mEnabled;
+ private final Context mContext;
private VolumeDialogComponent mVolumeComponent;
@Inject
public VolumeUI(Context context, VolumeDialogComponent volumeDialogComponent) {
- super(context);
+ mContext = context;
mVolumeComponent = volumeDialogComponent;
}
@@ -59,8 +60,7 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
+ public void onConfigurationChanged(Configuration newConfig) {
if (!mEnabled) return;
mVolumeComponent.onConfigurationChanged(newConfig);
}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 3472cb1..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;
@@ -89,8 +92,10 @@
* -> WMShell starts and binds SysUI with Shell components via exported Shell interfaces
*/
@SysUISingleton
-public final class WMShell extends CoreStartable
- implements CommandQueue.Callbacks, ProtoTraceable<SystemUiTraceProto> {
+public final class WMShell implements
+ CoreStartable,
+ CommandQueue.Callbacks,
+ ProtoTraceable<SystemUiTraceProto> {
private static final String TAG = WMShell.class.getName();
private static final int INVALID_SYSUI_STATE_MASK =
SYSUI_STATE_DIALOG_SHOWING
@@ -102,12 +107,14 @@
| SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED
| SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
+ private final Context mContext;
// Shell interfaces
private final ShellInterface mShell;
private final Optional<Pip> mPipOptional;
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;
@@ -163,12 +170,14 @@
private WakefulnessLifecycle.Observer mWakefulnessObserver;
@Inject
- public WMShell(Context context,
+ public WMShell(
+ Context context,
ShellInterface shell,
Optional<Pip> pipOptional,
Optional<SplitScreen> splitScreenOptional,
Optional<OneHanded> oneHandedOptional,
Optional<FloatingTasks> floatingTasksOptional,
+ Optional<DesktopMode> desktopMode,
CommandQueue commandQueue,
ConfigurationController configurationController,
KeyguardStateController keyguardStateController,
@@ -179,7 +188,7 @@
WakefulnessLifecycle wakefulnessLifecycle,
UserTracker userTracker,
@Main Executor sysUiMainExecutor) {
- super(context);
+ mContext = context;
mShell = shell;
mCommandQueue = commandQueue;
mConfigurationController = configurationController;
@@ -190,6 +199,7 @@
mPipOptional = pipOptional;
mSplitScreenOptional = splitScreenOptional;
mOneHandedOptional = oneHandedOptional;
+ mDesktopModeOptional = desktopMode;
mWakefulnessLifecycle = wakefulnessLifecycle;
mProtoTracer = protoTracer;
mUserTracker = userTracker;
@@ -215,6 +225,7 @@
mPipOptional.ifPresent(this::initPip);
mSplitScreenOptional.ifPresent(this::initSplitScreen);
mOneHandedOptional.ifPresent(this::initOneHanded);
+ mDesktopModeOptional.ifPresent(this::initDesktopMode);
}
@VisibleForTesting
@@ -322,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/Android.bp b/packages/SystemUI/tests/Android.bp
new file mode 100644
index 0000000..3c418ed
--- /dev/null
+++ b/packages/SystemUI/tests/Android.bp
@@ -0,0 +1,50 @@
+//
+// 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 {
+ default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_test {
+ name: "SystemUITests",
+
+ dxflags: ["--multi-dex"],
+ platform_apis: true,
+ test_suites: ["device-tests"],
+ static_libs: ["SystemUI-tests"],
+ compile_multilib: "both",
+
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ "libmultiplejvmtiagentsinterferenceagent",
+ "libstaticjvmtiagent",
+ ],
+ libs: [
+ "android.test.runner",
+ "telephony-common",
+ "android.test.base",
+ ],
+ aaptflags: [
+ "--extra-packages com.android.systemui",
+ ],
+
+ // sign this with platform cert, so this test is allowed to inject key events into
+ // UI it doesn't own. This is necessary to allow screenshots to be taken
+ certificate: "platform",
+
+ additional_manifests: ["AndroidManifest.xml"],
+ manifest: "AndroidManifest-base.xml",
+}
diff --git a/packages/SystemUI/tests/Android.mk b/packages/SystemUI/tests/Android.mk
deleted file mode 100644
index ff5165d..0000000
--- a/packages/SystemUI/tests/Android.mk
+++ /dev/null
@@ -1,93 +0,0 @@
-# Copyright (C) 2011 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_USE_AAPT2 := true
-LOCAL_MODULE_TAGS := tests
-
-LOCAL_JACK_FLAGS := --multi-dex native
-LOCAL_DX_FLAGS := --multi-dex
-
-LOCAL_PACKAGE_NAME := SystemUITests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../NOTICE
-LOCAL_PRIVATE_PLATFORM_APIS := true
-LOCAL_COMPATIBILITY_SUITE := device-tests
-
-LOCAL_STATIC_ANDROID_LIBRARIES := \
- SystemUI-tests
-
-LOCAL_MULTILIB := both
-
-LOCAL_JNI_SHARED_LIBRARIES := \
- libdexmakerjvmtiagent \
- libmultiplejvmtiagentsinterferenceagent \
- libstaticjvmtiagent
-
-LOCAL_JAVA_LIBRARIES := \
- android.test.runner \
- telephony-common \
- android.test.base \
-
-LOCAL_AAPT_FLAGS := --extra-packages com.android.systemui
-
-# sign this with platform cert, so this test is allowed to inject key events into
-# UI it doesn't own. This is necessary to allow screenshots to be taken
-LOCAL_CERTIFICATE := platform
-
-LOCAL_FULL_LIBS_MANIFEST_FILES := $(LOCAL_PATH)/AndroidManifest.xml
-LOCAL_MANIFEST_FILE := AndroidManifest-base.xml
-
-# Provide jack a list of classes to exclude from code coverage.
-# This is needed because the SystemUITests compile SystemUI source directly, rather than using
-# LOCAL_INSTRUMENTATION_FOR := SystemUI.
-#
-# We want to exclude the test classes from code coverage measurements, but they share the same
-# package as the rest of SystemUI so they can't be easily filtered by package name.
-#
-# Generate a comma separated list of patterns based on the test source files under src/
-# SystemUI classes are in ../src/ so they won't be excluded.
-# Example:
-# Input files: src/com/android/systemui/Test.java src/com/android/systemui/AnotherTest.java
-# Generated exclude list: com.android.systemui.Test*,com.android.systemui.AnotherTest*
-
-# Filter all src files under src/ to just java files
-local_java_files := $(filter %.java,$(call all-java-files-under, src))
-# Transform java file names into full class names.
-# This only works if the class name matches the file name and the directory structure
-# matches the package.
-local_classes := $(subst /,.,$(patsubst src/%.java,%,$(local_java_files)))
-local_comma := ,
-local_empty :=
-local_space := $(local_empty) $(local_empty)
-# Convert class name list to jacoco exclude list
-# This appends a * to all classes and replace the space separators with commas.
-jacoco_exclude := $(subst $(space),$(comma),$(patsubst %,%*,$(local_classes)))
-
-LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.systemui.*,com.android.keyguard.*
-LOCAL_JACK_COVERAGE_EXCLUDE_FILTER := $(jacoco_exclude)
-
-ifeq ($(EXCLUDE_SYSTEMUI_TESTS),)
- include $(BUILD_PACKAGE)
-endif
-
-# Reset variables
-local_java_files :=
-local_classes :=
-local_comma :=
-local_space :=
-jacoco_exclude :=
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
new file mode 100644
index 0000000..7b9b39f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.util.AttributeSet
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class BouncerKeyguardMessageAreaTest : SysuiTestCase() {
+ class FakeBouncerKeyguardMessageArea(context: Context, attrs: AttributeSet?) :
+ BouncerKeyguardMessageArea(context, attrs) {
+ override val SHOW_DURATION_MILLIS = 0L
+ override val HIDE_DURATION_MILLIS = 0L
+ }
+ lateinit var underTest: BouncerKeyguardMessageArea
+
+ @Before
+ fun setup() {
+ underTest = FakeBouncerKeyguardMessageArea(context, null)
+ }
+
+ @Test
+ fun testSetSameMessage() {
+ val underTestSpy = spy(underTest)
+ underTestSpy.setMessage("abc")
+ underTestSpy.setMessage("abc")
+ verify(underTestSpy, times(1)).text = "abc"
+ }
+
+ @Test
+ fun testSetDifferentMessage() {
+ underTest.setMessage("abc")
+ underTest.setMessage("def")
+ assertThat(underTest.text).isEqualTo("def")
+ }
+
+ @Test
+ fun testSetNullMessage() {
+ underTest.setMessage(null)
+ assertThat(underTest.text).isEqualTo("")
+ }
+
+ @Test
+ fun testSetNullClearsPreviousMessage() {
+ underTest.setMessage("something not null")
+ assertThat(underTest.text).isEqualTo("something not null")
+
+ underTest.setMessage(null)
+ assertThat(underTest.text).isEqualTo("")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 8a2c354..1c3656d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -17,17 +17,22 @@
import android.content.BroadcastReceiver
import android.testing.AndroidTestingRunner
+import android.view.View
import android.widget.TextView
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.plugins.ClockAnimations
import com.android.systemui.plugins.ClockController
import com.android.systemui.plugins.ClockEvents
import com.android.systemui.plugins.ClockFaceController
import com.android.systemui.plugins.ClockFaceEvents
-import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.mockito.any
@@ -37,6 +42,9 @@
import com.android.systemui.util.mockito.mock
import java.util.TimeZone
import java.util.concurrent.Executor
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
@@ -57,7 +65,7 @@
class ClockEventControllerTest : SysuiTestCase() {
@JvmField @Rule val mockito = MockitoJUnit.rule()
- @Mock private lateinit var statusBarStateController: StatusBarStateController
+ @Mock private lateinit var keyguardInteractor: KeyguardInteractor
@Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock private lateinit var batteryController: BatteryController
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@@ -72,8 +80,11 @@
@Mock private lateinit var largeClockController: ClockFaceController
@Mock private lateinit var smallClockEvents: ClockFaceEvents
@Mock private lateinit var largeClockEvents: ClockFaceEvents
-
- private lateinit var clockEventController: ClockEventController
+ @Mock private lateinit var parentView: View
+ @Mock private lateinit var transitionRepository: KeyguardTransitionRepository
+ private lateinit var repository: FakeKeyguardRepository
+ @Mock private lateinit var logBuffer: LogBuffer
+ private lateinit var underTest: ClockEventController
@Before
fun setUp() {
@@ -86,8 +97,11 @@
whenever(clock.events).thenReturn(events)
whenever(clock.animations).thenReturn(animations)
- clockEventController = ClockEventController(
- statusBarStateController,
+ repository = FakeKeyguardRepository()
+
+ underTest = ClockEventController(
+ KeyguardInteractor(repository = repository),
+ KeyguardTransitionInteractor(repository = transitionRepository),
broadcastDispatcher,
batteryController,
keyguardUpdateMonitor,
@@ -96,33 +110,36 @@
context,
mainExecutor,
bgExecutor,
+ logBuffer,
featureFlags
)
+ underTest.clock = clock
+
+ runBlocking(IMMEDIATE) {
+ underTest.registerListeners(parentView)
+
+ repository.setDozing(true)
+ repository.setDozeAmount(1f)
+ }
}
@Test
fun clockSet_validateInitialization() {
- clockEventController.clock = clock
-
verify(clock).initialize(any(), anyFloat(), anyFloat())
}
@Test
fun clockUnset_validateState() {
- clockEventController.clock = clock
- clockEventController.clock = null
+ underTest.clock = null
- assertEquals(clockEventController.clock, null)
+ assertEquals(underTest.clock, null)
}
@Test
- fun themeChanged_verifyClockPaletteUpdated() {
- clockEventController.clock = clock
+ fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) {
verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
- clockEventController.registerListeners()
-
val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
verify(configurationController).addCallback(capture(captor))
captor.value.onThemeChanged()
@@ -131,13 +148,10 @@
}
@Test
- fun fontChanged_verifyFontSizeUpdated() {
- clockEventController.clock = clock
+ fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) {
verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
- clockEventController.registerListeners()
-
val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
verify(configurationController).addCallback(capture(captor))
captor.value.onDensityOrFontScaleChanged()
@@ -146,10 +160,7 @@
}
@Test
- fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() {
- clockEventController.clock = clock
- clockEventController.registerListeners()
-
+ fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) {
val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
verify(batteryController).addCallback(capture(batteryCaptor))
val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
@@ -161,26 +172,21 @@
}
@Test
- fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() {
- clockEventController.clock = clock
- clockEventController.registerListeners()
+ fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() =
+ runBlocking(IMMEDIATE) {
+ val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+ verify(batteryController).addCallback(capture(batteryCaptor))
+ val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+ verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
+ keyguardCaptor.value.onKeyguardVisibilityChanged(true)
+ batteryCaptor.value.onBatteryLevelChanged(10, false, true)
+ batteryCaptor.value.onBatteryLevelChanged(10, false, true)
- val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
- verify(batteryController).addCallback(capture(batteryCaptor))
- val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
- verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
- keyguardCaptor.value.onKeyguardVisibilityChanged(true)
- batteryCaptor.value.onBatteryLevelChanged(10, false, true)
- batteryCaptor.value.onBatteryLevelChanged(10, false, true)
-
- verify(animations, times(1)).charge()
- }
+ verify(animations, times(1)).charge()
+ }
@Test
- fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() {
- clockEventController.clock = clock
- clockEventController.registerListeners()
-
+ fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) {
val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
verify(batteryController).addCallback(capture(batteryCaptor))
val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
@@ -192,25 +198,20 @@
}
@Test
- fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() {
- clockEventController.clock = clock
- clockEventController.registerListeners()
+ fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() =
+ runBlocking(IMMEDIATE) {
+ val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+ verify(batteryController).addCallback(capture(batteryCaptor))
+ val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+ verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
+ keyguardCaptor.value.onKeyguardVisibilityChanged(true)
+ batteryCaptor.value.onBatteryLevelChanged(10, false, false)
- val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
- verify(batteryController).addCallback(capture(batteryCaptor))
- val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
- verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
- keyguardCaptor.value.onKeyguardVisibilityChanged(true)
- batteryCaptor.value.onBatteryLevelChanged(10, false, false)
-
- verify(animations, never()).charge()
- }
+ verify(animations, never()).charge()
+ }
@Test
- fun localeCallback_verifyClockNotified() {
- clockEventController.clock = clock
- clockEventController.registerListeners()
-
+ fun localeCallback_verifyClockNotified() = runBlocking(IMMEDIATE) {
val captor = argumentCaptor<BroadcastReceiver>()
verify(broadcastDispatcher).registerReceiver(
capture(captor), any(), eq(null), eq(null), anyInt(), eq(null)
@@ -221,10 +222,7 @@
}
@Test
- fun keyguardCallback_visibilityChanged_clockDozeCalled() {
- clockEventController.clock = clock
- clockEventController.registerListeners()
-
+ fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) {
val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(captor))
@@ -236,10 +234,7 @@
}
@Test
- fun keyguardCallback_timeFormat_clockNotified() {
- clockEventController.clock = clock
- clockEventController.registerListeners()
-
+ fun keyguardCallback_timeFormat_clockNotified() = runBlocking(IMMEDIATE) {
val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(captor))
captor.value.onTimeFormatChanged("12h")
@@ -248,11 +243,8 @@
}
@Test
- fun keyguardCallback_timezoneChanged_clockNotified() {
+ fun keyguardCallback_timezoneChanged_clockNotified() = runBlocking(IMMEDIATE) {
val mockTimeZone = mock<TimeZone>()
- clockEventController.clock = clock
- clockEventController.registerListeners()
-
val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(captor))
captor.value.onTimeZoneChanged(mockTimeZone)
@@ -261,10 +253,7 @@
}
@Test
- fun keyguardCallback_userSwitched_clockNotified() {
- clockEventController.clock = clock
- clockEventController.registerListeners()
-
+ fun keyguardCallback_userSwitched_clockNotified() = runBlocking(IMMEDIATE) {
val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(captor))
captor.value.onUserSwitchComplete(10)
@@ -273,25 +262,27 @@
}
@Test
- fun keyguardCallback_verifyKeyguardChanged() {
- clockEventController.clock = clock
- clockEventController.registerListeners()
+ fun keyguardCallback_verifyKeyguardChanged() = runBlocking(IMMEDIATE) {
+ val job = underTest.listenForDozeAmount(this)
+ repository.setDozeAmount(0.4f)
- val captor = argumentCaptor<StatusBarStateController.StateListener>()
- verify(statusBarStateController).addCallback(capture(captor))
- captor.value.onDozeAmountChanged(0.4f, 0.6f)
+ yield()
verify(animations).doze(0.4f)
+
+ job.cancel()
}
@Test
- fun unregisterListeners_validate() {
- clockEventController.clock = clock
- clockEventController.unregisterListeners()
+ fun unregisterListeners_validate() = runBlocking(IMMEDIATE) {
+ underTest.unregisterListeners()
verify(broadcastDispatcher).unregisterReceiver(any())
verify(configurationController).removeCallback(any())
verify(batteryController).removeCallback(any())
verify(keyguardUpdateMonitor).removeCallback(any())
- verify(statusBarStateController).removeCallback(any())
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
}
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt
index aa671d1..91b544b 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt
@@ -17,7 +17,6 @@
package com.android.keyguard
import android.hardware.biometrics.BiometricSourceType
-import org.mockito.Mockito.verify
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
@@ -30,9 +29,10 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
-import org.mockito.Captor
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@@ -63,7 +63,6 @@
whenever(keyguardUpdateMonitor.strongAuthTracker).thenReturn(strongAuthTracker)
whenever(sessionTracker.getSessionId(anyInt())).thenReturn(sessionId)
keyguardBiometricLockoutLogger = KeyguardBiometricLockoutLogger(
- mContext,
uiEventLogger,
keyguardUpdateMonitor,
sessionTracker)
@@ -195,4 +194,4 @@
verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallbackCaptor.capture())
updateMonitorCallback = updateMonitorCallbackCaptor.value
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 400caa3..61c7bb5 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;
@@ -43,8 +44,8 @@
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.plugins.ClockAnimations;
import com.android.systemui.plugins.ClockController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shared.clocks.AnimatableClockView;
@@ -103,8 +104,6 @@
private FrameLayout mLargeClockFrame;
@Mock
private SecureSettings mSecureSettings;
- @Mock
- private FeatureFlags mFeatureFlags;
private final View mFakeSmartspaceView = new View(mContext);
@@ -141,8 +140,7 @@
mSecureSettings,
mExecutor,
mDumpManager,
- mClockEventController,
- mFeatureFlags
+ mClockEventController
);
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
@@ -262,9 +260,22 @@
verify(mView).switchToClock(KeyguardClockSwitch.SMALL, /* animate */ true);
}
+ @Test
+ public void testGetClockAnimationsForwardsToClock() {
+ ClockController mockClockController = mock(ClockController.class);
+ ClockAnimations mockClockAnimations = mock(ClockAnimations.class);
+ when(mClockEventController.getClock()).thenReturn(mockClockController);
+ when(mockClockController.getAnimations()).thenReturn(mockClockAnimations);
+
+ Rect r1 = new Rect(1, 2, 3, 4);
+ Rect r2 = new Rect(5, 6, 7, 8);
+ mController.getClockAnimations().onPositionUpdated(r1, r2, 0.2f);
+ verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.2f);
+ }
+
private void verifyAttachment(VerificationMode times) {
verify(mClockRegistry, times).registerClockChangeListener(
any(ClockRegistry.ClockChangeListener.class));
- verify(mClockEventController, times).registerListeners();
+ verify(mClockEventController, times).registerListeners(mView);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
index 69524e5..5d2b0ca 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
@@ -17,13 +17,11 @@
package com.android.keyguard;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
-import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -92,19 +90,4 @@
mMessageAreaController.setIsVisible(true);
verify(mKeyguardMessageArea).setIsVisible(true);
}
-
- @Test
- public void testSetMessageIfEmpty_empty() {
- mMessageAreaController.setMessage("");
- mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
- verify(mKeyguardMessageArea).setMessage(R.string.keyguard_enter_your_pin);
- }
-
- @Test
- public void testSetMessageIfEmpty_notEmpty() {
- mMessageAreaController.setMessage("abc");
- mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
- verify(mKeyguardMessageArea, never()).setMessage(getContext()
- .getResources().getText(R.string.keyguard_enter_your_pin));
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index b89dbd9..b369098 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -114,9 +114,8 @@
}
@Test
- fun onResume_testSetInitialText() {
- keyguardPasswordViewController.onResume(KeyguardSecurityView.SCREEN_ON)
- verify(mKeyguardMessageAreaController)
- .setMessageIfEmpty(R.string.keyguard_enter_your_password)
+ fun startAppearAnimation() {
+ keyguardPasswordViewController.startAppearAnimation()
+ verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index 3262a77..9eff704 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -100,16 +100,16 @@
}
@Test
- fun onPause_clearsTextField() {
+ fun onPause_resetsText() {
mKeyguardPatternViewController.init()
mKeyguardPatternViewController.onPause()
- verify(mKeyguardMessageAreaController).setMessage("")
+ verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
}
+
@Test
- fun onResume_setInitialText() {
- mKeyguardPatternViewController.onResume(KeyguardSecurityView.SCREEN_ON)
- verify(mKeyguardMessageAreaController)
- .setMessageIfEmpty(R.string.keyguard_enter_your_pattern)
+ fun startAppearAnimation() {
+ mKeyguardPatternViewController.startAppearAnimation()
+ verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index 97d556b..ce1101f 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -113,11 +113,4 @@
mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON);
verify(mPasswordEntry).requestFocus();
}
-
- @Test
- public void onResume_setInitialText() {
- mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON);
- verify(mKeyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin);
- }
}
-
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 9e5bfe5..d9efdea 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -98,6 +98,6 @@
@Test
fun startAppearAnimation() {
pinViewController.startAppearAnimation()
- verify(keyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin)
+ verify(keyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index c6ebaa8..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;
@@ -221,15 +223,17 @@
public void onResourcesUpdate_callsThroughOnRotationChange() {
// Rotation is the same, shouldn't cause an update
mKeyguardSecurityContainerController.updateResources();
- verify(mView, never()).initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ verify(mView, never()).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
+ eq(mUserSwitcherController),
+ any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
// Update rotation. Should trigger update
mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
mKeyguardSecurityContainerController.updateResources();
- verify(mView).initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
+ eq(mUserSwitcherController),
+ any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
}
private void touchDown() {
@@ -263,8 +267,9 @@
.thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController);
mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern);
- verify(mView).initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
+ eq(mUserSwitcherController),
+ any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
}
@Test
@@ -275,8 +280,9 @@
.thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController);
mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern);
- verify(mView).initMode(MODE_ONE_HANDED, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ verify(mView).initMode(eq(MODE_ONE_HANDED), eq(mGlobalSettings), eq(mFalsingManager),
+ eq(mUserSwitcherController),
+ any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
}
@Test
@@ -285,8 +291,26 @@
setupGetSecurityView();
mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Password);
- verify(mView).initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
+ eq(mUserSwitcherController),
+ any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
+ }
+
+ @Test
+ public void addUserSwitcherCallback() {
+ ArgumentCaptor<KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback>
+ captor = ArgumentCaptor.forClass(
+ KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class);
+
+ setupGetSecurityView();
+
+ mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Password);
+ verify(mView).initMode(anyInt(), any(GlobalSettings.class), any(FalsingManager.class),
+ any(UserSwitcherController.class),
+ captor.capture());
+ captor.getValue().showUnlockToContinueMessage();
+ verify(mKeyguardPasswordViewControllerMock).showMessage(
+ getContext().getString(R.string.keyguard_unlock_to_continue), null);
}
@Test
@@ -453,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/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 52f8825..82d3ca7 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -119,7 +119,7 @@
int systemBarInsetAmount = 0;
mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ mUserSwitcherController, () -> {});
Insets imeInset = Insets.of(0, 0, 0, imeInsetAmount);
Insets systemBarInset = Insets.of(0, 0, 0, systemBarInsetAmount);
@@ -141,7 +141,7 @@
int systemBarInsetAmount = paddingBottom + 1;
mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ mUserSwitcherController, () -> {});
Insets imeInset = Insets.of(0, 0, 0, imeInsetAmount);
Insets systemBarInset = Insets.of(0, 0, 0, systemBarInsetAmount);
@@ -158,9 +158,10 @@
@Test
public void testDefaultViewMode() {
mKeyguardSecurityContainer.initMode(MODE_ONE_HANDED, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ mUserSwitcherController, () -> {
+ });
mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ mUserSwitcherController, () -> {});
ConstraintSet.Constraint viewFlipperConstraint =
getViewConstraint(mSecurityViewFlipper.getId());
assertThat(viewFlipperConstraint.layout.topToTop).isEqualTo(PARENT_ID);
@@ -377,7 +378,7 @@
private void setupUserSwitcher() {
when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_RIGHT);
mKeyguardSecurityContainer.initMode(KeyguardSecurityContainer.MODE_USER_SWITCHER,
- mGlobalSettings, mFalsingManager, mUserSwitcherController);
+ mGlobalSettings, mFalsingManager, mUserSwitcherController, () -> {});
}
private ArrayList<UserRecord> buildUserRecords(int count) {
@@ -387,7 +388,8 @@
0 /* flags */);
users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */,
false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */,
- false /* isAddSupervisedUser */, null /* enforcedAdmin */));
+ false /* isAddSupervisedUser */, null /* enforcedAdmin */,
+ false /* isManageUsers */));
}
return users;
}
@@ -395,7 +397,7 @@
private void setupForUpdateKeyguardPosition(boolean oneHandedMode) {
int mode = oneHandedMode ? MODE_ONE_HANDED : MODE_DEFAULT;
mKeyguardSecurityContainer.initMode(mode, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ mUserSwitcherController, () -> {});
}
/** Get the ConstraintLayout constraint of the view. */
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/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index df10dfe..181839a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -36,6 +36,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -231,7 +232,7 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mExecutor.runAllReady();
}
@@ -255,6 +256,7 @@
});
mScreenDecorations.mDisplayInfo = mDisplayInfo;
doReturn(1f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio();
+ doNothing().when(mScreenDecorations).updateOverlayProviderViews(any());
reset(mTunerService);
try {
@@ -1005,18 +1007,13 @@
assertEquals(new Size(3, 3), resDelegate.getTopRoundedSize());
assertEquals(new Size(4, 4), resDelegate.getBottomRoundedSize());
- setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
- getTestsDrawable(com.android.systemui.tests.R.drawable.rounded4px)
- /* roundedTopDrawable */,
- getTestsDrawable(com.android.systemui.tests.R.drawable.rounded5px)
- /* roundedBottomDrawable */,
- 0 /* roundedPadding */, true /* privacyDot */, false /* faceScanning*/);
+ doReturn(2f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio();
mDisplayInfo.rotation = Surface.ROTATION_270;
mScreenDecorations.onConfigurationChanged(null);
- assertEquals(new Size(4, 4), resDelegate.getTopRoundedSize());
- assertEquals(new Size(5, 5), resDelegate.getBottomRoundedSize());
+ assertEquals(new Size(6, 6), resDelegate.getTopRoundedSize());
+ assertEquals(new Size(8, 8), resDelegate.getBottomRoundedSize());
}
@Test
@@ -1293,51 +1290,6 @@
}
@Test
- public void testOnDisplayChanged_hwcLayer() {
- setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
- null /* roundedTopDrawable */, null /* roundedBottomDrawable */,
- 0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */);
- final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport();
- decorationSupport.format = PixelFormat.R_8;
- doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport();
-
- // top cutout
- mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP));
-
- mScreenDecorations.start();
-
- final ScreenDecorHwcLayer hwcLayer = mScreenDecorations.mScreenDecorHwcLayer;
- spyOn(hwcLayer);
- doReturn(mDisplay).when(hwcLayer).getDisplay();
-
- mScreenDecorations.mDisplayListener.onDisplayChanged(1);
-
- verify(hwcLayer, times(1)).onDisplayChanged(any());
- }
-
- @Test
- public void testOnDisplayChanged_nonHwcLayer() {
- setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
- null /* roundedTopDrawable */, null /* roundedBottomDrawable */,
- 0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */);
-
- // top cutout
- mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP));
-
- mScreenDecorations.start();
-
- final ScreenDecorations.DisplayCutoutView cutoutView = (ScreenDecorations.DisplayCutoutView)
- mScreenDecorations.getOverlayView(R.id.display_cutout);
- assertNotNull(cutoutView);
- spyOn(cutoutView);
- doReturn(mDisplay).when(cutoutView).getDisplay();
-
- mScreenDecorations.mDisplayListener.onDisplayChanged(1);
-
- verify(cutoutView, times(1)).onDisplayChanged(any());
- }
-
- @Test
public void testHasSameProvidersWithNullOverlays() {
setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
null /* roundedTopDrawable */, null /* roundedBottomDrawable */,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
index 986e7cd..6ab54a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
@@ -935,6 +935,251 @@
checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
}
+ /* ******** start of animatesViewRemoval_includeMarginsTrue tests ******** */
+ @Test
+ fun animatesViewRemoval_includeMarginsTrue_center() {
+ setUpRootWithChildren(includeMarginsOnFirstChild = true)
+ val removedChild = rootView.getChildAt(0)
+ val originalLeft = removedChild.left
+ val originalTop = removedChild.top
+ val originalRight = removedChild.right
+ val originalBottom = removedChild.bottom
+
+ val success = ViewHierarchyAnimator.animateRemoval(
+ removedChild,
+ destination = ViewHierarchyAnimator.Hotspot.CENTER,
+ includeMargins = true,
+ )
+ forceLayout()
+
+ assertTrue(success)
+ assertNotNull(removedChild.getTag(R.id.tag_animator))
+ advanceAnimation(removedChild, 1.0f)
+ val expectedX = ((originalLeft - M_LEFT) + (originalRight + M_RIGHT)) / 2
+ val expectedY = ((originalTop - M_TOP) + (originalBottom + M_BOTTOM)) / 2
+
+ checkBounds(
+ removedChild,
+ l = expectedX,
+ t = expectedY,
+ r = expectedX,
+ b = expectedY
+ )
+ }
+
+ @Test
+ fun animatesViewRemoval_includeMarginsTrue_left() {
+ setUpRootWithChildren(includeMarginsOnFirstChild = true)
+ val removedChild = rootView.getChildAt(0)
+ val originalLeft = removedChild.left
+ val originalTop = removedChild.top
+ val originalBottom = removedChild.bottom
+
+ val success = ViewHierarchyAnimator.animateRemoval(
+ removedChild,
+ destination = ViewHierarchyAnimator.Hotspot.LEFT,
+ includeMargins = true,
+ )
+ forceLayout()
+
+ assertTrue(success)
+ assertNotNull(removedChild.getTag(R.id.tag_animator))
+ advanceAnimation(removedChild, 1.0f)
+ checkBounds(
+ removedChild,
+ l = originalLeft - M_LEFT,
+ t = originalTop,
+ r = originalLeft - M_LEFT,
+ b = originalBottom
+ )
+ }
+
+ @Test
+ fun animatesViewRemoval_includeMarginsTrue_topLeft() {
+ setUpRootWithChildren(includeMarginsOnFirstChild = true)
+ val removedChild = rootView.getChildAt(0)
+ val originalLeft = removedChild.left
+ val originalTop = removedChild.top
+
+ val success = ViewHierarchyAnimator.animateRemoval(
+ removedChild,
+ destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT,
+ includeMargins = true,
+ )
+ forceLayout()
+
+ assertTrue(success)
+ assertNotNull(removedChild.getTag(R.id.tag_animator))
+ advanceAnimation(removedChild, 1.0f)
+ checkBounds(
+ removedChild,
+ l = originalLeft - M_LEFT,
+ t = originalTop - M_TOP,
+ r = originalLeft - M_LEFT,
+ b = originalTop - M_TOP
+ )
+ }
+
+ @Test
+ fun animatesViewRemoval_includeMarginsTrue_top() {
+ setUpRootWithChildren(includeMarginsOnFirstChild = true)
+ val removedChild = rootView.getChildAt(0)
+ val originalLeft = removedChild.left
+ val originalTop = removedChild.top
+ val originalRight = removedChild.right
+
+ val success = ViewHierarchyAnimator.animateRemoval(
+ removedChild,
+ destination = ViewHierarchyAnimator.Hotspot.TOP,
+ includeMargins = true,
+ )
+ forceLayout()
+
+ assertTrue(success)
+ assertNotNull(removedChild.getTag(R.id.tag_animator))
+ advanceAnimation(removedChild, 1.0f)
+ checkBounds(
+ removedChild,
+ l = originalLeft,
+ t = originalTop - M_TOP,
+ r = originalRight,
+ b = originalTop - M_TOP
+ )
+ }
+
+ @Test
+ fun animatesViewRemoval_includeMarginsTrue_topRight() {
+ setUpRootWithChildren(includeMarginsOnFirstChild = true)
+ val removedChild = rootView.getChildAt(0)
+ val originalTop = removedChild.top
+ val originalRight = removedChild.right
+
+ val success = ViewHierarchyAnimator.animateRemoval(
+ removedChild,
+ destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT,
+ includeMargins = true,
+ )
+ forceLayout()
+
+ assertTrue(success)
+ assertNotNull(removedChild.getTag(R.id.tag_animator))
+ advanceAnimation(removedChild, 1.0f)
+ checkBounds(
+ removedChild,
+ l = originalRight + M_RIGHT,
+ t = originalTop - M_TOP,
+ r = originalRight + M_RIGHT,
+ b = originalTop - M_TOP
+ )
+ }
+
+ @Test
+ fun animatesViewRemoval_includeMarginsTrue_right() {
+ setUpRootWithChildren(includeMarginsOnFirstChild = true)
+ val removedChild = rootView.getChildAt(0)
+ val originalTop = removedChild.top
+ val originalRight = removedChild.right
+ val originalBottom = removedChild.bottom
+
+ val success = ViewHierarchyAnimator.animateRemoval(
+ removedChild,
+ destination = ViewHierarchyAnimator.Hotspot.RIGHT,
+ includeMargins = true,
+ )
+ forceLayout()
+
+ assertTrue(success)
+ assertNotNull(removedChild.getTag(R.id.tag_animator))
+ advanceAnimation(removedChild, 1.0f)
+ checkBounds(
+ removedChild,
+ l = originalRight + M_RIGHT,
+ t = originalTop,
+ r = originalRight + M_RIGHT,
+ b = originalBottom
+ )
+ }
+
+ @Test
+ fun animatesViewRemoval_includeMarginsTrue_bottomRight() {
+ setUpRootWithChildren(includeMarginsOnFirstChild = true)
+ val removedChild = rootView.getChildAt(0)
+ val originalRight = removedChild.right
+ val originalBottom = removedChild.bottom
+
+ val success = ViewHierarchyAnimator.animateRemoval(
+ removedChild,
+ destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT,
+ includeMargins = true,
+ )
+ forceLayout()
+
+ assertTrue(success)
+ assertNotNull(removedChild.getTag(R.id.tag_animator))
+ advanceAnimation(removedChild, 1.0f)
+ checkBounds(
+ removedChild,
+ l = originalRight + M_RIGHT,
+ t = originalBottom + M_BOTTOM,
+ r = originalRight + M_RIGHT,
+ b = originalBottom + M_BOTTOM
+ )
+ }
+
+ @Test
+ fun animatesViewRemoval_includeMarginsTrue_bottom() {
+ setUpRootWithChildren(includeMarginsOnFirstChild = true)
+ val removedChild = rootView.getChildAt(0)
+ val originalLeft = removedChild.left
+ val originalRight = removedChild.right
+ val originalBottom = removedChild.bottom
+
+ val success = ViewHierarchyAnimator.animateRemoval(
+ removedChild,
+ destination = ViewHierarchyAnimator.Hotspot.BOTTOM,
+ includeMargins = true,
+ )
+ forceLayout()
+
+ assertTrue(success)
+ assertNotNull(removedChild.getTag(R.id.tag_animator))
+ advanceAnimation(removedChild, 1.0f)
+ checkBounds(
+ removedChild,
+ l = originalLeft,
+ t = originalBottom + M_BOTTOM,
+ r = originalRight,
+ b = originalBottom + M_BOTTOM
+ )
+ }
+
+ @Test
+ fun animatesViewRemoval_includeMarginsTrue_bottomLeft() {
+ setUpRootWithChildren(includeMarginsOnFirstChild = true)
+ val removedChild = rootView.getChildAt(0)
+ val originalLeft = removedChild.left
+ val originalBottom = removedChild.bottom
+
+ val success = ViewHierarchyAnimator.animateRemoval(
+ removedChild,
+ destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT,
+ includeMargins = true,
+ )
+ forceLayout()
+
+ assertTrue(success)
+ assertNotNull(removedChild.getTag(R.id.tag_animator))
+ advanceAnimation(removedChild, 1.0f)
+ checkBounds(
+ removedChild,
+ l = originalLeft - M_LEFT,
+ t = originalBottom + M_BOTTOM,
+ r = originalLeft - M_LEFT,
+ b = originalBottom + M_BOTTOM
+ )
+ }
+ /* ******** end of animatesViewRemoval_includeMarginsTrue tests ******** */
+
@Test
fun animatesChildrenDuringViewRemoval() {
setUpRootWithChildren()
@@ -1215,7 +1460,7 @@
checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
}
- private fun setUpRootWithChildren() {
+ private fun setUpRootWithChildren(includeMarginsOnFirstChild: Boolean = false) {
rootView = LinearLayout(mContext)
(rootView as LinearLayout).orientation = LinearLayout.HORIZONTAL
(rootView as LinearLayout).weightSum = 1f
@@ -1229,13 +1474,26 @@
val secondChild = View(mContext)
rootView.addView(secondChild)
- val childParams = LinearLayout.LayoutParams(
+ val firstChildParams = LinearLayout.LayoutParams(
0 /* width */,
LinearLayout.LayoutParams.MATCH_PARENT
)
- childParams.weight = 0.5f
- firstChild.layoutParams = childParams
- secondChild.layoutParams = childParams
+ firstChildParams.weight = 0.5f
+ if (includeMarginsOnFirstChild) {
+ firstChildParams.leftMargin = M_LEFT
+ firstChildParams.topMargin = M_TOP
+ firstChildParams.rightMargin = M_RIGHT
+ firstChildParams.bottomMargin = M_BOTTOM
+ }
+ firstChild.layoutParams = firstChildParams
+
+ val secondChildParams = LinearLayout.LayoutParams(
+ 0 /* width */,
+ LinearLayout.LayoutParams.MATCH_PARENT
+ )
+ secondChildParams.weight = 0.5f
+ secondChild.layoutParams = secondChildParams
+
firstGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */)
(firstGrandChild.layoutParams as RelativeLayout.LayoutParams)
.addRule(RelativeLayout.ALIGN_PARENT_START)
@@ -1315,3 +1573,9 @@
}
}
}
+
+// Margin values.
+private const val M_LEFT = 14
+private const val M_TOP = 16
+private const val M_RIGHT = 18
+private const val M_BOTTOM = 20
diff --git a/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java
new file mode 100644
index 0000000..dd9683f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.assist.ui;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+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)
+public class DisplayUtilsTest extends SysuiTestCase {
+
+ @Mock
+ Resources mResources;
+ @Mock
+ Context mMockContext;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testGetCornerRadii_noOverlay() {
+ assertEquals(0, DisplayUtils.getCornerRadiusBottom(mContext));
+ assertEquals(0, DisplayUtils.getCornerRadiusTop(mContext));
+ }
+
+ @Test
+ public void testGetCornerRadii_onlyDefaultOverridden() {
+ when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size)).thenReturn(100);
+ when(mMockContext.getResources()).thenReturn(mResources);
+
+ assertEquals(100, DisplayUtils.getCornerRadiusBottom(mMockContext));
+ assertEquals(100, DisplayUtils.getCornerRadiusTop(mMockContext));
+ }
+
+ @Test
+ public void testGetCornerRadii_allOverridden() {
+ when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size)).thenReturn(100);
+ when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size_top)).thenReturn(
+ 150);
+ when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size_bottom)).thenReturn(
+ 200);
+ when(mMockContext.getResources()).thenReturn(mResources);
+
+ assertEquals(200, DisplayUtils.getCornerRadiusBottom(mMockContext));
+ assertEquals(150, DisplayUtils.getCornerRadiusTop(mMockContext));
+ }
+}
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 5c564e6..c85334d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -17,13 +17,24 @@
package com.android.systemui.biometrics
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.provider.Settings
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
-import android.view.*
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.Surface
import android.view.Surface.Rotation
+import android.view.View
+import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardUpdateMonitor
@@ -32,11 +43,11 @@
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.phone.SystemUIDialogManager
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.time.SystemClock
@@ -52,8 +63,8 @@
import org.mockito.Mock
import org.mockito.Mockito.mock
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 REQUEST_ID = 2L
@@ -75,7 +86,7 @@
@Mock private lateinit var windowManager: WindowManager
@Mock private lateinit var accessibilityManager: AccessibilityManager
@Mock private lateinit var statusBarStateController: StatusBarStateController
- @Mock private lateinit var panelExpansionStateManager: PanelExpansionStateManager
+ @Mock private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
@Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var dialogManager: SystemUIDialogManager
@@ -114,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, panelExpansionStateManager, statusBarKeyguardViewManager,
+ statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager,
keyguardUpdateMonitor, dialogManager, dumpManager, transitionController,
configurationController, systemClock, keyguardStateController,
unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason,
- controllerCallback, onTouch, activityLaunchAnimator
+ controllerCallback, onTouch, activityLaunchAnimator, isDebuggable
)
block()
}
@@ -141,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) {
@@ -362,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) {
@@ -384,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) {
@@ -406,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 53e8c6e..28e13b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -38,6 +38,7 @@
import static org.mockito.Mockito.when;
import android.graphics.Rect;
+import android.hardware.biometrics.BiometricFingerprintConstants;
import android.hardware.biometrics.BiometricOverlayConstants;
import android.hardware.biometrics.ComponentInfoInternal;
import android.hardware.biometrics.SensorProperties;
@@ -71,12 +72,12 @@
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.SystemUIDialogManager;
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.Execution;
@@ -168,6 +169,8 @@
@Mock
private LatencyTracker mLatencyTracker;
private FakeExecutor mFgExecutor;
+ @Mock
+ private UdfpsDisplayMode mUdfpsDisplayMode;
// Stuff for configuring mocks
@Mock
@@ -245,7 +248,7 @@
mWindowManager,
mStatusBarStateController,
mFgExecutor,
- new PanelExpansionStateManager(),
+ new ShadeExpansionStateManager(),
mStatusBarKeyguardViewManager,
mDumpManager,
mKeyguardUpdateMonitor,
@@ -257,7 +260,6 @@
mVibrator,
mUdfpsHapticsSimulator,
mUdfpsShell,
- Optional.of(mDisplayModeProvider),
mKeyguardStateController,
mDisplayManager,
mHandler,
@@ -274,6 +276,7 @@
verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
mScreenObserver = mScreenObserverCaptor.getValue();
mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, new UdfpsOverlayParams());
+ mUdfpsController.setUdfpsDisplayMode(mUdfpsDisplayMode);
}
@Test
@@ -658,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();
}
@@ -682,7 +685,7 @@
}
@Test
- public void aodInterruptCancelTimeoutActionWhenFingerUp() throws RemoteException {
+ public void aodInterruptCancelTimeoutActionOnFingerUp() throws RemoteException {
when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true);
when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
@@ -734,6 +737,56 @@
}
@Test
+ public void aodInterruptCancelTimeoutActionOnAcquired() throws RemoteException {
+ when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true);
+ when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
+
+ // GIVEN AOD interrupt
+ mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+ BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+ mScreenObserver.onScreenTurnedOn();
+ mFgExecutor.runAllReady();
+ mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
+ mFgExecutor.runAllReady();
+
+ // Configure UdfpsView to accept the acquired event
+ when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+
+ // WHEN acquired is received
+ mOverlayController.onAcquired(TEST_UDFPS_SENSOR_ID,
+ BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD);
+
+ // Configure UdfpsView to accept the ACTION_DOWN event
+ when(mUdfpsView.isDisplayConfigured()).thenReturn(false);
+
+ // WHEN ACTION_DOWN is received
+ verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
+ MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0);
+ mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent);
+ mBiometricsExecutor.runAllReady();
+ downEvent.recycle();
+
+ // WHEN ACTION_MOVE is received
+ MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0);
+ mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent);
+ mBiometricsExecutor.runAllReady();
+ moveEvent.recycle();
+ mFgExecutor.runAllReady();
+
+ // Configure UdfpsView to accept the finger up event
+ when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+
+ // WHEN it times out
+ mFgExecutor.advanceClockToNext();
+ mFgExecutor.runAllReady();
+
+ // THEN the display should be unconfigured once. If the timeout action is not
+ // cancelled, the display would be unconfigured twice which would cause two
+ // FP attempts.
+ verify(mUdfpsView, times(1)).unconfigureDisplay();
+ }
+
+ @Test
public void aodInterruptScreenOff() throws RemoteException {
// GIVEN screen off
mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
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/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
index b61bda8..c0f9c82 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
@@ -41,14 +41,14 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.shade.ShadeExpansionListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.SystemUIDialogManager;
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -76,7 +76,7 @@
@Mock
private StatusBarStateController mStatusBarStateController;
@Mock
- private PanelExpansionStateManager mPanelExpansionStateManager;
+ private ShadeExpansionStateManager mShadeExpansionStateManager;
@Mock
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@Mock
@@ -109,8 +109,8 @@
@Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerCaptor;
private StatusBarStateController.StateListener mStatusBarStateListener;
- @Captor private ArgumentCaptor<PanelExpansionListener> mExpansionListenerCaptor;
- private List<PanelExpansionListener> mExpansionListeners;
+ @Captor private ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor;
+ private List<ShadeExpansionListener> mExpansionListeners;
@Captor private ArgumentCaptor<StatusBarKeyguardViewManager.AlternateAuthInterceptor>
mAltAuthInterceptorCaptor;
@@ -130,7 +130,7 @@
mController = new UdfpsKeyguardViewController(
mView,
mStatusBarStateController,
- mPanelExpansionStateManager,
+ mShadeExpansionStateManager,
mStatusBarKeyguardViewManager,
mKeyguardUpdateMonitor,
mDumpManager,
@@ -182,8 +182,8 @@
mController.onViewDetached();
verify(mStatusBarStateController).removeCallback(mStatusBarStateListener);
- for (PanelExpansionListener listener : mExpansionListeners) {
- verify(mPanelExpansionStateManager).removeExpansionListener(listener);
+ for (ShadeExpansionListener listener : mExpansionListeners) {
+ verify(mShadeExpansionStateManager).removeExpansionListener(listener);
}
verify(mKeyguardStateController).removeCallback(mKeyguardStateControllerCallback);
}
@@ -513,7 +513,7 @@
}
private void captureStatusBarExpansionListeners() {
- verify(mPanelExpansionStateManager, times(2))
+ verify(mShadeExpansionStateManager, times(2))
.addExpansionListener(mExpansionListenerCaptor.capture());
// first (index=0) is from super class, UdfpsAnimationViewController.
// second (index=1) is from UdfpsKeyguardViewController
@@ -521,10 +521,10 @@
}
private void updateStatusBarExpansion(float fraction, boolean expanded) {
- PanelExpansionChangeEvent event =
- new PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent event =
+ new ShadeExpansionChangeEvent(
fraction, expanded, /* tracking= */ false, /* dragDownPxAmount= */ 0f);
- for (PanelExpansionListener listener : mExpansionListeners) {
+ for (ShadeExpansionListener listener : mExpansionListeners) {
listener.onPanelExpansionChanged(event);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
index b2a9e82..6bc7308 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
@@ -145,6 +145,35 @@
}
@Test
+ public void testIsFalseTouch_SeekBar_FalseTouch() {
+ when(mClassifierA.classifyGesture(anyInt(), anyDouble(), anyDouble()))
+ .thenReturn(mFalsedResult);
+ when(mSingleTapClassfier.isTap(any(List.class), anyDouble())).thenReturn(mFalsedResult);
+ assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).isTrue();
+ }
+
+ @Test
+ public void testIsFalseTouch_SeekBar_RealTouch() {
+ when(mSingleTapClassfier.isTap(any(List.class), anyDouble())).thenReturn(mFalsedResult);
+ assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).isFalse();
+ }
+
+ @Test
+ public void testIsFalseTouch_SeekBar_FalseTap() {
+ when(mClassifierA.classifyGesture(anyInt(), anyDouble(), anyDouble()))
+ .thenReturn(mFalsedResult);
+ when(mSingleTapClassfier.isTap(any(List.class), anyDouble())).thenReturn(mFalsedResult);
+ assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).isTrue();
+ }
+
+ @Test
+ public void testIsFalseTouch_SeekBar_RealTap() {
+ when(mClassifierA.classifyGesture(anyInt(), anyDouble(), anyDouble()))
+ .thenReturn(mFalsedResult);
+ assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).isFalse();
+ }
+
+ @Test
public void testIsFalseTouch_ClassifierBRejects() {
when(mClassifierB.classifyGesture(anyInt(), anyDouble(), anyDouble()))
.thenReturn(mFalsedResult);
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/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
index 91214a8..e7e6918 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
@@ -38,6 +38,8 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.util.DeviceConfigProxyFake;
import org.junit.Before;
@@ -47,6 +49,9 @@
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import javax.inject.Provider;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -55,11 +60,15 @@
@Mock
private ClipboardManager mClipboardManager;
@Mock
- private ClipboardOverlayControllerFactory mClipboardOverlayControllerFactory;
+ private ClipboardOverlayControllerLegacyFactory mClipboardOverlayControllerLegacyFactory;
+ @Mock
+ private ClipboardOverlayControllerLegacy mOverlayControllerLegacy;
@Mock
private ClipboardOverlayController mOverlayController;
@Mock
private UiEventLogger mUiEventLogger;
+ @Mock
+ private FeatureFlags mFeatureFlags;
private DeviceConfigProxyFake mDeviceConfigProxy;
private ClipData mSampleClipData;
@@ -72,12 +81,17 @@
@Captor
private ArgumentCaptor<String> mStringCaptor;
+ @Spy
+ private Provider<ClipboardOverlayController> mOverlayControllerProvider;
+
@Before
public void setup() {
+ mOverlayControllerProvider = () -> mOverlayController;
+
MockitoAnnotations.initMocks(this);
- when(mClipboardOverlayControllerFactory.create(any())).thenReturn(
- mOverlayController);
+ when(mClipboardOverlayControllerLegacyFactory.create(any()))
+ .thenReturn(mOverlayControllerLegacy);
when(mClipboardManager.hasPrimaryClip()).thenReturn(true);
@@ -94,7 +108,8 @@
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"false", false);
ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger);
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardManager, mUiEventLogger, mFeatureFlags);
listener.start();
verifyZeroInteractions(mClipboardManager);
verifyZeroInteractions(mUiEventLogger);
@@ -105,7 +120,8 @@
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"true", false);
ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger);
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardManager, mUiEventLogger, mFeatureFlags);
listener.start();
verify(mClipboardManager).addPrimaryClipChangedListener(any());
verifyZeroInteractions(mUiEventLogger);
@@ -113,16 +129,58 @@
@Test
public void test_consecutiveCopies() {
+ when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false);
+
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"true", false);
ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger);
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardManager, mUiEventLogger, mFeatureFlags);
listener.start();
listener.onPrimaryClipChanged();
- verify(mClipboardOverlayControllerFactory).create(any());
+ verify(mClipboardOverlayControllerLegacyFactory).create(any());
- verify(mOverlayController).setClipData(mClipDataCaptor.capture(), mStringCaptor.capture());
+ verify(mOverlayControllerLegacy).setClipData(
+ mClipDataCaptor.capture(), mStringCaptor.capture());
+
+ assertEquals(mSampleClipData, mClipDataCaptor.getValue());
+ assertEquals(mSampleSource, mStringCaptor.getValue());
+
+ verify(mOverlayControllerLegacy).setOnSessionCompleteListener(mRunnableCaptor.capture());
+
+ // Should clear the overlay controller
+ mRunnableCaptor.getValue().run();
+
+ listener.onPrimaryClipChanged();
+
+ verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any());
+
+ // Not calling the runnable here, just change the clip again and verify that the overlay is
+ // NOT recreated.
+
+ listener.onPrimaryClipChanged();
+
+ verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any());
+ verifyZeroInteractions(mOverlayControllerProvider);
+ }
+
+ @Test
+ public void test_consecutiveCopies_new() {
+ when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true);
+
+ mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
+ "true", false);
+ ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardManager, mUiEventLogger, mFeatureFlags);
+ listener.start();
+ listener.onPrimaryClipChanged();
+
+ verify(mOverlayControllerProvider).get();
+
+ verify(mOverlayController).setClipData(
+ mClipDataCaptor.capture(), mStringCaptor.capture());
assertEquals(mSampleClipData, mClipDataCaptor.getValue());
assertEquals(mSampleSource, mStringCaptor.getValue());
@@ -134,14 +192,15 @@
listener.onPrimaryClipChanged();
- verify(mClipboardOverlayControllerFactory, times(2)).create(any());
+ verify(mOverlayControllerProvider, times(2)).get();
// Not calling the runnable here, just change the clip again and verify that the overlay is
// NOT recreated.
listener.onPrimaryClipChanged();
- verify(mClipboardOverlayControllerFactory, times(2)).create(any());
+ verify(mOverlayControllerProvider, times(2)).get();
+ verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory);
}
@Test
@@ -169,4 +228,40 @@
assertTrue(ClipboardListener.shouldSuppressOverlay(suppressableClipData,
ClipboardListener.SHELL_PACKAGE, false));
}
+
+ @Test
+ public void test_logging_enterAndReenter() {
+ when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false);
+
+ ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardManager, mUiEventLogger, mFeatureFlags);
+ listener.start();
+
+ listener.onPrimaryClipChanged();
+ listener.onPrimaryClipChanged();
+
+ verify(mUiEventLogger, times(1)).log(
+ ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource);
+ verify(mUiEventLogger, times(1)).log(
+ ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource);
+ }
+
+ @Test
+ public void test_logging_enterAndReenter_new() {
+ when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true);
+
+ ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardManager, mUiEventLogger, mFeatureFlags);
+ listener.start();
+
+ listener.onPrimaryClipChanged();
+ listener.onPrimaryClipChanged();
+
+ verify(mUiEventLogger, times(1)).log(
+ ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource);
+ verify(mUiEventLogger, times(1)).log(
+ ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
new file mode 100644
index 0000000..b7f1c1a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.clipboardoverlay;
+
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.Animator;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.net.Uri;
+import android.os.PersistableBundle;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.logging.UiEventLogger;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.screenshot.TimeoutHandler;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ClipboardOverlayControllerTest extends SysuiTestCase {
+
+ private ClipboardOverlayController mOverlayController;
+ @Mock
+ private ClipboardOverlayView mClipboardOverlayView;
+ @Mock
+ private ClipboardOverlayWindow mClipboardOverlayWindow;
+ @Mock
+ private BroadcastSender mBroadcastSender;
+ @Mock
+ private TimeoutHandler mTimeoutHandler;
+ @Mock
+ private UiEventLogger mUiEventLogger;
+
+ @Mock
+ private Animator mAnimator;
+
+ private ClipData mSampleClipData;
+
+ @Captor
+ private ArgumentCaptor<ClipboardOverlayView.ClipboardOverlayCallbacks> mOverlayCallbacksCaptor;
+ private ClipboardOverlayView.ClipboardOverlayCallbacks mCallbacks;
+
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator);
+ when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator);
+
+ mSampleClipData = new ClipData("Test", new String[]{"text/plain"},
+ new ClipData.Item("Test Item"));
+
+ mOverlayController = new ClipboardOverlayController(
+ mContext,
+ mClipboardOverlayView,
+ mClipboardOverlayWindow,
+ getFakeBroadcastDispatcher(),
+ mBroadcastSender,
+ mTimeoutHandler,
+ mUiEventLogger);
+ verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture());
+ mCallbacks = mOverlayCallbacksCaptor.getValue();
+ }
+
+ @After
+ public void tearDown() {
+ mOverlayController.hideImmediate();
+ }
+
+ @Test
+ public void test_setClipData_nullData() {
+ ClipData clipData = null;
+ mOverlayController.setClipData(clipData, "");
+
+ verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
+ verify(mClipboardOverlayView, times(0)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_invalidImageData() {
+ ClipData clipData = new ClipData("", new String[]{"image/png"},
+ new ClipData.Item(Uri.parse("")));
+
+ mOverlayController.setClipData(clipData, "");
+
+ verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
+ verify(mClipboardOverlayView, times(0)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_textData() {
+ mOverlayController.setClipData(mSampleClipData, "");
+
+ verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false);
+ verify(mClipboardOverlayView, times(1)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_sensitiveTextData() {
+ ClipDescription description = mSampleClipData.getDescription();
+ PersistableBundle b = new PersistableBundle();
+ b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
+ description.setExtras(b);
+ ClipData data = new ClipData(description, mSampleClipData.getItemAt(0));
+ mOverlayController.setClipData(data, "");
+
+ verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true);
+ verify(mClipboardOverlayView, times(1)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_repeatedCalls() {
+ when(mAnimator.isRunning()).thenReturn(true);
+
+ mOverlayController.setClipData(mSampleClipData, "");
+ mOverlayController.setClipData(mSampleClipData, "");
+
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_viewCallbacks_onShareTapped() {
+ mOverlayController.setClipData(mSampleClipData, "");
+
+ mCallbacks.onShareButtonTapped();
+
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED);
+ verify(mClipboardOverlayView, times(1)).getExitAnimation();
+ }
+
+ @Test
+ public void test_viewCallbacks_onDismissTapped() {
+ mOverlayController.setClipData(mSampleClipData, "");
+
+ mCallbacks.onDismissButtonTapped();
+
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
+ verify(mClipboardOverlayView, times(1)).getExitAnimation();
+ }
+
+ @Test
+ public void test_multipleDismissals_dismissesOnce() {
+ mCallbacks.onSwipeDismissInitiated(mAnimator);
+ mCallbacks.onDismissButtonTapped();
+ mCallbacks.onSwipeDismissInitiated(mAnimator);
+ mCallbacks.onDismissButtonTapped();
+
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
+ verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java
deleted file mode 100644
index c7c2cd8d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.clipboardoverlay;
-
-import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.provider.DeviceConfig;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.util.DeviceConfigProxyFake;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class ClipboardOverlayEventTest extends SysuiTestCase {
-
- @Mock
- private ClipboardManager mClipboardManager;
- @Mock
- private ClipboardOverlayControllerFactory mClipboardOverlayControllerFactory;
- @Mock
- private ClipboardOverlayController mOverlayController;
- @Mock
- private UiEventLogger mUiEventLogger;
-
- private final String mSampleSource = "Example source";
-
- private ClipboardListener mClipboardListener;
-
-
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- when(mClipboardOverlayControllerFactory.create(any())).thenReturn(
- mOverlayController);
- when(mClipboardManager.hasPrimaryClip()).thenReturn(true);
-
- ClipData sampleClipData = new ClipData("Test", new String[]{"text/plain"},
- new ClipData.Item("Test Item"));
- when(mClipboardManager.getPrimaryClip()).thenReturn(sampleClipData);
- when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource);
-
- DeviceConfigProxyFake deviceConfigProxy = new DeviceConfigProxyFake();
- deviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
- "true", false);
-
- mClipboardListener = new ClipboardListener(getContext(), deviceConfigProxy,
- mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger);
- }
-
- @Test
- public void test_enterAndReenter() {
- mClipboardListener.start();
-
- mClipboardListener.onPrimaryClipChanged();
- mClipboardListener.onPrimaryClipChanged();
-
- verify(mUiEventLogger, times(1)).log(
- ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource);
- verify(mUiEventLogger, times(1)).log(
- ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource);
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt
index f933361..93a1868 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt
@@ -24,12 +24,11 @@
import androidx.test.filters.SmallTest
import com.android.internal.R as InternalR
import com.android.systemui.R as SystemUIR
-import com.android.systemui.tests.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.tests.R
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
-
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@@ -102,14 +101,11 @@
assertEquals(Size(3, 3), roundedCornerResDelegate.topRoundedSize)
assertEquals(Size(4, 4), roundedCornerResDelegate.bottomRoundedSize)
- setupResources(radius = 100,
- roundedTopDrawable = getTestsDrawable(R.drawable.rounded4px),
- roundedBottomDrawable = getTestsDrawable(R.drawable.rounded5px))
-
+ roundedCornerResDelegate.physicalPixelDisplaySizeRatio = 2f
roundedCornerResDelegate.updateDisplayUniqueId(null, 1)
- assertEquals(Size(4, 4), roundedCornerResDelegate.topRoundedSize)
- assertEquals(Size(5, 5), roundedCornerResDelegate.bottomRoundedSize)
+ assertEquals(Size(6, 6), roundedCornerResDelegate.topRoundedSize)
+ assertEquals(Size(8, 8), roundedCornerResDelegate.bottomRoundedSize)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
index 6a55a60..5bbd810 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
@@ -16,6 +16,9 @@
package com.android.systemui.doze;
+import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
+
import static com.android.systemui.doze.DozeMachine.State.DOZE;
import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD;
import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_DOCKED;
@@ -38,16 +41,17 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.app.UiModeManager;
import android.content.res.Configuration;
import android.hardware.display.AmbientDisplayConfiguration;
import android.testing.AndroidTestingRunner;
import android.testing.UiThreadTest;
import android.view.Display;
+import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
@@ -78,25 +82,30 @@
@Mock
private DozeHost mHost;
@Mock
- private UiModeManager mUiModeManager;
+ private DozeMachine.Part mPartMock;
+ @Mock
+ private DozeMachine.Part mAnotherPartMock;
private DozeServiceFake mServiceFake;
private WakeLockFake mWakeLockFake;
- private AmbientDisplayConfiguration mConfigMock;
- private DozeMachine.Part mPartMock;
+ private AmbientDisplayConfiguration mAmbientDisplayConfigMock;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mServiceFake = new DozeServiceFake();
mWakeLockFake = new WakeLockFake();
- mConfigMock = mock(AmbientDisplayConfiguration.class);
- mPartMock = mock(DozeMachine.Part.class);
+ mAmbientDisplayConfigMock = mock(AmbientDisplayConfiguration.class);
when(mDockManager.isDocked()).thenReturn(false);
when(mDockManager.isHidden()).thenReturn(false);
- mMachine = new DozeMachine(mServiceFake, mConfigMock, mWakeLockFake,
- mWakefulnessLifecycle, mUiModeManager, mDozeLog, mDockManager,
- mHost, new DozeMachine.Part[]{mPartMock});
+ mMachine = new DozeMachine(mServiceFake,
+ mAmbientDisplayConfigMock,
+ mWakeLockFake,
+ mWakefulnessLifecycle,
+ mDozeLog,
+ mDockManager,
+ mHost,
+ new DozeMachine.Part[]{mPartMock, mAnotherPartMock});
}
@Test
@@ -108,7 +117,7 @@
@Test
public void testInitialize_goesToDoze() {
- when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+ when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
mMachine.requestState(INITIALIZED);
@@ -118,7 +127,7 @@
@Test
public void testInitialize_goesToAod() {
- when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+ when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
mMachine.requestState(INITIALIZED);
@@ -138,7 +147,7 @@
@Test
public void testInitialize_afterDockPaused_goesToDoze() {
- when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+ when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
when(mDockManager.isDocked()).thenReturn(true);
when(mDockManager.isHidden()).thenReturn(true);
@@ -151,7 +160,7 @@
@Test
public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_goesToDoze() {
when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
- when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+ when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
mMachine.requestState(INITIALIZED);
@@ -162,7 +171,7 @@
@Test
public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_goesToDoze() {
when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
- when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+ when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
mMachine.requestState(INITIALIZED);
@@ -184,7 +193,7 @@
@Test
public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_afterDockPaused_goesToDoze() {
when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
- when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+ when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
when(mDockManager.isDocked()).thenReturn(true);
when(mDockManager.isHidden()).thenReturn(true);
@@ -197,7 +206,7 @@
@Test
public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_afterDockPaused_goesToDoze() {
when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
- when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+ when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
when(mDockManager.isDocked()).thenReturn(true);
when(mDockManager.isHidden()).thenReturn(true);
@@ -209,7 +218,7 @@
@Test
public void testPulseDone_goesToDoze() {
- when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+ when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
mMachine.requestState(INITIALIZED);
mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
mMachine.requestState(DOZE_PULSING);
@@ -222,7 +231,7 @@
@Test
public void testPulseDone_goesToAoD() {
- when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+ when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
mMachine.requestState(INITIALIZED);
mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
mMachine.requestState(DOZE_PULSING);
@@ -236,7 +245,7 @@
@Test
public void testPulseDone_alwaysOnSuppressed_goesToSuppressed() {
when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
- when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+ when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
mMachine.requestState(INITIALIZED);
mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
mMachine.requestState(DOZE_PULSING);
@@ -287,7 +296,7 @@
@Test
public void testPulseDone_afterDockPaused_goesToDoze() {
- when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+ when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
when(mDockManager.isDocked()).thenReturn(true);
when(mDockManager.isHidden()).thenReturn(true);
mMachine.requestState(INITIALIZED);
@@ -303,7 +312,7 @@
@Test
public void testPulseDone_alwaysOnSuppressed_afterDockPaused_goesToDoze() {
when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
- when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+ when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
when(mDockManager.isDocked()).thenReturn(true);
when(mDockManager.isHidden()).thenReturn(true);
mMachine.requestState(INITIALIZED);
@@ -471,7 +480,9 @@
@Test
public void testTransitionToInitialized_carModeIsEnabled() {
- when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+ Configuration configuration = configWithCarNightUiMode();
+
+ mMachine.onConfigurationChanged(configuration);
mMachine.requestState(INITIALIZED);
verify(mPartMock).transitionTo(UNINITIALIZED, INITIALIZED);
@@ -481,7 +492,9 @@
@Test
public void testTransitionToFinish_carModeIsEnabled() {
- when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+ Configuration configuration = configWithCarNightUiMode();
+
+ mMachine.onConfigurationChanged(configuration);
mMachine.requestState(INITIALIZED);
mMachine.requestState(FINISH);
@@ -490,7 +503,9 @@
@Test
public void testDozeToDozeSuspendTriggers_carModeIsEnabled() {
- when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+ Configuration configuration = configWithCarNightUiMode();
+
+ mMachine.onConfigurationChanged(configuration);
mMachine.requestState(INITIALIZED);
mMachine.requestState(DOZE);
@@ -499,7 +514,9 @@
@Test
public void testDozeAoDToDozeSuspendTriggers_carModeIsEnabled() {
- when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+ Configuration configuration = configWithCarNightUiMode();
+
+ mMachine.onConfigurationChanged(configuration);
mMachine.requestState(INITIALIZED);
mMachine.requestState(DOZE_AOD);
@@ -508,7 +525,9 @@
@Test
public void testDozePulsingBrightDozeSuspendTriggers_carModeIsEnabled() {
- when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+ Configuration configuration = configWithCarNightUiMode();
+
+ mMachine.onConfigurationChanged(configuration);
mMachine.requestState(INITIALIZED);
mMachine.requestState(DOZE_PULSING_BRIGHT);
@@ -517,7 +536,9 @@
@Test
public void testDozeAodDockedDozeSuspendTriggers_carModeIsEnabled() {
- when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+ Configuration configuration = configWithCarNightUiMode();
+
+ mMachine.onConfigurationChanged(configuration);
mMachine.requestState(INITIALIZED);
mMachine.requestState(DOZE_AOD_DOCKED);
@@ -525,7 +546,35 @@
}
@Test
+ public void testOnConfigurationChanged_propagatesUiModeTypeToParts() {
+ Configuration newConfig = configWithCarNightUiMode();
+
+ mMachine.onConfigurationChanged(newConfig);
+
+ verify(mPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+ verify(mAnotherPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+ }
+
+ @Test
+ public void testOnConfigurationChanged_propagatesOnlyUiModeChangesToParts() {
+ Configuration newConfig = configWithCarNightUiMode();
+
+ mMachine.onConfigurationChanged(newConfig);
+ mMachine.onConfigurationChanged(newConfig);
+
+ verify(mPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+ verify(mAnotherPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+ }
+
+ @Test
public void testDozeSuppressTriggers_screenState() {
assertEquals(Display.STATE_OFF, DOZE_SUSPEND_TRIGGERS.screenState(null));
}
+
+ @NonNull
+ private Configuration configWithCarNightUiMode() {
+ Configuration configuration = Configuration.EMPTY;
+ configuration.uiMode = UI_MODE_TYPE_CAR | UI_MODE_NIGHT_YES;
+ return configuration;
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
index 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/DozeSuppressorTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
index 0f29dcd..32b9945 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
@@ -10,14 +10,14 @@
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions andatest
+ * See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.doze;
-import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE;
-import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
+import static android.content.res.Configuration.UI_MODE_TYPE_NORMAL;
import static com.android.systemui.doze.DozeMachine.State.DOZE;
import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD;
@@ -26,17 +26,16 @@
import static com.android.systemui.doze.DozeMachine.State.INITIALIZED;
import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED;
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.app.UiModeManager;
-import android.content.BroadcastReceiver;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
import android.hardware.display.AmbientDisplayConfiguration;
import android.testing.AndroidTestingRunner;
import android.testing.UiThreadTest;
@@ -44,13 +43,13 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.statusbar.phone.BiometricUnlockController;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.AdditionalMatchers;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
@@ -71,10 +70,6 @@
@Mock
private AmbientDisplayConfiguration mConfig;
@Mock
- private BroadcastDispatcher mBroadcastDispatcher;
- @Mock
- private UiModeManager mUiModeManager;
- @Mock
private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
@Mock
private BiometricUnlockController mBiometricUnlockController;
@@ -83,13 +78,6 @@
private DozeMachine mDozeMachine;
@Captor
- private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor;
- @Captor
- private ArgumentCaptor<IntentFilter> mIntentFilterCaptor;
- private BroadcastReceiver mBroadcastReceiver;
- private IntentFilter mIntentFilter;
-
- @Captor
private ArgumentCaptor<DozeHost.Callback> mDozeHostCaptor;
private DozeHost.Callback mDozeHostCallback;
@@ -106,8 +94,6 @@
mDozeHost,
mConfig,
mDozeLog,
- mBroadcastDispatcher,
- mUiModeManager,
mBiometricUnlockControllerLazy);
mDozeSuppressor.setDozeMachine(mDozeMachine);
@@ -122,36 +108,35 @@
public void testRegistersListenersOnInitialized_unregisteredOnFinish() {
// check that receivers and callbacks registered
mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
- captureBroadcastReceiver();
captureDozeHostCallback();
// check that receivers and callbacks are unregistered
mDozeSuppressor.transitionTo(INITIALIZED, FINISH);
- verify(mBroadcastDispatcher).unregisterReceiver(mBroadcastReceiver);
verify(mDozeHost).removeCallback(mDozeHostCallback);
}
@Test
public void testSuspendTriggersDoze_carMode() {
// GIVEN car mode
- when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+ mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
// WHEN dozing begins
mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
// THEN doze continues with all doze triggers disabled.
- verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS);
+ verify(mDozeMachine, atLeastOnce()).requestState(DOZE_SUSPEND_TRIGGERS);
+ verify(mDozeMachine, never())
+ .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS)));
}
@Test
public void testSuspendTriggersDoze_enterCarMode() {
// GIVEN currently dozing
mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
- captureBroadcastReceiver();
mDozeSuppressor.transitionTo(INITIALIZED, DOZE);
// WHEN car mode entered
- mBroadcastReceiver.onReceive(null, new Intent(ACTION_ENTER_CAR_MODE));
+ mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
// THEN doze continues with all doze triggers disabled.
verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS);
@@ -160,13 +145,13 @@
@Test
public void testDozeResume_exitCarMode() {
// GIVEN currently suspended, with AOD not enabled
+ mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(false);
mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
- captureBroadcastReceiver();
mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
// WHEN exiting car mode
- mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE));
+ mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL);
// THEN doze is resumed
verify(mDozeMachine).requestState(DOZE);
@@ -175,19 +160,53 @@
@Test
public void testDozeAoDResume_exitCarMode() {
// GIVEN currently suspended, with AOD not enabled
+ mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(true);
mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
- captureBroadcastReceiver();
mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
// WHEN exiting car mode
- mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE));
+ mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL);
// THEN doze AOD is resumed
verify(mDozeMachine).requestState(DOZE_AOD);
}
@Test
+ public void testUiModeDoesNotChange_noStateTransition() {
+ mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
+ clearInvocations(mDozeMachine);
+
+ mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+ mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+ mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+
+ verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS);
+ verify(mDozeMachine, never())
+ .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS)));
+ }
+
+ @Test
+ public void testUiModeTypeChange_whenDozeMachineIsNotReady_doesNotDoAnything() {
+ when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true);
+
+ mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+
+ verify(mDozeMachine, never()).requestState(any());
+ }
+
+ @Test
+ public void testUiModeTypeChange_CarModeEnabledAndDozeMachineNotReady_suspendsTriggersAfter() {
+ when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true);
+ mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+ verify(mDozeMachine, never()).requestState(any());
+
+ mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
+
+ verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS);
+ }
+
+ @Test
public void testEndDoze_unprovisioned() {
// GIVEN device unprovisioned
when(mDozeHost.isProvisioned()).thenReturn(false);
@@ -276,14 +295,4 @@
verify(mDozeHost).addCallback(mDozeHostCaptor.capture());
mDozeHostCallback = mDozeHostCaptor.getValue();
}
-
- private void captureBroadcastReceiver() {
- verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiverCaptor.capture(),
- mIntentFilterCaptor.capture());
- mBroadcastReceiver = mBroadcastReceiverCaptor.getValue();
- mIntentFilter = mIntentFilterCaptor.getValue();
- assertEquals(2, mIntentFilter.countActions());
- org.hamcrest.MatcherAssert.assertThat(() -> mIntentFilter.actionsIterator(),
- containsInAnyOrder(ACTION_ENTER_CAR_MODE, ACTION_EXIT_CAR_MODE));
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 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/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index eec33ca..f370be1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -27,10 +28,10 @@
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
-import android.service.dreams.DreamService;
import android.service.dreams.IDreamOverlay;
import android.service.dreams.IDreamOverlayCallback;
import android.testing.AndroidTestingRunner;
+import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowManagerImpl;
@@ -53,6 +54,8 @@
import org.junit.Rule;
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;
@@ -61,6 +64,7 @@
public class DreamOverlayServiceTest extends SysuiTestCase {
private static final ComponentName LOW_LIGHT_COMPONENT = new ComponentName("package",
"lowlight");
+ private static final String DREAM_COMPONENT = "package/dream";
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
@@ -108,12 +112,14 @@
@Mock
UiEventLogger mUiEventLogger;
+ @Captor
+ ArgumentCaptor<View> mViewCaptor;
+
DreamOverlayService mService;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
- mContext.addMockSystemService(WindowManager.class, mWindowManager);
when(mDreamOverlayComponent.getDreamOverlayContainerViewController())
.thenReturn(mDreamOverlayContainerViewController);
@@ -129,7 +135,7 @@
when(mDreamOverlayContainerViewController.getContainerView())
.thenReturn(mDreamOverlayContainerView);
- mService = new DreamOverlayService(mContext, mMainExecutor,
+ mService = new DreamOverlayService(mContext, mMainExecutor, mWindowManager,
mDreamOverlayComponentFactory,
mStateController,
mKeyguardUpdateMonitor,
@@ -143,7 +149,8 @@
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback);
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
@@ -157,7 +164,8 @@
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback);
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
verify(mWindowManager).addView(any(), any());
@@ -169,7 +177,8 @@
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback);
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
verify(mDreamOverlayContainerViewController).init();
@@ -186,49 +195,76 @@
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback);
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView);
}
@Test
- public void testShouldShowComplicationsFalseByDefault() {
- mService.onBind(new Intent());
+ public void testShouldShowComplicationsSetByStartDream() throws RemoteException {
+ final IBinder proxy = mService.onBind(new Intent());
+ final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
- assertThat(mService.shouldShowComplications()).isFalse();
- }
-
- @Test
- public void testShouldShowComplicationsSetByIntentExtra() {
- final Intent intent = new Intent();
- intent.putExtra(DreamService.EXTRA_SHOW_COMPLICATIONS, true);
- mService.onBind(intent);
+ // Inform the overlay service of dream starting.
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ true /*shouldShowComplication*/);
assertThat(mService.shouldShowComplications()).isTrue();
}
@Test
- public void testLowLightSetByIntentExtra() throws RemoteException {
- final Intent intent = new Intent();
- intent.putExtra(DreamService.EXTRA_DREAM_COMPONENT, LOW_LIGHT_COMPONENT);
-
- final IBinder proxy = mService.onBind(intent);
+ public void testLowLightSetByStartDream() throws RemoteException {
+ final IBinder proxy = mService.onBind(new Intent());
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
- assertThat(mService.getDreamComponent()).isEqualTo(LOW_LIGHT_COMPONENT);
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback);
+ overlay.startDream(mWindowParams, mDreamOverlayCallback,
+ LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
+ assertThat(mService.getDreamComponent()).isEqualTo(LOW_LIGHT_COMPONENT);
verify(mStateController).setLowLightActive(true);
}
@Test
- public void testDestroy() {
+ public void testDestroy() throws RemoteException {
+ final IBinder proxy = mService.onBind(new Intent());
+ final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+
+ // Inform the overlay service of dream starting.
+ overlay.startDream(mWindowParams, mDreamOverlayCallback,
+ LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/);
+ mMainExecutor.runAllReady();
+
+ // Verify view added.
+ verify(mWindowManager).addView(mViewCaptor.capture(), any());
+
+ // Service destroyed.
mService.onDestroy();
mMainExecutor.runAllReady();
+ // Verify view removed.
+ verify(mWindowManager).removeView(mViewCaptor.getValue());
+
+ // Verify state correctly set.
+ verify(mKeyguardUpdateMonitor).removeCallback(any());
+ verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED);
+ verify(mStateController).setOverlayActive(false);
+ verify(mStateController).setLowLightActive(false);
+ }
+
+ @Test
+ public void testDoNotRemoveViewOnDestroyIfOverlayNotStarted() {
+ // Service destroyed without ever starting dream.
+ mService.onDestroy();
+ mMainExecutor.runAllReady();
+
+ // Verify no view is removed.
+ verify(mWindowManager, never()).removeView(any());
+
+ // Verify state still correctly set.
verify(mKeyguardUpdateMonitor).removeCallback(any());
verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED);
verify(mStateController).setOverlayActive(false);
@@ -245,7 +281,8 @@
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback);
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
// Destroy the service.
mService.onDestroy();
@@ -255,4 +292,44 @@
verify(mWindowManager, never()).addView(any(), any());
}
+
+ @Test
+ public void testResetCurrentOverlayWhenConnectedToNewDream() throws RemoteException {
+ final IBinder proxy = mService.onBind(new Intent());
+ final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+
+ // Inform the overlay service of dream starting. Do not show dream complications.
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
+ mMainExecutor.runAllReady();
+
+ // Verify that a new window is added.
+ verify(mWindowManager).addView(mViewCaptor.capture(), any());
+ final View windowDecorView = mViewCaptor.getValue();
+
+ // Assert that the overlay is not showing complications.
+ assertThat(mService.shouldShowComplications()).isFalse();
+
+ clearInvocations(mDreamOverlayComponent);
+ clearInvocations(mWindowManager);
+
+ // New dream starting with dream complications showing. Note that when a new dream is
+ // binding to the dream overlay service, it receives the same instance of IBinder as the
+ // first one.
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ true /*shouldShowComplication*/);
+ mMainExecutor.runAllReady();
+
+ // Assert that the overlay is showing complications.
+ assertThat(mService.shouldShowComplications()).isTrue();
+
+ // Verify that the old overlay window has been removed, and a new one created.
+ verify(mWindowManager).removeView(windowDecorView);
+ verify(mWindowManager).addView(any(), any());
+
+ // Verify that new instances of overlay container view controller and overlay touch monitor
+ // are created.
+ verify(mDreamOverlayComponent).getDreamOverlayContainerViewController();
+ verify(mDreamOverlayComponent).getDreamOverlayTouchMonitor();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java
index 571dd3d..9f4a7c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java
@@ -71,7 +71,7 @@
MockitoAnnotations.initMocks(this);
when(mDreamBackend.getEnabledComplications()).thenReturn(new HashSet<>());
- mController = new ComplicationTypesUpdater(mContext, mDreamBackend, mExecutor,
+ mController = new ComplicationTypesUpdater(mDreamBackend, mExecutor,
mSecureSettings, mDreamOverlayStateController);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java
index 314a30b..ec448f9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java
@@ -82,7 +82,6 @@
public void testComplicationAdded() {
final DreamClockTimeComplication.Registrant registrant =
new DreamClockTimeComplication.Registrant(
- mContext,
mDreamOverlayStateController,
mComplication);
registrant.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
index db6082d..aa8c93e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
@@ -115,7 +115,7 @@
@Test
public void complicationAvailability_serviceNotAvailable_noFavorites_doNotAddComplication() {
final DreamHomeControlsComplication.Registrant registrant =
- new DreamHomeControlsComplication.Registrant(mContext, mComplication,
+ new DreamHomeControlsComplication.Registrant(mComplication,
mDreamOverlayStateController, mControlsComponent);
registrant.start();
@@ -128,7 +128,7 @@
@Test
public void complicationAvailability_serviceAvailable_noFavorites_doNotAddComplication() {
final DreamHomeControlsComplication.Registrant registrant =
- new DreamHomeControlsComplication.Registrant(mContext, mComplication,
+ new DreamHomeControlsComplication.Registrant(mComplication,
mDreamOverlayStateController, mControlsComponent);
registrant.start();
@@ -141,7 +141,7 @@
@Test
public void complicationAvailability_serviceNotAvailable_haveFavorites_doNotAddComplication() {
final DreamHomeControlsComplication.Registrant registrant =
- new DreamHomeControlsComplication.Registrant(mContext, mComplication,
+ new DreamHomeControlsComplication.Registrant(mComplication,
mDreamOverlayStateController, mControlsComponent);
registrant.start();
@@ -154,7 +154,7 @@
@Test
public void complicationAvailability_serviceAvailable_haveFavorites_addComplication() {
final DreamHomeControlsComplication.Registrant registrant =
- new DreamHomeControlsComplication.Registrant(mContext, mComplication,
+ new DreamHomeControlsComplication.Registrant(mComplication,
mDreamOverlayStateController, mControlsComponent);
registrant.start();
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/dreams/complication/SmartSpaceComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java
index fa8f88a..c8b2b25 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java
@@ -24,7 +24,6 @@
import static org.mockito.Mockito.when;
import android.app.smartspace.SmartspaceTarget;
-import android.content.Context;
import android.testing.AndroidTestingRunner;
import android.view.View;
@@ -48,8 +47,6 @@
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class SmartSpaceComplicationTest extends SysuiTestCase {
- @Mock
- private Context mContext;
@Mock
private DreamSmartspaceController mSmartspaceController;
@@ -80,7 +77,6 @@
private SmartSpaceComplication.Registrant getRegistrant() {
return new SmartSpaceComplication.Registrant(
- mContext,
mDreamOverlayStateController,
mComplication,
mSmartspaceController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
index c3fca29..4bd53c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
@@ -41,12 +41,12 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.KeyguardBouncer;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
import com.android.wm.shell.animation.FlingAnimationUtils;
import org.junit.Before;
@@ -285,8 +285,8 @@
final float dragDownAmount = event2.getY() - event1.getY();
// Ensure correct expansion passed in.
- PanelExpansionChangeEvent event =
- new PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent event =
+ new ShadeExpansionChangeEvent(
expansion, /* expanded= */ false, /* tracking= */ true, dragDownAmount);
verify(mStatusBarKeyguardViewManager).onPanelExpansionChanged(event);
}
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 fc67201..65ae90b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
@@ -17,11 +17,19 @@
package com.android.systemui.dump
import androidx.test.filters.SmallTest
+import com.android.systemui.CoreStartable
import com.android.systemui.Dumpable
+import com.android.systemui.ProtoDumpable
import com.android.systemui.SysuiTestCase
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import java.io.StringWriter
+import javax.inject.Provider
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
@@ -29,7 +37,6 @@
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-import java.io.PrintWriter
@SmallTest
class DumpHandlerTest : SysuiTestCase() {
@@ -43,6 +50,8 @@
@Mock
private lateinit var pw: PrintWriter
+ @Mock
+ private lateinit var fd: FileDescriptor
@Mock
private lateinit var dumpable1: Dumpable
@@ -52,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
@@ -66,7 +80,9 @@
mContext,
dumpManager,
logBufferEulogizer,
- mutableMapOf(),
+ mutableMapOf(
+ EmptyCoreStartable::class.java to Provider { EmptyCoreStartable() }
+ ),
exceptionHandlerManager
)
}
@@ -82,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)
@@ -101,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)
@@ -118,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)
@@ -139,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(
@@ -154,4 +170,44 @@
verify(buffer1).dump(pw, 0)
verify(buffer2).dump(pw, 0)
}
-}
\ No newline at end of file
+
+ @Test
+ fun testConfigDump() {
+ // GIVEN a StringPrintWriter
+ val stringWriter = StringWriter()
+ val spw = PrintWriter(stringWriter)
+
+ // When a config dump is requested
+ dumpHandler.dump(fd, spw, arrayOf("config"))
+
+ assertThat(stringWriter.toString()).contains(EmptyCoreStartable::class.java.simpleName)
+ }
+
+ @Test
+ fun testDumpAllProtoDumpables() {
+ dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
+ dumpManager.registerDumpable("protoDumpable2", protoDumpable2)
+
+ val args = arrayOf(DumpHandler.PROTO)
+ dumpHandler.dump(fd, pw, args)
+
+ verify(protoDumpable1).dumpProto(any(), eq(args))
+ verify(protoDumpable2).dumpProto(any(), eq(args))
+ }
+
+ @Test
+ fun testDumpSingleProtoDumpable() {
+ dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
+ dumpManager.registerDumpable("protoDumpable2", protoDumpable2)
+
+ val args = arrayOf(DumpHandler.PROTO, "protoDumpable1")
+ dumpHandler.dump(fd, pw, args)
+
+ verify(protoDumpable1).dumpProto(any(), eq(args))
+ verify(protoDumpable2, never()).dumpProto(any(), any())
+ }
+
+ private class EmptyCoreStartable : CoreStartable {
+ override fun start() {}
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
index bd029a7..64547f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
@@ -16,9 +16,9 @@
package com.android.systemui.dump
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogcatEchoTracker
/**
* Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
index 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/AnimatableClockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java
deleted file mode 100644
index b5e9e8d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyObject;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.res.Resources;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.View;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.AnimatableClockController;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.settingslib.Utils;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.shared.clocks.AnimatableClockView;
-import com.android.systemui.statusbar.policy.BatteryController;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.util.concurrent.Executor;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class AnimatableClockControllerTest extends SysuiTestCase {
- @Mock
- private AnimatableClockView mClockView;
- @Mock
- private StatusBarStateController mStatusBarStateController;
- @Mock
- private BroadcastDispatcher mBroadcastDispatcher;
- @Mock
- private BatteryController mBatteryController;
- @Mock
- private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
- @Mock
- private Resources mResources;
- @Mock
- private Executor mMainExecutor;
- @Mock
- private Executor mBgExecutor;
- @Mock
- private FeatureFlags mFeatureFlags;
-
- private MockitoSession mStaticMockSession;
- private AnimatableClockController mAnimatableClockController;
-
- // Capture listeners so that they can be used to send events
- @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor =
- ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
- private View.OnAttachStateChangeListener mAttachListener;
-
- @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor;
- private StatusBarStateController.StateListener mStatusBarStateCallback;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
- mStaticMockSession = mockitoSession()
- .mockStatic(Utils.class)
- .strictness(Strictness.LENIENT) // it's ok if mocked classes aren't used
- .startMocking();
- when(Utils.getColorAttrDefaultColor(anyObject(), anyInt())).thenReturn(0);
-
- mAnimatableClockController = new AnimatableClockController(
- mClockView,
- mStatusBarStateController,
- mBroadcastDispatcher,
- mBatteryController,
- mKeyguardUpdateMonitor,
- mResources,
- mMainExecutor,
- mBgExecutor,
- mFeatureFlags
- );
- mAnimatableClockController.init();
- captureAttachListener();
- }
-
- @After
- public void tearDown() {
- mStaticMockSession.finishMocking();
- }
-
- @Test
- public void testOnAttachedUpdatesDozeStateToTrue() {
- // GIVEN dozing
- when(mStatusBarStateController.isDozing()).thenReturn(true);
- when(mStatusBarStateController.getDozeAmount()).thenReturn(1f);
-
- // WHEN the clock view gets attached
- mAttachListener.onViewAttachedToWindow(mClockView);
-
- // THEN the clock controller updated its dozing state to true
- assertTrue(mAnimatableClockController.isDozing());
- }
-
- @Test
- public void testOnAttachedUpdatesDozeStateToFalse() {
- // GIVEN not dozing
- when(mStatusBarStateController.isDozing()).thenReturn(false);
- when(mStatusBarStateController.getDozeAmount()).thenReturn(0f);
-
- // WHEN the clock view gets attached
- mAttachListener.onViewAttachedToWindow(mClockView);
-
- // THEN the clock controller updated its dozing state to false
- assertFalse(mAnimatableClockController.isDozing());
- }
-
- private void captureAttachListener() {
- verify(mClockView).addOnAttachStateChangeListener(mAttachCaptor.capture());
- mAttachListener = mAttachCaptor.getValue();
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 21c018a..4c986bf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -19,6 +19,7 @@
import static android.view.WindowManagerPolicyConstants.OFF_BECAUSE_OF_USER;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -176,7 +177,7 @@
// and the keyguard goes away
mViewMediator.setShowingLocked(false);
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false);
TestableLooper.get(this).processAllMessages();
@@ -201,7 +202,7 @@
// and the keyguard goes away
mViewMediator.setShowingLocked(false);
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false);
TestableLooper.get(this).processAllMessages();
@@ -229,6 +230,28 @@
}
@Test
+ public void testBouncerPrompt_nonStrongIdleTimeout() {
+ // GIVEN trust agents enabled and biometrics are enrolled
+ when(mUpdateMonitor.isTrustUsuallyManaged(anyInt())).thenReturn(true);
+ when(mUpdateMonitor.isUnlockingWithBiometricsPossible(anyInt())).thenReturn(true);
+
+ // WHEN the strong auth reason is STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT
+ KeyguardUpdateMonitor.StrongAuthTracker strongAuthTracker =
+ mock(KeyguardUpdateMonitor.StrongAuthTracker.class);
+ when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(strongAuthTracker);
+ when(strongAuthTracker.hasUserAuthenticatedSinceBoot()).thenReturn(true);
+ when(strongAuthTracker.isNonStrongBiometricAllowedAfterIdleTimeout(
+ anyInt())).thenReturn(false);
+ when(strongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(
+ STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT);
+
+ // THEN the bouncer prompt reason should return
+ // STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT
+ assertEquals(KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT,
+ mViewMediator.mViewMediatorCallback.getBouncerPromptReason());
+ }
+
+ @Test
public void testHideSurfaceBehindKeyguardMarksKeyguardNotGoingAway() {
mViewMediator.hideSurfaceBehindKeyguard();
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/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
new file mode 100644
index 0000000..1b34100
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import android.animation.AnimationHandler.AnimationFrameCallbackProvider
+import android.animation.ValueAnimator
+import android.util.Log
+import android.util.Log.TerribleFailure
+import android.util.Log.TerribleFailureHandler
+import android.view.Choreographer.FrameCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.google.common.truth.Truth.assertThat
+import java.math.BigDecimal
+import java.math.RoundingMode
+import java.util.UUID
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.After
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardTransitionRepositoryTest : SysuiTestCase() {
+
+ private lateinit var underTest: KeyguardTransitionRepository
+ private lateinit var oldWtfHandler: TerribleFailureHandler
+ private lateinit var wtfHandler: WtfHandler
+
+ @Before
+ fun setUp() {
+ underTest = KeyguardTransitionRepository()
+ wtfHandler = WtfHandler()
+ oldWtfHandler = Log.setWtfHandler(wtfHandler)
+ }
+
+ @After
+ fun tearDown() {
+ oldWtfHandler?.let { Log.setWtfHandler(it) }
+ }
+
+ @Test
+ fun `startTransition runs animator to completion`() =
+ runBlocking(IMMEDIATE) {
+ val (animator, provider) = setupAnimator(this)
+
+ val steps = mutableListOf<TransitionStep>()
+ val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)
+
+ underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator))
+
+ val startTime = System.currentTimeMillis()
+ while (animator.isRunning()) {
+ yield()
+ if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) {
+ fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION")
+ }
+ }
+
+ assertSteps(steps, listWithStep(BigDecimal(.1)))
+
+ job.cancel()
+ provider.stop()
+ }
+
+ @Test
+ fun `startTransition called during another transition fails`() {
+ underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, null))
+ underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, BOUNCER, null))
+
+ assertThat(wtfHandler.failed).isTrue()
+ }
+
+ @Test
+ fun `Null animator enables manual control with updateTransition`() =
+ runBlocking(IMMEDIATE) {
+ val steps = mutableListOf<TransitionStep>()
+ val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)
+
+ val uuid =
+ underTest.startTransition(
+ TransitionInfo(
+ ownerName = OWNER_NAME,
+ from = AOD,
+ to = LOCKSCREEN,
+ animator = null,
+ )
+ )
+
+ checkNotNull(uuid).let {
+ underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
+ underTest.updateTransition(it, 1f, TransitionState.FINISHED)
+ }
+
+ assertThat(steps.size).isEqualTo(3)
+ assertThat(steps[0])
+ .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED))
+ assertThat(steps[1])
+ .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING))
+ assertThat(steps[2])
+ .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED))
+ job.cancel()
+ }
+
+ @Test
+ fun `Attempt to manually update transition with invalid UUID throws exception`() {
+ underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING)
+ assertThat(wtfHandler.failed).isTrue()
+ }
+
+ @Test
+ fun `Attempt to manually update transition after FINISHED state throws exception`() {
+ val uuid =
+ underTest.startTransition(
+ TransitionInfo(
+ ownerName = OWNER_NAME,
+ from = AOD,
+ to = LOCKSCREEN,
+ animator = null,
+ )
+ )
+
+ checkNotNull(uuid).let {
+ underTest.updateTransition(it, 1f, TransitionState.FINISHED)
+ underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
+ }
+ assertThat(wtfHandler.failed).isTrue()
+ }
+
+ private fun listWithStep(step: BigDecimal): List<BigDecimal> {
+ val steps = mutableListOf<BigDecimal>()
+
+ var i = BigDecimal.ZERO
+ while (i.compareTo(BigDecimal.ONE) <= 0) {
+ steps.add(i)
+ i = (i + step).setScale(2, RoundingMode.HALF_UP)
+ }
+
+ return steps
+ }
+
+ private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) {
+ // + 2 accounts for start and finish of automated transition
+ assertThat(steps.size).isEqualTo(fractions.size + 2)
+
+ assertThat(steps[0]).isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED))
+ fractions.forEachIndexed { index, fraction ->
+ assertThat(steps[index + 1])
+ .isEqualTo(
+ TransitionStep(AOD, LOCKSCREEN, fraction.toFloat(), TransitionState.RUNNING)
+ )
+ }
+ assertThat(steps[steps.size - 1])
+ .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED))
+
+ assertThat(wtfHandler.failed).isFalse()
+ }
+
+ private fun setupAnimator(
+ scope: CoroutineScope
+ ): Pair<ValueAnimator, TestFrameCallbackProvider> {
+ val animator =
+ ValueAnimator().apply {
+ setInterpolator(Interpolators.LINEAR)
+ setDuration(ANIMATION_DURATION)
+ }
+
+ val provider = TestFrameCallbackProvider(animator, scope)
+ provider.start()
+
+ return Pair(animator, provider)
+ }
+
+ /** Gives direct control over ValueAnimator. See [AnimationHandler] */
+ private class TestFrameCallbackProvider(
+ private val animator: ValueAnimator,
+ private val scope: CoroutineScope,
+ ) : AnimationFrameCallbackProvider {
+
+ private var frameCount = 1L
+ private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null))
+ private var job: Job? = null
+
+ fun start() {
+ animator.getAnimationHandler().setProvider(this)
+
+ job =
+ scope.launch {
+ frames.collect {
+ // Delay is required for AnimationHandler to properly register a callback
+ delay(1)
+ val (frameNumber, callback) = it
+ callback?.doFrame(frameNumber)
+ }
+ }
+ }
+
+ fun stop() {
+ job?.cancel()
+ animator.getAnimationHandler().setProvider(null)
+ }
+
+ override fun postFrameCallback(cb: FrameCallback) {
+ frames.value = Pair(++frameCount, cb)
+ }
+ override fun postCommitCallback(runnable: Runnable) {}
+ override fun getFrameTime() = frameCount
+ override fun getFrameDelay() = 1L
+ override fun setFrameDelay(delay: Long) {}
+ }
+
+ private class WtfHandler : TerribleFailureHandler {
+ var failed = false
+ override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) {
+ failed = true
+ }
+ }
+
+ companion object {
+ private const val MAX_TEST_DURATION = 100L
+ private const val ANIMATION_DURATION = 10L
+ private const val OWNER_NAME = "Test"
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorParameterizedTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index b6d7559..b4d5464 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -12,20 +12,20 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
+ *
*/
-package com.android.systemui.keyguard.domain.usecase
+package com.android.systemui.keyguard.domain.interactor
import android.content.Intent
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityLaunchAnimator
+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.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
@@ -195,6 +195,7 @@
@Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
+ @Mock private lateinit var expandable: Expandable
private lateinit var underTest: KeyguardQuickAffordanceInteractor
@@ -208,6 +209,7 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ whenever(expandable.activityLaunchController()).thenReturn(animationController)
homeControls = object : FakeKeyguardQuickAffordanceConfig() {}
underTest =
@@ -259,7 +261,7 @@
underTest.onQuickAffordanceClicked(
configKey = homeControls::class,
- animationController = animationController,
+ expandable = expandable,
)
if (startActivity) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 1dd919a..65fd6e5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -12,9 +12,10 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
+ *
*/
-package com.android.systemui.keyguard.domain.usecase
+package com.android.systemui.keyguard.domain.interactor
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
@@ -22,13 +23,12 @@
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -103,6 +103,7 @@
homeControls.setState(
KeyguardQuickAffordanceConfig.State.Visible(
icon = ICON,
+ toggle = KeyguardQuickAffordanceToggleState.On,
)
)
@@ -123,6 +124,7 @@
assertThat(visibleModel.icon).isEqualTo(ICON)
assertThat(visibleModel.icon.contentDescription)
.isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
+ assertThat(visibleModel.toggle).isEqualTo(KeyguardQuickAffordanceToggleState.On)
job.cancel()
}
@@ -152,6 +154,7 @@
assertThat(visibleModel.icon).isEqualTo(ICON)
assertThat(visibleModel.icon.contentDescription)
.isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
+ assertThat(visibleModel.toggle).isEqualTo(KeyguardQuickAffordanceToggleState.NotSupported)
job.cancel()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
index 6ea1daa..e99c139 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
@@ -17,7 +17,7 @@
package com.android.systemui.keyguard.domain.quickaffordance
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -40,7 +40,7 @@
override val state: Flow<KeyguardQuickAffordanceConfig.State> = _state
override fun onQuickAffordanceClicked(
- animationController: ActivityLaunchAnimator.Controller?,
+ expandable: Expandable?,
): OnClickedResult {
return onClickedResult
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
index dede4ec..a809f05 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
@@ -20,7 +20,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
@@ -44,7 +44,7 @@
class HomeControlsKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
@Mock private lateinit var component: ControlsComponent
- @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
+ @Mock private lateinit var expandable: Expandable
private lateinit var underTest: HomeControlsKeyguardQuickAffordanceConfig
@@ -103,7 +103,7 @@
fun `onQuickAffordanceClicked - canShowWhileLockedSetting is true`() = runBlockingTest {
whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(true))
- val onClickedResult = underTest.onQuickAffordanceClicked(animationController)
+ val onClickedResult = underTest.onQuickAffordanceClicked(expandable)
assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java)
assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isTrue()
@@ -113,7 +113,7 @@
fun `onQuickAffordanceClicked - canShowWhileLockedSetting is false`() = runBlockingTest {
whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(false))
- val onClickedResult = underTest.onQuickAffordanceClicked(animationController)
+ val onClickedResult = underTest.onQuickAffordanceClicked(expandable)
assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java)
assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
index 0a4478f..98dc4c4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
@@ -24,11 +24,13 @@
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityLaunchAnimator
+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.plugins.ActivityStarter
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import com.android.systemui.wallet.controller.QuickAccessWalletController
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
@@ -40,7 +42,6 @@
import org.junit.runners.JUnit4
import org.mockito.Mock
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@SmallTest
@@ -135,8 +136,11 @@
@Test
fun onQuickAffordanceClicked() {
val animationController: ActivityLaunchAnimator.Controller = mock()
+ val expandable: Expandable = mock {
+ whenever(this.activityLaunchController()).thenReturn(animationController)
+ }
- assertThat(underTest.onQuickAffordanceClicked(animationController))
+ assertThat(underTest.onQuickAffordanceClicked(expandable))
.isEqualTo(KeyguardQuickAffordanceConfig.OnClickedResult.Handled)
verify(walletController)
.startQuickAccessUiIntent(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 96544e7..d674c89 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -20,7 +20,7 @@
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.doze.util.BurnInHelperWrapper
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
@@ -31,6 +31,7 @@
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -59,7 +60,7 @@
@RunWith(JUnit4::class)
class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
- @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
+ @Mock private lateinit var expandable: Expandable
@Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
@Mock private lateinit var lockPatternUtils: LockPatternUtils
@Mock private lateinit var keyguardStateController: KeyguardStateController
@@ -130,6 +131,7 @@
TestConfig(
isVisible = true,
isClickable = true,
+ isActivated = true,
icon = mock(),
canShowWhileLocked = false,
intent = Intent("action"),
@@ -505,6 +507,12 @@
}
KeyguardQuickAffordanceConfig.State.Visible(
icon = testConfig.icon ?: error("Icon is unexpectedly null!"),
+ toggle =
+ when (testConfig.isActivated) {
+ true -> KeyguardQuickAffordanceToggleState.On
+ false -> KeyguardQuickAffordanceToggleState.Off
+ null -> KeyguardQuickAffordanceToggleState.NotSupported
+ }
)
} else {
KeyguardQuickAffordanceConfig.State.Hidden
@@ -521,12 +529,13 @@
checkNotNull(viewModel)
assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
assertThat(viewModel.isClickable).isEqualTo(testConfig.isClickable)
+ assertThat(viewModel.isActivated).isEqualTo(testConfig.isActivated)
if (testConfig.isVisible) {
assertThat(viewModel.icon).isEqualTo(testConfig.icon)
viewModel.onClicked.invoke(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = configKey,
- animationController = animationController,
+ expandable = expandable,
)
)
if (testConfig.intent != null) {
@@ -542,6 +551,7 @@
private data class TestConfig(
val isVisible: Boolean,
val isClickable: Boolean = false,
+ val isActivated: Boolean = false,
val icon: Icon? = null,
val canShowWhileLocked: Boolean = false,
val intent: Intent? = null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
index b8e9cf4..dc5522e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
@@ -82,7 +82,6 @@
MockitoAnnotations.initMocks(this);
mSessionTracker = new SessionTracker(
- mContext,
mStatusBarService,
mAuthController,
mKeyguardUpdateMonitor,
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 5ad3542..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
+++ /dev/null
@@ -1,401 +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.plugins.ActivityStarter
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
-import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.time.FakeSystemClock
-import javax.inject.Provider
-import junit.framework.Assert.assertEquals
-import junit.framework.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-private val DATA = MediaTestUtils.emptyMediaData
-
-private val SMARTSPACE_KEY = "smartspace"
-
-@SmallTest
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
-class MediaCarouselControllerTest : SysuiTestCase() {
-
- @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel>
- @Mock lateinit var panel: MediaControlPanel
- @Mock lateinit var visualStabilityProvider: VisualStabilityProvider
- @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
- @Mock lateinit var mediaHostState: MediaHostState
- @Mock lateinit var activityStarter: ActivityStarter
- @Mock @Main private lateinit var executor: DelayableExecutor
- @Mock lateinit var mediaDataManager: MediaDataManager
- @Mock lateinit var configurationController: ConfigurationController
- @Mock lateinit var falsingCollector: FalsingCollector
- @Mock lateinit var falsingManager: FalsingManager
- @Mock lateinit var dumpManager: DumpManager
- @Mock lateinit var logger: MediaUiEventLogger
- @Mock lateinit var debugLogger: MediaCarouselControllerLogger
- @Mock lateinit var mediaPlayer: MediaControlPanel
- @Mock lateinit var mediaViewController: MediaViewController
- @Mock lateinit var smartspaceMediaData: SmartspaceMediaData
- @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
- @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener>
-
- private val clock = FakeSystemClock()
- private lateinit var mediaCarouselController: MediaCarouselController
-
- @Before
- fun setup() {
- MockitoAnnotations.initMocks(this)
- mediaCarouselController = MediaCarouselController(
- context,
- mediaControlPanelFactory,
- visualStabilityProvider,
- mediaHostStatesManager,
- activityStarter,
- clock,
- executor,
- mediaDataManager,
- configurationController,
- falsingCollector,
- falsingManager,
- dumpManager,
- logger,
- debugLogger
- )
- verify(mediaDataManager).addListener(capture(listener))
- verify(visualStabilityProvider)
- .addPersistentReorderingAllowedListener(capture(visualStabilityCallback))
- whenever(mediaControlPanelFactory.get()).thenReturn(mediaPlayer)
- whenever(mediaPlayer.mediaViewController).thenReturn(mediaViewController)
- whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
- MediaPlayerData.clear()
- }
-
- @Test
- fun testPlayerOrdering() {
- // Test values: key, data, last active time
- val playingLocal = Triple("playing local",
- DATA.copy(active = true, isPlaying = true,
- playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false),
- 4500L)
-
- val playingCast = Triple("playing cast",
- DATA.copy(active = true, isPlaying = true,
- playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false),
- 5000L)
-
- val pausedLocal = Triple("paused local",
- DATA.copy(active = true, isPlaying = false,
- playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false),
- 1000L)
-
- val pausedCast = Triple("paused cast",
- DATA.copy(active = true, isPlaying = false,
- playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false),
- 2000L)
-
- val playingRcn = Triple("playing RCN",
- DATA.copy(active = true, isPlaying = true,
- playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false),
- 5000L)
-
- val pausedRcn = Triple("paused RCN",
- DATA.copy(active = true, isPlaying = false,
- playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false),
- 5000L)
-
- val active = Triple("active",
- DATA.copy(active = true, isPlaying = false,
- playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
- 250L)
-
- val resume1 = Triple("resume 1",
- DATA.copy(active = false, isPlaying = false,
- playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
- 500L)
-
- val resume2 = Triple("resume 2",
- DATA.copy(active = false, isPlaying = false,
- playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
- 1000L)
-
- val activeMoreRecent = Triple("active more recent",
- DATA.copy(active = false, isPlaying = false,
- playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 2L),
- 1000L)
-
- val activeLessRecent = Triple("active less recent",
- DATA.copy(active = false, isPlaying = false,
- playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 1L),
- 1000L)
- // Expected ordering for media players:
- // Actively playing local sessions
- // Actively playing cast sessions
- // Paused local and cast sessions, by last active
- // RCNs
- // Resume controls, by last active
-
- val expected = listOf(playingLocal, playingCast, pausedCast, pausedLocal, playingRcn,
- pausedRcn, active, resume2, resume1)
-
- expected.forEach {
- clock.setCurrentTimeMillis(it.third)
- MediaPlayerData.addMediaPlayer(it.first, it.second.copy(notificationKey = it.first),
- panel, clock, isSsReactivated = false)
- }
-
- for ((index, key) in MediaPlayerData.playerKeys().withIndex()) {
- assertEquals(expected.get(index).first, key.data.notificationKey)
- }
- }
-
- @Test
- fun testOrderWithSmartspace_prioritized() {
- testPlayerOrdering()
-
- // If smartspace is prioritized
- MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
- true, clock)
-
- // Then it should be shown immediately after any actively playing controls
- assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
- }
-
- @Test
- fun testOrderWithSmartspace_notPrioritized() {
- testPlayerOrdering()
-
- // If smartspace is not prioritized
- MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
- false, clock)
-
- // Then it should be shown at the end of the carousel's active entries
- val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1
- assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec)
- }
-
- @Test
- fun testSwipeDismiss_logged() {
- mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke()
-
- verify(logger).logSwipeDismiss()
- }
-
- @Test
- fun testSettingsButton_logged() {
- mediaCarouselController.settingsButton.callOnClick()
-
- verify(logger).logCarouselSettings()
- }
-
- @Test
- fun testLocationChangeQs_logged() {
- mediaCarouselController.onDesiredLocationChanged(
- MediaHierarchyManager.LOCATION_QS,
- mediaHostState,
- animate = false)
- verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS)
- }
-
- @Test
- fun testLocationChangeQqs_logged() {
- mediaCarouselController.onDesiredLocationChanged(
- MediaHierarchyManager.LOCATION_QQS,
- mediaHostState,
- animate = false)
- verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
- }
-
- @Test
- fun testLocationChangeLockscreen_logged() {
- mediaCarouselController.onDesiredLocationChanged(
- MediaHierarchyManager.LOCATION_LOCKSCREEN,
- mediaHostState,
- animate = false)
- verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
- }
-
- @Test
- fun testLocationChangeDream_logged() {
- mediaCarouselController.onDesiredLocationChanged(
- MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
- mediaHostState,
- animate = false)
- verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
- }
-
- @Test
- fun testRecommendationRemoved_logged() {
- val packageName = "smartspace package"
- val instanceId = InstanceId.fakeInstanceId(123)
-
- val smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- packageName = packageName,
- instanceId = instanceId
- )
- MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock)
- mediaCarouselController.removePlayer(SMARTSPACE_KEY)
-
- verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!))
- }
-
- 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.
- MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
- false, clock)
- mediaCarouselController.shouldScrollToActivePlayer = true
- // switching between media players.
- listener.value.onMediaDataLoaded("playing local",
- "playing local",
- DATA.copy(active = true, isPlaying = false,
- playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true)
- )
- listener.value.onMediaDataLoaded("paused local",
- "paused local",
- DATA.copy(active = true, isPlaying = true,
- playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false))
-
- assertEquals(
- MediaPlayerData.getMediaPlayerIndex("paused local"),
- mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
- )
- }
-
- @Test
- fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() {
- MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
- false, clock)
- listener.value.onMediaDataLoaded("playing local",
- null,
- DATA.copy(active = true, isPlaying = true,
- playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)
- )
-
- var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
- assertEquals(
- playerIndex,
- mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
- )
- assertEquals(playerIndex, 0)
-
- // Replaying the same media player one more time.
- // And check that the card stays in its position.
- listener.value.onMediaDataLoaded("playing local",
- null,
- DATA.copy(active = true, isPlaying = true,
- playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)
- )
- playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
- assertEquals(playerIndex, 0)
- }
-
- @Test
- fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() {
- var result = false
- mediaCarouselController.updateHostVisibility = { result = true }
-
- whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true)
- listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false)
-
- assertEquals(true, result)
- }
-
- @Test
- fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() {
- var result = false
- mediaCarouselController.updateHostVisibility = { result = true }
-
- whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false)
- listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false)
- assertEquals(false, result)
-
- visualStabilityCallback.value.onReorderingAllowed()
- assertEquals(true, result)
- }
-
- @Test
- fun testGetCurrentVisibleMediaContentIntent() {
- val clickIntent1 = mock(PendingIntent::class.java)
- val player1 = Triple("player1",
- DATA.copy(clickIntent = clickIntent1),
- 1000L)
- clock.setCurrentTimeMillis(player1.third)
- MediaPlayerData.addMediaPlayer(player1.first,
- player1.second.copy(notificationKey = player1.first),
- panel, clock, isSsReactivated = false)
-
- assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
-
- val clickIntent2 = mock(PendingIntent::class.java)
- val player2 = Triple("player2",
- DATA.copy(clickIntent = clickIntent2),
- 2000L)
- clock.setCurrentTimeMillis(player2.third)
- MediaPlayerData.addMediaPlayer(player2.first,
- player2.second.copy(notificationKey = player2.first),
- panel, clock, isSsReactivated = false)
-
- // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
- // added to the front because it was active more recently.
- assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
-
- val clickIntent3 = mock(PendingIntent::class.java)
- val player3 = Triple("player3",
- DATA.copy(clickIntent = clickIntent3),
- 500L)
- clock.setCurrentTimeMillis(player3.third)
- MediaPlayerData.addMediaPlayer(player3.first,
- player3.second.copy(notificationKey = player3.first),
- panel, clock, isSsReactivated = false)
-
- // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
- // added to the end because it was active less recently.
- assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
- }
-}
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 3d3ac83..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/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
new file mode 100644
index 0000000..6b76155
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+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
+import com.android.systemui.util.animation.WidgetState
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.floatThat
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class MediaViewControllerTest : SysuiTestCase() {
+ private val mediaHostStateHolder = MediaHost.MediaHostStateHolder()
+ private val mediaHostStatesManager = MediaHostStatesManager()
+ private val configurationController =
+ com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context)
+ private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
+ private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
+ @Mock lateinit var logger: MediaViewLogger
+ @Mock private lateinit var mockViewState: TransitionViewState
+ @Mock private lateinit var mockCopiedState: TransitionViewState
+ @Mock private lateinit var detailWidgetState: WidgetState
+ @Mock private lateinit var controlWidgetState: WidgetState
+ @Mock private lateinit var mediaTitleWidgetState: WidgetState
+ @Mock private lateinit var mediaContainerWidgetState: WidgetState
+
+ val delta = 0.0001F
+
+ private lateinit var mediaViewController: MediaViewController
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ mediaViewController =
+ MediaViewController(context, configurationController, mediaHostStatesManager, logger)
+ }
+
+ @Test
+ fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() {
+ mediaViewController.attach(player, MediaViewController.TYPE.PLAYER)
+ player.measureState = TransitionViewState().apply { this.height = 100 }
+ mediaHostStateHolder.expansion = 1f
+ val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ mediaHostStateHolder.measurementInput =
+ MeasurementInput(widthMeasureSpec, heightMeasureSpec)
+
+ // Test no squish
+ mediaHostStateHolder.squishFraction = 1f
+ assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
+
+ // Test half squish
+ mediaHostStateHolder.squishFraction = 0.5f
+ assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
+ }
+
+ @Test
+ fun testObtainViewState_applySquishFraction_toRecommendationTransitionViewState_height() {
+ mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION)
+ recommendation.measureState = TransitionViewState().apply { this.height = 100 }
+ mediaHostStateHolder.expansion = 1f
+ val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ mediaHostStateHolder.measurementInput =
+ MeasurementInput(widthMeasureSpec, heightMeasureSpec)
+
+ // Test no squish
+ mediaHostStateHolder.squishFraction = 1f
+ assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
+
+ // Test half squish
+ mediaHostStateHolder.squishFraction = 0.5f
+ assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
+ }
+
+ @Test
+ fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() {
+ whenever(mockViewState.copy()).thenReturn(mockCopiedState)
+ whenever(mockCopiedState.widgetStates)
+ .thenReturn(
+ mutableMapOf(
+ R.id.media_progress_bar to controlWidgetState,
+ R.id.header_artist to detailWidgetState
+ )
+ )
+
+ val detailSquishMiddle =
+ TRANSFORM_BEZIER.getInterpolation(
+ (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, detailSquishMiddle)
+ verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+ val detailSquishEnd =
+ TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+ mediaViewController.squishViewState(mockViewState, detailSquishEnd)
+ verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+
+ val controlSquishMiddle =
+ TRANSFORM_BEZIER.getInterpolation(
+ (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, controlSquishMiddle)
+ verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+ val controlSquishEnd =
+ TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+ mediaViewController.squishViewState(mockViewState, controlSquishEnd)
+ verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+ }
+
+ @Test
+ fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() {
+ whenever(mockViewState.copy()).thenReturn(mockCopiedState)
+ whenever(mockCopiedState.widgetStates)
+ .thenReturn(
+ mutableMapOf(
+ R.id.media_title1 to mediaTitleWidgetState,
+ R.id.media_cover1_container to mediaContainerWidgetState
+ )
+ )
+
+ val containerSquishMiddle =
+ TRANSFORM_BEZIER.getInterpolation(
+ (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, containerSquishMiddle)
+ verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+ val containerSquishEnd =
+ TRANSFORM_BEZIER.getInterpolation(
+ (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, containerSquishEnd)
+ verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+
+ val titleSquishMiddle =
+ TRANSFORM_BEZIER.getInterpolation(
+ (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, titleSquishMiddle)
+ verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+ val titleSquishEnd =
+ TRANSFORM_BEZIER.getInterpolation(
+ (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, titleSquishEnd)
+ verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+ }
+}
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 2f52950..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;
@@ -73,7 +73,7 @@
@Test
public void testOnMediaDataLoaded_complicationAddition() {
- final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
+ final MediaDreamSentinel sentinel = new MediaDreamSentinel(mMediaDataManager,
mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags);
sentinel.start();
@@ -94,7 +94,7 @@
@Test
public void testOnMediaDataRemoved_complicationRemoval() {
- final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
+ final MediaDreamSentinel sentinel = new MediaDreamSentinel(mMediaDataManager,
mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags);
sentinel.start();
@@ -114,7 +114,7 @@
@Test
public void testOnMediaDataLoaded_complicationRemoval() {
- final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
+ final MediaDreamSentinel sentinel = new MediaDreamSentinel(mMediaDataManager,
mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags);
sentinel.start();
@@ -139,7 +139,7 @@
public void testOnMediaDataLoaded_mediaComplicationDisabled_doesNotAddComplication() {
when(mFeatureFlags.isEnabled(DREAM_MEDIA_COMPLICATION)).thenReturn(false);
- final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
+ final MediaDreamSentinel sentinel = new MediaDreamSentinel(mMediaDataManager,
mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags);
sentinel.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
index 1078cda..e009e86 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
@@ -19,9 +19,9 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
index 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/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index 9577274..8c3ae3d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -34,6 +34,7 @@
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.taptotransfer.MediaTttFlags
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -49,6 +50,7 @@
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@@ -70,6 +72,8 @@
@Mock
private lateinit var configurationController: ConfigurationController
@Mock
+ private lateinit var mediaTttFlags: MediaTttFlags
+ @Mock
private lateinit var powerManager: PowerManager
@Mock
private lateinit var viewUtil: ViewUtil
@@ -85,6 +89,7 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true)
fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
@@ -107,6 +112,7 @@
configurationController,
powerManager,
Handler.getMain(),
+ mediaTttFlags,
receiverUiEventLogger,
viewUtil,
)
@@ -118,6 +124,30 @@
}
@Test
+ fun commandQueueCallback_flagOff_noCallbackAdded() {
+ reset(commandQueue)
+ whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(false)
+
+ controllerReceiver = MediaTttChipControllerReceiver(
+ commandQueue,
+ context,
+ logger,
+ windowManager,
+ FakeExecutor(FakeSystemClock()),
+ accessibilityManager,
+ configurationController,
+ powerManager,
+ Handler.getMain(),
+ mediaTttFlags,
+ receiverUiEventLogger,
+ viewUtil,
+ )
+ controllerReceiver.start()
+
+ verify(commandQueue, never()).addCallback(any())
+ }
+
+ @Test
fun commandQueueCallback_closeToSender_triggersChip() {
val appName = "FakeAppName"
commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
deleted file mode 100644
index 3a8a51d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
+++ /dev/null
@@ -1,868 +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.taptotransfer.sender
-
-import android.app.StatusBarManager
-import android.content.Context
-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.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
-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.media.taptotransfer.common.MediaTttLogger
-import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.concurrency.DelayableExecutor
-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
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-class MediaTttChipControllerSenderTest : SysuiTestCase() {
- private lateinit var controllerSender: TestMediaTttChipControllerSender
-
- @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 commandQueue: CommandQueue
- @Mock
- private lateinit var falsingManager: FalsingManager
- @Mock
- private lateinit var falsingCollector: FalsingCollector
- @Mock
- private lateinit var viewUtil: ViewUtil
- 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
- 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)
-
- fakeClock = FakeSystemClock()
- fakeExecutor = FakeExecutor(fakeClock)
-
- uiEventLoggerFake = UiEventLoggerFake()
- senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
-
- whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
-
- controllerSender = TestMediaTttChipControllerSender(
- commandQueue,
- context,
- logger,
- windowManager,
- fakeExecutor,
- accessibilityManager,
- configurationController,
- powerManager,
- senderUiEventLogger,
- falsingManager,
- falsingCollector,
- viewUtil,
- )
- controllerSender.start()
-
- val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
- verify(commandQueue).addCallback(callbackCaptor.capture())
- commandQueueCallback = callbackCaptor.value!!
- }
-
- @Test
- fun commandQueueCallback_almostCloseToStartCast_triggersCorrectChip() {
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
- routeInfo,
- null
- )
-
- assertThat(getChipView().getChipText()).isEqualTo(
- almostCloseToStartCast().state.getChipTextString(context, OTHER_DEVICE_NAME)
- )
- assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
- MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id
- )
- }
-
- @Test
- fun commandQueueCallback_almostCloseToEndCast_triggersCorrectChip() {
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
- routeInfo,
- null
- )
-
- assertThat(getChipView().getChipText()).isEqualTo(
- almostCloseToEndCast().state.getChipTextString(context, OTHER_DEVICE_NAME)
- )
- assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
- MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id
- )
- }
-
- @Test
- fun commandQueueCallback_transferToReceiverTriggered_triggersCorrectChip() {
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
- routeInfo,
- null
- )
-
- assertThat(getChipView().getChipText()).isEqualTo(
- transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
- )
- assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
- MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id
- )
- }
-
- @Test
- fun commandQueueCallback_transferToThisDeviceTriggered_triggersCorrectChip() {
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
- routeInfo,
- null
- )
-
- assertThat(getChipView().getChipText()).isEqualTo(
- transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
- )
- assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
- MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id
- )
- }
-
- @Test
- fun commandQueueCallback_transferToReceiverSucceeded_triggersCorrectChip() {
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
- routeInfo,
- null
- )
-
- assertThat(getChipView().getChipText()).isEqualTo(
- transferToReceiverSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME)
- )
- assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
- MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id
- )
- }
-
- @Test
- fun commandQueueCallback_transferToThisDeviceSucceeded_triggersCorrectChip() {
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
- routeInfo,
- null
- )
-
- assertThat(getChipView().getChipText()).isEqualTo(
- transferToThisDeviceSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME)
- )
- assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
- MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id
- )
- }
-
- @Test
- fun commandQueueCallback_transferToReceiverFailed_triggersCorrectChip() {
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED,
- routeInfo,
- null
- )
-
- assertThat(getChipView().getChipText()).isEqualTo(
- transferToReceiverFailed().state.getChipTextString(context, OTHER_DEVICE_NAME)
- )
- assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
- MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id
- )
- }
-
- @Test
- fun commandQueueCallback_transferToThisDeviceFailed_triggersCorrectChip() {
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED,
- routeInfo,
- null
- )
-
- assertThat(getChipView().getChipText()).isEqualTo(
- transferToThisDeviceFailed().state.getChipTextString(context, OTHER_DEVICE_NAME)
- )
- assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
- MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id
- )
- }
-
- @Test
- fun commandQueueCallback_farFromReceiver_noChipShown() {
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
- routeInfo,
- null
- )
-
- verify(windowManager, never()).addView(any(), any())
- assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
- MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER.id
- )
- }
-
- @Test
- fun commandQueueCallback_almostCloseThenFarFromReceiver_chipShownThenHidden() {
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
- routeInfo,
- null
- )
-
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
- routeInfo,
- null
- )
-
- val viewCaptor = ArgumentCaptor.forClass(View::class.java)
- verify(windowManager).addView(viewCaptor.capture(), any())
- verify(windowManager).removeView(viewCaptor.value)
- }
-
- @Test
- fun commandQueueCallback_invalidStateParam_noChipShown() {
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- 100,
- routeInfo,
- null
- )
-
- verify(windowManager, never()).addView(any(), any())
- }
-
- @Test
- fun receivesNewStateFromCommandQueue_isLogged() {
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
- routeInfo,
- null
- )
-
- verify(logger).logStateChange(any(), any(), any())
- }
-
- @Test
- fun almostCloseToStartCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() {
- val state = almostCloseToStartCast()
- controllerSender.displayView(state)
-
- val chipView = getChipView()
- assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
- assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
- assertThat(chipView.getChipText()).isEqualTo(
- state.state.getChipTextString(context, OTHER_DEVICE_NAME)
- )
- assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
- assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
- assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
- }
-
- @Test
- fun almostCloseToEndCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() {
- val state = almostCloseToEndCast()
- controllerSender.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()
- controllerSender.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()
- controllerSender.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()
- controllerSender.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() {
- controllerSender.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() {}
- }
- controllerSender.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
- }
- }
-
- controllerSender.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
- }
- }
-
- controllerSender.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
- }
- }
-
- controllerSender.displayView(transferToReceiverSucceeded(undoCallback))
- getChipView().getUndoButton().performClick()
-
- assertThat(undoCallbackCalled).isTrue()
- }
-
- @Test
- fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
- val undoCallback = object : IUndoMediaTransferCallback.Stub() {
- override fun onUndoTriggered() {}
- }
- controllerSender.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()
- controllerSender.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() {
- controllerSender.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() {}
- }
- controllerSender.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
- }
- }
-
- controllerSender.displayView(transferToThisDeviceSucceeded(undoCallback))
- getChipView().getUndoButton().performClick()
-
- assertThat(undoCallbackCalled).isTrue()
- }
-
- @Test
- fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToReceiverTriggered() {
- val undoCallback = object : IUndoMediaTransferCallback.Stub() {
- override fun onUndoTriggered() {}
- }
- controllerSender.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()
- controllerSender.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()
- controllerSender.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() {
- controllerSender.displayView(almostCloseToStartCast())
- controllerSender.displayView(transferToReceiverTriggered())
-
- assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
- }
-
- @Test
- fun changeFromTransferTriggeredToTransferSucceeded_loadingIconDisappears() {
- controllerSender.displayView(transferToReceiverTriggered())
- controllerSender.displayView(transferToReceiverSucceeded())
-
- assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE)
- }
-
- @Test
- fun changeFromTransferTriggeredToTransferSucceeded_undoButtonAppears() {
- controllerSender.displayView(transferToReceiverTriggered())
- controllerSender.displayView(
- transferToReceiverSucceeded(
- object : IUndoMediaTransferCallback.Stub() {
- override fun onUndoTriggered() {}
- }
- )
- )
-
- assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE)
- }
-
- @Test
- fun changeFromTransferSucceededToAlmostCloseToStart_undoButtonDisappears() {
- controllerSender.displayView(transferToReceiverSucceeded())
- controllerSender.displayView(almostCloseToStartCast())
-
- assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.GONE)
- }
-
- @Test
- fun changeFromTransferTriggeredToTransferFailed_failureIconAppears() {
- controllerSender.displayView(transferToReceiverTriggered())
- controllerSender.displayView(transferToReceiverFailed())
-
- assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE)
- }
-
- @Test
- fun transferToReceiverTriggeredThenRemoveView_viewStillDisplayed() {
- controllerSender.displayView(transferToReceiverTriggered())
- fakeClock.advanceTime(1000L)
-
- controllerSender.removeView("fakeRemovalReason")
- fakeExecutor.runAllReady()
-
- verify(windowManager, never()).removeView(any())
- verify(logger).logRemovalBypass(any(), any())
- }
-
- @Test
- fun transferToReceiverTriggeredThenFarFromReceiver_viewStillDisplayed() {
- controllerSender.displayView(transferToReceiverTriggered())
-
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
- routeInfo,
- null
- )
- fakeExecutor.runAllReady()
-
- verify(windowManager, never()).removeView(any())
- verify(logger).logRemovalBypass(any(), any())
- }
-
- @Test
- fun transferToReceiverTriggeredThenRemoveView_eventuallyTimesOut() {
- controllerSender.displayView(transferToReceiverTriggered())
-
- controllerSender.removeView("fakeRemovalReason")
- fakeClock.advanceTime(TIMEOUT + 1L)
-
- verify(windowManager).removeView(any())
- }
-
- @Test
- fun transferToThisDeviceTriggeredThenRemoveView_viewStillDisplayed() {
- controllerSender.displayView(transferToThisDeviceTriggered())
- fakeClock.advanceTime(1000L)
-
- controllerSender.removeView("fakeRemovalReason")
- fakeExecutor.runAllReady()
-
- verify(windowManager, never()).removeView(any())
- verify(logger).logRemovalBypass(any(), any())
- }
-
- @Test
- fun transferToThisDeviceTriggeredThenRemoveView_eventuallyTimesOut() {
- controllerSender.displayView(transferToThisDeviceTriggered())
-
- controllerSender.removeView("fakeRemovalReason")
- fakeClock.advanceTime(TIMEOUT + 1L)
-
- verify(windowManager).removeView(any())
- }
-
- @Test
- fun transferToThisDeviceTriggeredThenFarFromReceiver_viewStillDisplayed() {
- controllerSender.displayView(transferToThisDeviceTriggered())
-
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
- routeInfo,
- null
- )
- fakeExecutor.runAllReady()
-
- verify(windowManager, never()).removeView(any())
- verify(logger).logRemovalBypass(any(), any())
- }
-
- @Test
- fun transferToReceiverSucceededThenRemoveView_viewStillDisplayed() {
- controllerSender.displayView(transferToReceiverSucceeded())
-
- controllerSender.removeView("fakeRemovalReason")
- fakeExecutor.runAllReady()
-
- verify(windowManager, never()).removeView(any())
- verify(logger).logRemovalBypass(any(), any())
- }
-
- @Test
- fun transferToReceiverSucceededThenRemoveView_eventuallyTimesOut() {
- controllerSender.displayView(transferToReceiverSucceeded())
-
- controllerSender.removeView("fakeRemovalReason")
- fakeClock.advanceTime(TIMEOUT + 1L)
-
- verify(windowManager).removeView(any())
- }
-
- @Test
- fun transferToReceiverSucceededThenFarFromReceiver_viewStillDisplayed() {
- controllerSender.displayView(transferToReceiverSucceeded())
-
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
- routeInfo,
- null
- )
- fakeExecutor.runAllReady()
-
- verify(windowManager, never()).removeView(any())
- verify(logger).logRemovalBypass(any(), any())
- }
-
- @Test
- fun transferToThisDeviceSucceededThenRemoveView_viewStillDisplayed() {
- controllerSender.displayView(transferToThisDeviceSucceeded())
-
- controllerSender.removeView("fakeRemovalReason")
- fakeExecutor.runAllReady()
-
- verify(windowManager, never()).removeView(any())
- verify(logger).logRemovalBypass(any(), any())
- }
-
- @Test
- fun transferToThisDeviceSucceededThenRemoveView_eventuallyTimesOut() {
- controllerSender.displayView(transferToThisDeviceSucceeded())
-
- controllerSender.removeView("fakeRemovalReason")
- fakeClock.advanceTime(TIMEOUT + 1L)
-
- verify(windowManager).removeView(any())
- }
-
- @Test
- fun transferToThisDeviceSucceededThenFarFromReceiver_viewStillDisplayed() {
- controllerSender.displayView(transferToThisDeviceSucceeded())
-
- commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
- routeInfo,
- null
- )
- fakeExecutor.runAllReady()
-
- verify(windowManager, never()).removeView(any())
- verify(logger).logRemovalBypass(any(), any())
- }
-
- private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_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.getUndoButton(): View = this.requireViewById(R.id.undo)
-
- private fun ViewGroup.getFailureIcon(): View = this.requireViewById(R.id.failure_icon)
-
- private fun getChipView(): ViewGroup {
- val viewCaptor = ArgumentCaptor.forClass(View::class.java)
- verify(windowManager).addView(viewCaptor.capture(), any())
- return viewCaptor.value as ViewGroup
- }
-
- /** 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 class TestMediaTttChipControllerSender(
- commandQueue: CommandQueue,
- context: Context,
- @MediaTttReceiverLogger logger: MediaTttLogger,
- windowManager: WindowManager,
- mainExecutor: DelayableExecutor,
- accessibilityManager: AccessibilityManager,
- configurationController: ConfigurationController,
- powerManager: PowerManager,
- uiEventLogger: MediaTttSenderUiEventLogger,
- falsingManager: FalsingManager,
- falsingCollector: FalsingCollector,
- viewUtil: ViewUtil,
- ) : MediaTttChipControllerSender(
- commandQueue,
- context,
- logger,
- windowManager,
- mainExecutor,
- accessibilityManager,
- configurationController,
- powerManager,
- uiEventLogger,
- falsingManager,
- falsingCollector,
- viewUtil,
- ) {
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
- // Just bypass the animation in tests
- onAnimationEnd.run()
- }
- }
-}
-
-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/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
new file mode 100644
index 0000000..fdeb3f5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -0,0 +1,691 @@
+/*
+ * 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.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
+import com.android.internal.statusbar.IUndoMediaTransferCallback
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.common.shared.model.Text.Companion.loadText
+import com.android.systemui.media.taptotransfer.MediaTttFlags
+import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.temporarydisplay.chipbar.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
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@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
+ private lateinit var uiEventLogger: MediaTttSenderUiEventLogger
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ 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)
+
+ uiEventLoggerFake = UiEventLoggerFake()
+ uiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
+
+ chipbarCoordinator =
+ FakeChipbarCoordinator(
+ context,
+ logger,
+ windowManager,
+ fakeExecutor,
+ accessibilityManager,
+ configurationController,
+ powerManager,
+ falsingManager,
+ falsingCollector,
+ viewUtil,
+ vibratorHelper,
+ )
+ chipbarCoordinator.start()
+
+ underTest =
+ MediaTttSenderCoordinator(
+ chipbarCoordinator,
+ commandQueue,
+ context,
+ logger,
+ mediaTttFlags,
+ uiEventLogger,
+ )
+ underTest.start()
+
+ val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
+ verify(commandQueue).addCallback(callbackCaptor.capture())
+ commandQueueCallback = callbackCaptor.value!!
+ }
+
+ @Test
+ fun commandQueueCallback_flagOff_noCallbackAdded() {
+ reset(commandQueue)
+ whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(false)
+ underTest =
+ MediaTttSenderCoordinator(
+ chipbarCoordinator,
+ commandQueue,
+ context,
+ logger,
+ mediaTttFlags,
+ uiEventLogger,
+ )
+ underTest.start()
+
+ verify(commandQueue, never()).addCallback(any())
+ }
+
+ @Test
+ fun commandQueueCallback_almostCloseToStartCast_triggersCorrectChip() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+ routeInfo,
+ null
+ )
+
+ 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
+ fun commandQueueCallback_almostCloseToEndCast_triggersCorrectChip() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+ routeInfo,
+ null
+ )
+
+ 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
+ fun commandQueueCallback_transferToReceiverTriggered_triggersCorrectChip() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+ routeInfo,
+ null
+ )
+
+ 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
+ fun commandQueueCallback_transferToThisDeviceTriggered_triggersCorrectChip() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+ routeInfo,
+ null
+ )
+
+ 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
+ fun commandQueueCallback_transferToReceiverSucceeded_triggersCorrectChip() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+ routeInfo,
+ null
+ )
+
+ 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
+ fun commandQueueCallback_transferToThisDeviceSucceeded_triggersCorrectChip() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+ routeInfo,
+ null
+ )
+
+ 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
+ fun commandQueueCallback_transferToReceiverFailed_triggersCorrectChip() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED,
+ routeInfo,
+ null
+ )
+
+ 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
+ fun commandQueueCallback_transferToThisDeviceFailed_triggersCorrectChip() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED,
+ routeInfo,
+ null
+ )
+
+ 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
+ fun commandQueueCallback_farFromReceiver_noChipShown() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+ routeInfo,
+ null
+ )
+
+ verify(windowManager, never()).addView(any(), any())
+ assertThat(uiEventLoggerFake.eventId(0))
+ .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER.id)
+ }
+
+ @Test
+ fun commandQueueCallback_almostCloseThenFarFromReceiver_chipShownThenHidden() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+ routeInfo,
+ null
+ )
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+ routeInfo,
+ null
+ )
+
+ val viewCaptor = ArgumentCaptor.forClass(View::class.java)
+ verify(windowManager).addView(viewCaptor.capture(), any())
+ verify(windowManager).removeView(viewCaptor.value)
+ }
+
+ @Test
+ fun commandQueueCallback_invalidStateParam_noChipShown() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(100, routeInfo, null)
+
+ verify(windowManager, never()).addView(any(), any())
+ }
+
+ @Test
+ fun receivesNewStateFromCommandQueue_isLogged() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+ routeInfo,
+ null
+ )
+
+ verify(logger).logStateChange(any(), any(), any())
+ }
+
+ @Test
+ fun transferToReceiverTriggeredThenFarFromReceiver_viewStillDisplayedButStillTimesOut() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+ routeInfo,
+ null
+ )
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+ routeInfo,
+ null
+ )
+ fakeExecutor.runAllReady()
+
+ verify(windowManager, never()).removeView(any())
+ verify(logger).logRemovalBypass(any(), any())
+
+ fakeClock.advanceTime(TIMEOUT + 1L)
+
+ verify(windowManager).removeView(any())
+ }
+
+ @Test
+ fun transferToThisDeviceTriggeredThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+ routeInfo,
+ null
+ )
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+ routeInfo,
+ null
+ )
+ fakeExecutor.runAllReady()
+
+ verify(windowManager, never()).removeView(any())
+ verify(logger).logRemovalBypass(any(), any())
+
+ fakeClock.advanceTime(TIMEOUT + 1L)
+
+ verify(windowManager).removeView(any())
+ }
+
+ @Test
+ fun transferToReceiverSucceededThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+ routeInfo,
+ null
+ )
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+ routeInfo,
+ null
+ )
+ fakeExecutor.runAllReady()
+
+ verify(windowManager, never()).removeView(any())
+ verify(logger).logRemovalBypass(any(), any())
+
+ fakeClock.advanceTime(TIMEOUT + 1L)
+
+ verify(windowManager).removeView(any())
+ }
+
+ @Test
+ fun transferToThisDeviceSucceededThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+ routeInfo,
+ null
+ )
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+ routeInfo,
+ null
+ )
+ fakeExecutor.runAllReady()
+
+ verify(windowManager, never()).removeView(any())
+ verify(logger).logRemovalBypass(any(), any())
+
+ fakeClock.advanceTime(TIMEOUT + 1L)
+
+ verify(windowManager).removeView(any())
+ }
+
+ @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
+
+ private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
+
+ private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error)
+
+ private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button)
+
+ 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(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/navigationbar/NavBarHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
index 8073103..6c03730 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
@@ -39,7 +39,6 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.keyguard.KeyguardViewController;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
@@ -49,6 +48,7 @@
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.CentralSurfaces;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
import org.junit.Before;
import org.junit.Test;
@@ -113,7 +113,7 @@
mNavBarHelper = new NavBarHelper(mContext, mAccessibilityManager,
mAccessibilityButtonModeObserver, mAccessibilityButtonTargetObserver,
mSystemActions, mOverviewProxyService, mAssistManagerLazy,
- () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardViewController.class),
+ () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardStateController.class),
mNavigationModeController, mUserTracker, mDumpManager);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 51f0953..6adce7a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -72,7 +72,6 @@
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
-import com.android.keyguard.KeyguardViewController;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestableContext;
import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
@@ -81,6 +80,7 @@
import com.android.systemui.assist.AssistManager;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.buttons.ButtonDispatcher;
import com.android.systemui.navigationbar.buttons.DeadZone;
@@ -194,10 +194,12 @@
@Mock
private CentralSurfaces mCentralSurfaces;
@Mock
- private KeyguardViewController mKeyguardViewController;
+ private KeyguardStateController mKeyguardStateController;
@Mock
private UserContextProvider mUserContextProvider;
@Mock
+ private WakefulnessLifecycle mWakefulnessLifecycle;
+ @Mock
private Resources mResources;
private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
private DeviceConfigProxyFake mDeviceConfigProxyFake = new DeviceConfigProxyFake();
@@ -240,7 +242,7 @@
mock(AccessibilityButtonTargetsObserver.class),
mSystemActions, mOverviewProxyService,
() -> mock(AssistManager.class), () -> Optional.of(mCentralSurfaces),
- mKeyguardViewController, mock(NavigationModeController.class),
+ mKeyguardStateController, mock(NavigationModeController.class),
mock(UserTracker.class), mock(DumpManager.class)));
mNavigationBar = createNavBar(mContext);
mExternalDisplayNavigationBar = createNavBar(mSysuiTestableContextExternal);
@@ -380,7 +382,7 @@
// Verify navbar didn't alter and showing back icon when the keyguard is showing without
// requesting IME insets visible.
- doReturn(true).when(mKeyguardViewController).isShowing();
+ doReturn(true).when(mKeyguardStateController).isShowing();
mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE,
BACK_DISPOSITION_DEFAULT, true);
assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
@@ -475,7 +477,8 @@
mNavigationBarTransitions,
mEdgeBackGestureHandler,
Optional.of(mock(BackAnimation.class)),
- mUserContextProvider));
+ mUserContextProvider,
+ mWakefulnessLifecycle));
}
private void processAllMessages() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
index 4e9b232..c377c37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
@@ -46,6 +46,7 @@
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.power.PowerUI.WarningsUI;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -84,6 +85,7 @@
private PowerUI mPowerUI;
@Mock private EnhancedEstimates mEnhancedEstimates;
@Mock private PowerManager mPowerManager;
+ @Mock private WakefulnessLifecycle mWakefulnessLifecycle;
@Mock private IThermalService mThermalServiceMock;
private IThermalEventListener mUsbThermalEventListener;
private IThermalEventListener mSkinThermalEventListener;
@@ -680,7 +682,7 @@
private void createPowerUi() {
mPowerUI = new PowerUI(
mContext, mBroadcastDispatcher, mCommandQueue, mCentralSurfacesOptionalLazy,
- mMockWarnings, mEnhancedEstimates, mPowerManager);
+ mMockWarnings, mEnhancedEstimates, mWakefulnessLifecycle, mPowerManager);
mPowerUI.mThermalService = mThermalServiceMock;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
index e2c6ff9..d6db62a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
@@ -20,7 +20,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogcatEchoTracker
import com.android.systemui.statusbar.DisableFlagsLogger
import com.google.common.truth.Truth.assertThat
import org.junit.Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index 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/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
index 2a4996f..760bb9b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
@@ -192,16 +192,6 @@
// UserManager change.
assertThat(iconTint()).isNull()
- // Trigger a user info change: there should now be a tint.
- userInfoController.updateInfo { userAccount = "doe" }
- assertThat(iconTint())
- .isEqualTo(
- Utils.getColorAttrDefaultColor(
- context,
- android.R.attr.colorForeground,
- )
- )
-
// Make sure we don't tint the icon if it is a user image (and not the default image), even
// in guest mode.
userInfoController.updateInfo { this.picture = mock<UserIconDrawable>() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
index da52a9b..3131f60 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
@@ -30,9 +30,11 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.qs.QSUserSwitcherEvent
+import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.user.data.source.UserRecord
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -40,20 +42,27 @@
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
-import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
class UserDetailViewAdapterTest : SysuiTestCase() {
- @Mock private lateinit var mUserSwitcherController: UserSwitcherController
- @Mock private lateinit var mParent: ViewGroup
- @Mock private lateinit var mUserDetailItemView: UserDetailItemView
- @Mock private lateinit var mOtherView: View
- @Mock private lateinit var mInflatedUserDetailItemView: UserDetailItemView
- @Mock private lateinit var mLayoutInflater: LayoutInflater
+ @Mock
+ private lateinit var mUserSwitcherController: UserSwitcherController
+ @Mock
+ private lateinit var mParent: ViewGroup
+ @Mock
+ private lateinit var mUserDetailItemView: UserDetailItemView
+ @Mock
+ private lateinit var mOtherView: View
+ @Mock
+ private lateinit var mInflatedUserDetailItemView: UserDetailItemView
+ @Mock
+ private lateinit var mLayoutInflater: LayoutInflater
private var falsingManagerFake: FalsingManagerFake = FalsingManagerFake()
private lateinit var adapter: UserDetailView.Adapter
private lateinit var uiEventLogger: UiEventLoggerFake
@@ -66,10 +75,12 @@
mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, mLayoutInflater)
`when`(mLayoutInflater.inflate(anyInt(), any(ViewGroup::class.java), anyBoolean()))
- .thenReturn(mInflatedUserDetailItemView)
+ .thenReturn(mInflatedUserDetailItemView)
`when`(mParent.context).thenReturn(mContext)
- adapter = UserDetailView.Adapter(mContext, mUserSwitcherController, uiEventLogger,
- falsingManagerFake)
+ adapter = UserDetailView.Adapter(
+ mContext, mUserSwitcherController, uiEventLogger,
+ falsingManagerFake
+ )
mPicture = UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.ic_avatar_user))
}
@@ -139,6 +150,20 @@
clickableTest(false, false, mUserDetailItemView, true)
}
+ @Test
+ fun testManageUsersIsNotAvailable() {
+ assertNull(adapter.users.find { it.isManageUsers })
+ }
+
+ @Test
+ fun clickDismissDialog() {
+ val shower: UserSwitchDialogController.DialogShower =
+ mock(UserSwitchDialogController.DialogShower::class.java)
+ adapter.injectDialogShower(shower)
+ adapter.onUserListItemClicked(createUserRecord(current = true, guest = false), shower)
+ verify(shower).dismiss()
+ }
+
private fun createUserRecord(current: Boolean, guest: Boolean) =
UserRecord(
UserInfo(0 /* id */, "name", 0 /* flags */),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
index 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/screenshot/ActionIntentCreatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt
new file mode 100644
index 0000000..b6a595b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.screenshot
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+class ActionIntentCreatorTest : SysuiTestCase() {
+
+ @Test
+ fun testCreateShareIntent() {
+ val uri = Uri.parse("content://fake")
+ val subject = "Example subject"
+
+ val output = ActionIntentCreator.createShareIntent(uri, subject)
+
+ assertThat(output.action).isEqualTo(Intent.ACTION_CHOOSER)
+ assertFlagsSet(
+ Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_CLEAR_TASK or
+ Intent.FLAG_GRANT_READ_URI_PERMISSION,
+ output.flags
+ )
+
+ val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
+ assertThat(wrappedIntent?.action).isEqualTo(Intent.ACTION_SEND)
+ assertThat(wrappedIntent?.data).isEqualTo(uri)
+ assertThat(wrappedIntent?.type).isEqualTo("image/png")
+ assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isEqualTo(subject)
+ assertThat(wrappedIntent?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java))
+ .isEqualTo(uri)
+ }
+
+ @Test
+ fun testCreateShareIntent_noSubject() {
+ val uri = Uri.parse("content://fake")
+ val output = ActionIntentCreator.createShareIntent(uri, null)
+ val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
+ assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isNull()
+ }
+
+ @Test
+ fun testCreateEditIntent() {
+ val uri = Uri.parse("content://fake")
+ val context = mock<Context>()
+
+ val output = ActionIntentCreator.createEditIntent(uri, context)
+
+ assertThat(output.action).isEqualTo(Intent.ACTION_EDIT)
+ assertThat(output.data).isEqualTo(uri)
+ assertThat(output.type).isEqualTo("image/png")
+ assertThat(output.component).isNull()
+ val expectedFlags =
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
+ Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_CLEAR_TASK
+ assertFlagsSet(expectedFlags, output.flags)
+ }
+
+ @Test
+ fun testCreateEditIntent_withEditor() {
+ val uri = Uri.parse("content://fake")
+ val context = mock<Context>()
+ var component = ComponentName("com.android.foo", "com.android.foo.Something")
+
+ whenever(context.getString(eq(R.string.config_screenshotEditor)))
+ .thenReturn(component.flattenToString())
+
+ val output = ActionIntentCreator.createEditIntent(uri, context)
+
+ assertThat(output.component).isEqualTo(component)
+ }
+
+ private fun assertFlagsSet(expected: Int, observed: Int) {
+ assertThat(observed and expected).isEqualTo(expected)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DraggableConstraintLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DraggableConstraintLayoutTest.java
new file mode 100644
index 0000000..c6ce51a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DraggableConstraintLayoutTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.screenshot;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.MotionEvent;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+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)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class DraggableConstraintLayoutTest extends SysuiTestCase {
+
+ @Mock
+ DraggableConstraintLayout.SwipeDismissCallbacks mCallbacks;
+
+ private DraggableConstraintLayout mDraggableConstraintLayout;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mDraggableConstraintLayout = new DraggableConstraintLayout(mContext, null, 0);
+ }
+
+ @Test
+ public void test_dismissDoesNotCallSwipeInitiated() {
+ mDraggableConstraintLayout.setCallbacks(mCallbacks);
+
+ mDraggableConstraintLayout.dismiss();
+
+ verify(mCallbacks, never()).onSwipeDismissInitiated(any());
+ }
+
+ @Test
+ public void test_onTouchCallsOnInteraction() {
+ mDraggableConstraintLayout.setCallbacks(mCallbacks);
+
+ mDraggableConstraintLayout.onInterceptTouchEvent(
+ MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0));
+
+ verify(mCallbacks).onInteraction();
+ }
+
+ @Test
+ public void test_callbacksNotSet() {
+ // just test that it doesn't throw an NPE
+ mDraggableConstraintLayout.onInterceptTouchEvent(
+ MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0));
+ mDraggableConstraintLayout.onInterceptHoverEvent(
+ MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0, 0, 0));
+ mDraggableConstraintLayout.dismiss();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
index 5cb27a4..46a502a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
@@ -99,13 +99,14 @@
policy.getDefaultDisplayId(),
DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
- val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+ val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER)
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val processedRequest = processor.process(request)
// Request has topComponent added, but otherwise unchanged.
assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN)
+ assertThat(processedRequest.source).isEqualTo(SCREENSHOT_OTHER)
assertThat(processedRequest.topComponent).isEqualTo(component)
}
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/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
index c76d9e7..14a3bc1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
@@ -645,6 +645,65 @@
verify(animator).start()
}
+ @Test
+ fun privacyChipParentVisibleFromStart() {
+ verify(privacyIconsController).onParentVisible()
+ }
+
+ @Test
+ fun privacyChipParentVisibleAlways() {
+ controller.largeScreenActive = true
+ controller.largeScreenActive = false
+ controller.largeScreenActive = true
+
+ verify(privacyIconsController, never()).onParentInvisible()
+ }
+
+ @Test
+ fun clockPivotYInCenter() {
+ val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
+ verify(clock).addOnLayoutChangeListener(capture(captor))
+ var height = 100
+ val width = 50
+
+ clock.executeLayoutChange(0, 0, width, height, captor.value)
+ verify(clock).pivotY = height.toFloat() / 2
+
+ height = 150
+ clock.executeLayoutChange(0, 0, width, height, captor.value)
+ verify(clock).pivotY = height.toFloat() / 2
+ }
+
+ private fun View.executeLayoutChange(
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ listener: View.OnLayoutChangeListener
+ ) {
+ val oldLeft = this.left
+ val oldTop = this.top
+ val oldRight = this.right
+ val oldBottom = this.bottom
+ whenever(this.left).thenReturn(left)
+ whenever(this.top).thenReturn(top)
+ whenever(this.right).thenReturn(right)
+ whenever(this.bottom).thenReturn(bottom)
+ whenever(this.height).thenReturn(bottom - top)
+ whenever(this.width).thenReturn(right - left)
+ listener.onLayoutChange(
+ this,
+ oldLeft,
+ oldTop,
+ oldRight,
+ oldBottom,
+ left,
+ top,
+ right,
+ bottom
+ )
+ }
+
private fun createWindowInsets(
topCutout: Rect? = Rect()
): WindowInsets {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 0c60d3c..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,14 +18,15 @@
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;
+import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
+import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPENING;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
-import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED;
-import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN;
-import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPENING;
import static com.google.common.truth.Truth.assertThat;
@@ -33,11 +34,13 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -76,6 +79,7 @@
import com.android.internal.logging.testing.UiEventLoggerFake;
import com.android.internal.util.CollectionUtils;
import com.android.internal.util.LatencyTracker;
+import com.android.keyguard.FaceAuthApiRequestReason;
import com.android.keyguard.KeyguardClockSwitch;
import com.android.keyguard.KeyguardClockSwitchController;
import com.android.keyguard.KeyguardStatusView;
@@ -93,7 +97,6 @@
import com.android.systemui.camera.CameraGestureHelper;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.controls.dagger.ControlsComponent;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
@@ -102,14 +105,15 @@
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
-import com.android.systemui.media.KeyguardMediaController;
-import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.MediaHierarchyManager;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.ui.KeyguardMediaController;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
import com.android.systemui.model.SysUiState;
+import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QS;
-import com.android.systemui.qrcodescanner.controller.QRCodeScannerController;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSFragment;
import com.android.systemui.screenrecord.RecordingController;
import com.android.systemui.shade.transition.ShadeTransitionController;
@@ -156,7 +160,6 @@
import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
import com.android.systemui.statusbar.phone.TapAgainViewController;
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -166,7 +169,6 @@
import com.android.systemui.unfold.SysUIUnfoldComponent;
import com.android.systemui.util.time.FakeSystemClock;
import com.android.systemui.util.time.SystemClock;
-import com.android.systemui.wallet.controller.QuickAccessWalletController;
import com.android.wm.shell.animation.FlingAnimationUtils;
import org.junit.After;
@@ -174,6 +176,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
@@ -252,17 +256,15 @@
@Mock private KeyguardMediaController mKeyguardMediaController;
@Mock private PrivacyDotViewController mPrivacyDotViewController;
@Mock private NavigationModeController mNavigationModeController;
+ @Mock private NavigationBarController mNavigationBarController;
@Mock private LargeScreenShadeHeaderController mLargeScreenShadeHeaderController;
@Mock private ContentResolver mContentResolver;
@Mock private TapAgainViewController mTapAgainViewController;
@Mock private KeyguardIndicationController mKeyguardIndicationController;
@Mock private FragmentService mFragmentService;
@Mock private FragmentHostManager mFragmentHostManager;
- @Mock private QuickAccessWalletController mQuickAccessWalletController;
- @Mock private QRCodeScannerController mQrCodeScannerController;
@Mock private NotificationRemoteInputManager mNotificationRemoteInputManager;
@Mock private RecordingController mRecordingController;
- @Mock private ControlsComponent mControlsComponent;
@Mock private LockscreenGestureLogger mLockscreenGestureLogger;
@Mock private DumpManager mDumpManager;
@Mock private InteractionJankMonitor mInteractionJankMonitor;
@@ -283,6 +285,10 @@
@Mock private ViewTreeObserver mViewTreeObserver;
@Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
@Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
+ @Mock private MotionEvent mDownMotionEvent;
+ @Captor
+ private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
+ mEmptySpaceClickListenerCaptor;
private NotificationPanelViewController.TouchHandler mTouchHandler;
private ConfigurationController mConfigurationController;
@@ -297,8 +303,8 @@
private final FalsingManagerFake mFalsingManager = new FalsingManagerFake();
private final Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty();
private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
- private final PanelExpansionStateManager mPanelExpansionStateManager =
- new PanelExpansionStateManager();
+ private final ShadeExpansionStateManager mShadeExpansionStateManager =
+ new ShadeExpansionStateManager();
private FragmentHostManager.FragmentListener mFragmentListener;
@Before
@@ -374,6 +380,7 @@
NotificationWakeUpCoordinator coordinator =
new NotificationWakeUpCoordinator(
+ mDumpManager,
mock(HeadsUpManagerPhone.class),
new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager,
mInteractionJankMonitor),
@@ -389,6 +396,7 @@
mConfigurationController,
mStatusBarStateController,
mFalsingManager,
+ mShadeExpansionStateManager,
mLockscreenShadeTransitionController,
new FalsingCollectorFake(),
mDumpManager);
@@ -426,6 +434,8 @@
when(mView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
when(mView.getParent()).thenReturn(mViewParent);
when(mQs.getHeader()).thenReturn(mQsHeader);
+ when(mDownMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN);
+ when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
mMainHandler = new Handler(Looper.getMainLooper());
NotificationPanelViewController.PanelEventsEmitter panelEventsEmitter =
@@ -469,13 +479,14 @@
mPrivacyDotViewController,
mTapAgainViewController,
mNavigationModeController,
+ mNavigationBarController,
mFragmentService,
mContentResolver,
mRecordingController,
mLargeScreenShadeHeaderController,
mScreenOffAnimationController,
mLockscreenGestureLogger,
- mPanelExpansionStateManager,
+ mShadeExpansionStateManager,
mNotificationRemoteInputManager,
mSysUIUnfoldComponent,
mInteractionJankMonitor,
@@ -513,6 +524,8 @@
.addCallback(mNotificationPanelViewController.mStatusBarStateListener);
mNotificationPanelViewController
.setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
+ verify(mNotificationStackScrollLayoutController)
+ .setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture());
}
@After
@@ -717,6 +730,72 @@
}
@Test
+ public void test_pulsing_onTouchEvent_noTracking() {
+ // GIVEN device is pulsing
+ mNotificationPanelViewController.setPulsing(true);
+
+ // WHEN touch DOWN & MOVE events received
+ 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 */, 200f /* y */,
+ 0 /* metaState */));
+
+ // THEN touch is NOT tracked (since the device is pulsing)
+ assertThat(mNotificationPanelViewController.isTracking()).isFalse();
+ }
+
+ @Test
+ public void test_onTouchEvent_startTracking() {
+ // GIVEN device is NOT pulsing
+ mNotificationPanelViewController.setPulsing(false);
+
+ // WHEN touch DOWN & MOVE events received
+ 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 */, 200f /* y */,
+ 0 /* metaState */));
+
+ // THEN touch is tracked
+ assertThat(mNotificationPanelViewController.isTracking()).isTrue();
+ }
+
+ @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);
@@ -1252,10 +1331,10 @@
@Test
public void testQsToBeImmediatelyExpandedWhenOpeningPanelInSplitShade() {
enableSplitShade(/* enabled= */ true);
- mPanelExpansionStateManager.updateState(STATE_CLOSED);
+ mShadeExpansionStateManager.updateState(STATE_CLOSED);
assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
- mPanelExpansionStateManager.updateState(STATE_OPENING);
+ mShadeExpansionStateManager.updateState(STATE_OPENING);
assertThat(mNotificationPanelViewController.mQsExpandImmediate).isTrue();
}
@@ -1263,11 +1342,11 @@
@Test
public void testQsNotToBeImmediatelyExpandedWhenGoingFromUnlockedToLocked() {
enableSplitShade(/* enabled= */ true);
- mPanelExpansionStateManager.updateState(STATE_CLOSED);
+ mShadeExpansionStateManager.updateState(STATE_CLOSED);
mStatusBarStateController.setState(KEYGUARD);
// going to lockscreen would trigger STATE_OPENING
- mPanelExpansionStateManager.updateState(STATE_OPENING);
+ mShadeExpansionStateManager.updateState(STATE_OPENING);
assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
}
@@ -1275,11 +1354,11 @@
@Test
public void testQsImmediateResetsWhenPanelOpensOrCloses() {
mNotificationPanelViewController.mQsExpandImmediate = true;
- mPanelExpansionStateManager.updateState(STATE_OPEN);
+ mShadeExpansionStateManager.updateState(STATE_OPEN);
assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
mNotificationPanelViewController.mQsExpandImmediate = true;
- mPanelExpansionStateManager.updateState(STATE_CLOSED);
+ mShadeExpansionStateManager.updateState(STATE_CLOSED);
assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
}
@@ -1300,7 +1379,7 @@
@Test
public void testPanelClosedWhenClosingQsInSplitShade() {
- mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
+ mShadeExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
/* expanded= */ true, /* tracking= */ false, /* dragDownPxAmount= */ 0);
enableSplitShade(/* enabled= */ true);
mNotificationPanelViewController.setExpandedFraction(1f);
@@ -1312,7 +1391,7 @@
@Test
public void testPanelStaysOpenWhenClosingQs() {
- mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
+ mShadeExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
/* expanded= */ true, /* tracking= */ false, /* dragDownPxAmount= */ 0);
mNotificationPanelViewController.setExpandedFraction(1f);
@@ -1507,6 +1586,103 @@
);
}
+ @Test
+ public void onEmptySpaceClicked_notDozingAndOnKeyguard_requestsFaceAuth() {
+ StatusBarStateController.StateListener statusBarStateListener =
+ mNotificationPanelViewController.mStatusBarStateListener;
+ statusBarStateListener.onStateChanged(KEYGUARD);
+ mNotificationPanelViewController.setDozing(false, false);
+
+ // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+ mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+ mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+ verify(mUpdateMonitor).requestFaceAuth(true,
+ FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
+ }
+
+ @Test
+ public void onEmptySpaceClicked_notDozingAndFaceDetectionIsNotRunning_startsUnlockAnimation() {
+ StatusBarStateController.StateListener statusBarStateListener =
+ mNotificationPanelViewController.mStatusBarStateListener;
+ statusBarStateListener.onStateChanged(KEYGUARD);
+ mNotificationPanelViewController.setDozing(false, false);
+ when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(false);
+
+ // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+ mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+ mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+ verify(mNotificationStackScrollLayoutController).setUnlockHintRunning(true);
+ }
+
+ @Test
+ public void onEmptySpaceClicked_notDozingAndFaceDetectionIsRunning_doesNotStartUnlockHint() {
+ StatusBarStateController.StateListener statusBarStateListener =
+ mNotificationPanelViewController.mStatusBarStateListener;
+ statusBarStateListener.onStateChanged(KEYGUARD);
+ mNotificationPanelViewController.setDozing(false, false);
+ when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(true);
+
+ // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+ mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+ mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+ verify(mNotificationStackScrollLayoutController, never()).setUnlockHintRunning(true);
+ }
+
+ @Test
+ public void onEmptySpaceClicked_whenDozingAndOnKeyguard_doesNotRequestFaceAuth() {
+ StatusBarStateController.StateListener statusBarStateListener =
+ mNotificationPanelViewController.mStatusBarStateListener;
+ statusBarStateListener.onStateChanged(KEYGUARD);
+ mNotificationPanelViewController.setDozing(true, false);
+
+ // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+ mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+ mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+ verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString());
+ }
+
+ @Test
+ public void onEmptySpaceClicked_whenStatusBarShadeLocked_doesNotRequestFaceAuth() {
+ StatusBarStateController.StateListener statusBarStateListener =
+ mNotificationPanelViewController.mStatusBarStateListener;
+ statusBarStateListener.onStateChanged(SHADE_LOCKED);
+
+ mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+ verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString());
+
+ }
+
+ /**
+ * When shade is flinging to close and this fling is not intercepted,
+ * {@link AmbientState#setIsClosing(boolean)} should be called before
+ * {@link NotificationStackScrollLayoutController#onExpansionStopped()}
+ * to ensure scrollY can be correctly set to be 0
+ */
+ @Test
+ public void onShadeFlingClosingEnd_mAmbientStateSetClose_thenOnExpansionStopped() {
+ // Given: Shade is expanded
+ mNotificationPanelViewController.notifyExpandingFinished();
+ mNotificationPanelViewController.setIsClosing(false);
+
+ // When: Shade flings to close not canceled
+ mNotificationPanelViewController.notifyExpandingStarted();
+ mNotificationPanelViewController.setIsClosing(true);
+ mNotificationPanelViewController.onFlingEnd(false);
+
+ // Then: AmbientState's mIsClosing should be set to false
+ // before mNotificationStackScrollLayoutController.onExpansionStopped() is called
+ // to ensure NotificationStackScrollLayout.resetScrollPosition() -> resetScrollPosition
+ // -> setOwnScrollY(0) can set scrollY to 0 when shade is closed
+ InOrder inOrder = inOrder(mAmbientState, mNotificationStackScrollLayoutController);
+ inOrder.verify(mAmbientState).setIsClosing(false);
+ inOrder.verify(mNotificationStackScrollLayoutController).onExpansionStopped();
+ }
+
private static MotionEvent createMotionEvent(int x, int y, int action) {
return MotionEvent.obtain(
/* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
index 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/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 481e4e9..db7e017 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -41,7 +41,6 @@
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
import com.android.systemui.statusbar.window.StatusBarWindowStateController
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -117,7 +116,7 @@
notificationShadeDepthController,
view,
notificationPanelViewController,
- PanelExpansionStateManager(),
+ ShadeExpansionStateManager(),
stackScrollLayoutController,
statusBarKeyguardViewManager,
statusBarWindowStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
index 4a7dec9..26a0770 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
@@ -51,7 +51,6 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
import com.android.systemui.tuner.TunerService;
@@ -118,7 +117,7 @@
mNotificationShadeDepthController,
mView,
mNotificationPanelViewController,
- new PanelExpansionStateManager(),
+ new ShadeExpansionStateManager(),
mNotificationStackScrollLayoutController,
mStatusBarKeyguardViewManager,
mStatusBarWindowStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeExpansionStateManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeExpansionStateManagerTest.kt
new file mode 100644
index 0000000..a601b67
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeExpansionStateManagerTest.kt
@@ -0,0 +1,249 @@
+/*
+ * 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.shade
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class ShadeExpansionStateManagerTest : SysuiTestCase() {
+
+ private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
+
+ @Before
+ fun setUp() {
+ shadeExpansionStateManager = ShadeExpansionStateManager()
+ }
+
+ @Test
+ fun onPanelExpansionChanged_listenerNotified() {
+ val listener = TestShadeExpansionListener()
+ shadeExpansionStateManager.addExpansionListener(listener)
+ val fraction = 0.6f
+ val expanded = true
+ val tracking = true
+ val dragDownAmount = 1234f
+
+ shadeExpansionStateManager.onPanelExpansionChanged(
+ fraction,
+ expanded,
+ tracking,
+ dragDownAmount
+ )
+
+ assertThat(listener.fraction).isEqualTo(fraction)
+ assertThat(listener.expanded).isEqualTo(expanded)
+ assertThat(listener.tracking).isEqualTo(tracking)
+ assertThat(listener.dragDownAmountPx).isEqualTo(dragDownAmount)
+ }
+
+ @Test
+ fun addExpansionListener_listenerNotifiedOfCurrentValues() {
+ val fraction = 0.6f
+ val expanded = true
+ val tracking = true
+ val dragDownAmount = 1234f
+ shadeExpansionStateManager.onPanelExpansionChanged(
+ fraction,
+ expanded,
+ tracking,
+ dragDownAmount
+ )
+ val listener = TestShadeExpansionListener()
+
+ shadeExpansionStateManager.addExpansionListener(listener)
+
+ assertThat(listener.fraction).isEqualTo(fraction)
+ assertThat(listener.expanded).isEqualTo(expanded)
+ assertThat(listener.tracking).isEqualTo(tracking)
+ assertThat(listener.dragDownAmountPx).isEqualTo(dragDownAmount)
+ }
+
+ @Test
+ fun updateState_listenerNotified() {
+ val listener = TestShadeStateListener()
+ shadeExpansionStateManager.addStateListener(listener)
+
+ shadeExpansionStateManager.updateState(STATE_OPEN)
+
+ assertThat(listener.state).isEqualTo(STATE_OPEN)
+ }
+
+ /* ***** [PanelExpansionStateManager.onPanelExpansionChanged] test cases *******/
+
+ /* Fraction < 1 test cases */
+
+ @Test
+ fun onPEC_fractionLessThanOne_expandedTrue_trackingFalse_becomesStateOpening() {
+ val listener = TestShadeStateListener()
+ shadeExpansionStateManager.addStateListener(listener)
+
+ shadeExpansionStateManager.onPanelExpansionChanged(
+ fraction = 0.5f,
+ expanded = true,
+ tracking = false,
+ dragDownPxAmount = 0f
+ )
+
+ assertThat(listener.state).isEqualTo(STATE_OPENING)
+ }
+
+ @Test
+ fun onPEC_fractionLessThanOne_expandedTrue_trackingTrue_becomesStateOpening() {
+ val listener = TestShadeStateListener()
+ shadeExpansionStateManager.addStateListener(listener)
+
+ shadeExpansionStateManager.onPanelExpansionChanged(
+ fraction = 0.5f,
+ expanded = true,
+ tracking = true,
+ dragDownPxAmount = 0f
+ )
+
+ assertThat(listener.state).isEqualTo(STATE_OPENING)
+ }
+
+ @Test
+ fun onPEC_fractionLessThanOne_expandedFalse_trackingFalse_becomesStateClosed() {
+ val listener = TestShadeStateListener()
+ shadeExpansionStateManager.addStateListener(listener)
+ // Start out on a different state
+ shadeExpansionStateManager.updateState(STATE_OPEN)
+
+ shadeExpansionStateManager.onPanelExpansionChanged(
+ fraction = 0.5f,
+ expanded = false,
+ tracking = false,
+ dragDownPxAmount = 0f
+ )
+
+ assertThat(listener.state).isEqualTo(STATE_CLOSED)
+ }
+
+ @Test
+ fun onPEC_fractionLessThanOne_expandedFalse_trackingTrue_doesNotBecomeStateClosed() {
+ val listener = TestShadeStateListener()
+ shadeExpansionStateManager.addStateListener(listener)
+ // Start out on a different state
+ shadeExpansionStateManager.updateState(STATE_OPEN)
+
+ shadeExpansionStateManager.onPanelExpansionChanged(
+ fraction = 0.5f,
+ expanded = false,
+ tracking = true,
+ dragDownPxAmount = 0f
+ )
+
+ assertThat(listener.state).isEqualTo(STATE_OPEN)
+ }
+
+ /* Fraction = 1 test cases */
+
+ @Test
+ fun onPEC_fractionOne_expandedTrue_trackingFalse_becomesStateOpeningThenStateOpen() {
+ val listener = TestShadeStateListener()
+ shadeExpansionStateManager.addStateListener(listener)
+
+ shadeExpansionStateManager.onPanelExpansionChanged(
+ fraction = 1f,
+ expanded = true,
+ tracking = false,
+ dragDownPxAmount = 0f
+ )
+
+ assertThat(listener.previousState).isEqualTo(STATE_OPENING)
+ assertThat(listener.state).isEqualTo(STATE_OPEN)
+ }
+
+ @Test
+ fun onPEC_fractionOne_expandedTrue_trackingTrue_becomesStateOpening() {
+ val listener = TestShadeStateListener()
+ shadeExpansionStateManager.addStateListener(listener)
+
+ shadeExpansionStateManager.onPanelExpansionChanged(
+ fraction = 1f,
+ expanded = true,
+ tracking = true,
+ dragDownPxAmount = 0f
+ )
+
+ assertThat(listener.state).isEqualTo(STATE_OPENING)
+ }
+
+ @Test
+ fun onPEC_fractionOne_expandedFalse_trackingFalse_becomesStateClosed() {
+ val listener = TestShadeStateListener()
+ shadeExpansionStateManager.addStateListener(listener)
+ // Start out on a different state
+ shadeExpansionStateManager.updateState(STATE_OPEN)
+
+ shadeExpansionStateManager.onPanelExpansionChanged(
+ fraction = 1f,
+ expanded = false,
+ tracking = false,
+ dragDownPxAmount = 0f
+ )
+
+ assertThat(listener.state).isEqualTo(STATE_CLOSED)
+ }
+
+ @Test
+ fun onPEC_fractionOne_expandedFalse_trackingTrue_doesNotBecomeStateClosed() {
+ val listener = TestShadeStateListener()
+ shadeExpansionStateManager.addStateListener(listener)
+ // Start out on a different state
+ shadeExpansionStateManager.updateState(STATE_OPEN)
+
+ shadeExpansionStateManager.onPanelExpansionChanged(
+ fraction = 1f,
+ expanded = false,
+ tracking = true,
+ dragDownPxAmount = 0f
+ )
+
+ assertThat(listener.state).isEqualTo(STATE_OPEN)
+ }
+
+ /* ***** end [PanelExpansionStateManager.onPanelExpansionChanged] test cases ******/
+
+ class TestShadeExpansionListener : ShadeExpansionListener {
+ var fraction: Float = 0f
+ var expanded: Boolean = false
+ var tracking: Boolean = false
+ var dragDownAmountPx: Float = 0f
+
+ override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
+ this.fraction = event.fraction
+ this.expanded = event.expanded
+ this.tracking = event.tracking
+ this.dragDownAmountPx = event.dragDownPxAmount
+ }
+ }
+
+ class TestShadeStateListener : ShadeStateListener {
+ @PanelState var previousState: Int = STATE_CLOSED
+ @PanelState var state: Int = STATE_CLOSED
+
+ override fun onPanelStateChanged(state: Int) {
+ this.previousState = this.state
+ this.state = state
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt
index 6be76a6..84f8656 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt
@@ -5,13 +5,13 @@
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
+import com.android.systemui.shade.STATE_CLOSED
+import com.android.systemui.shade.STATE_OPEN
+import com.android.systemui.shade.STATE_OPENING
+import com.android.systemui.shade.ShadeExpansionChangeEvent
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
-import com.android.systemui.statusbar.phone.panelstate.STATE_CLOSED
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPEN
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING
import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.statusbar.policy.HeadsUpManager
import org.junit.Before
@@ -148,7 +148,7 @@
companion object {
val EXPANSION_EVENT =
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 0.5f, expanded = true, tracking = true, dragDownPxAmount = 10f)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
index b6f8326..7cac854 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
@@ -7,12 +7,12 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.qs.QS
import com.android.systemui.shade.NotificationPanelViewController
+import com.android.systemui.shade.STATE_OPENING
+import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionStateManager
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING
import com.android.systemui.statusbar.policy.FakeConfigurationController
import org.junit.Before
import org.junit.Test
@@ -40,7 +40,7 @@
private lateinit var controller: ShadeTransitionController
private val configurationController = FakeConfigurationController()
- private val panelExpansionStateManager = PanelExpansionStateManager()
+ private val shadeExpansionStateManager = ShadeExpansionStateManager()
@Before
fun setUp() {
@@ -49,7 +49,7 @@
controller =
ShadeTransitionController(
configurationController,
- panelExpansionStateManager,
+ shadeExpansionStateManager,
dumpManager,
context,
splitShadeOverScrollerFactory = { _, _ -> splitShadeOverScroller },
@@ -166,7 +166,7 @@
}
private fun startPanelExpansion() {
- panelExpansionStateManager.onPanelExpansionChanged(
+ shadeExpansionStateManager.onPanelExpansionChanged(
DEFAULT_EXPANSION_EVENT.fraction,
DEFAULT_EXPANSION_EVENT.expanded,
DEFAULT_EXPANSION_EVENT.tracking,
@@ -194,7 +194,7 @@
companion object {
private const val DEFAULT_DRAG_DOWN_AMOUNT = 123f
private val DEFAULT_EXPANSION_EVENT =
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 0.5f,
expanded = true,
tracking = true,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt
index aafd871..0e48b48 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt
@@ -7,11 +7,11 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.qs.QS
+import com.android.systemui.shade.STATE_CLOSED
+import com.android.systemui.shade.STATE_OPEN
+import com.android.systemui.shade.STATE_OPENING
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.phone.panelstate.STATE_CLOSED
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPEN
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING
import com.android.systemui.statusbar.policy.FakeConfigurationController
import org.junit.Before
import org.junit.Test
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/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index ffb41e5..70cbc64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Handler
+import android.os.UserHandle
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -104,13 +105,14 @@
mockContext,
mockPluginManager,
mockHandler,
- fakeDefaultProvider
+ isEnabled = true,
+ userHandle = UserHandle.USER_ALL,
+ defaultClockProvider = fakeDefaultProvider
) {
override var currentClockId: ClockId
get() = settingValue
set(value) { settingValue = value }
}
- registry.isEnabled = true
verify(mockPluginManager)
.addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java), eq(true))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
index 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/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
index f9e279e..b761647 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
@@ -3,26 +3,25 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
+import java.lang.Thread.UncaughtExceptionHandler
import org.junit.Assert.assertThrows
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.mockito.Mock
-import org.mockito.Mockito.only
import org.mockito.Mockito.any
+import org.mockito.Mockito.only
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
-import java.lang.Thread.UncaughtExceptionHandler
@SmallTest
class UncaughtExceptionPreHandlerTest : SysuiTestCase() {
private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager
- @Mock
- private lateinit var mockHandler: UncaughtExceptionHandler
+ @Mock private lateinit var mockHandler: UncaughtExceptionHandler
- @Mock
- private lateinit var mockHandler2: UncaughtExceptionHandler
+ @Mock private lateinit var mockHandler2: UncaughtExceptionHandler
@Before
fun setUp() {
@@ -55,6 +54,7 @@
}
@Test
+ @Ignore
fun registerHandler_toleratesHandlersThatThrow() {
`when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException())
preHandlerManager.registerHandler(mockHandler2)
@@ -71,4 +71,4 @@
preHandlerManager.registerHandler(mockHandler)
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
index 8cb530c..5fc0ffe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
@@ -4,7 +4,7 @@
import android.util.DisplayMetrics
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.phone.LSShadeTransitionLogger
import com.android.systemui.statusbar.phone.LockscreenGestureLogger
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index 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/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index 6446fb5..77b1e37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -28,10 +28,10 @@
import com.android.systemui.animation.ShadeInterpolation
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionChangeEvent
import com.android.systemui.statusbar.phone.BiometricUnlockController
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.WallpaperController
@@ -137,7 +137,7 @@
@Test
fun onPanelExpansionChanged_apliesBlur_ifShade() {
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
verify(shadeAnimation).animateTo(eq(maxBlur))
}
@@ -145,7 +145,7 @@
@Test
fun onPanelExpansionChanged_animatesBlurIn_ifShade() {
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 0.01f, expanded = false, tracking = false, dragDownPxAmount = 0f))
verify(shadeAnimation).animateTo(eq(maxBlur))
}
@@ -155,7 +155,7 @@
onPanelExpansionChanged_animatesBlurIn_ifShade()
clearInvocations(shadeAnimation)
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 0f, expanded = false, tracking = false, dragDownPxAmount = 0f))
verify(shadeAnimation).animateTo(eq(0))
}
@@ -163,7 +163,7 @@
@Test
fun onPanelExpansionChanged_animatesBlurOut_ifFlick() {
val event =
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)
onPanelExpansionChanged_apliesBlur_ifShade()
clearInvocations(shadeAnimation)
@@ -184,7 +184,7 @@
onPanelExpansionChanged_animatesBlurOut_ifFlick()
clearInvocations(shadeAnimation)
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 0.6f, expanded = true, tracking = true, dragDownPxAmount = 0f))
verify(shadeAnimation).animateTo(eq(maxBlur))
}
@@ -192,7 +192,7 @@
@Test
fun onPanelExpansionChanged_respectsMinPanelPullDownFraction() {
val event =
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 0.5f, expanded = true, tracking = true, dragDownPxAmount = 0f)
notificationShadeDepthController.panelPullDownMinFraction = 0.5f
notificationShadeDepthController.onPanelExpansionChanged(event)
@@ -220,7 +220,7 @@
statusBarState = StatusBarState.KEYGUARD
notificationShadeDepthController.qsPanelExpansion = 1f
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
notificationShadeDepthController.updateBlurCallback.doFrame(0)
verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false))
@@ -231,7 +231,7 @@
statusBarState = StatusBarState.KEYGUARD
notificationShadeDepthController.qsPanelExpansion = 0.25f
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
notificationShadeDepthController.updateBlurCallback.doFrame(0)
verify(wallpaperController)
@@ -243,7 +243,7 @@
enableSplitShade()
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
notificationShadeDepthController.updateBlurCallback.doFrame(0)
@@ -255,7 +255,7 @@
disableSplitShade()
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
notificationShadeDepthController.updateBlurCallback.doFrame(0)
@@ -269,7 +269,7 @@
val expanded = true
val tracking = false
val dragDownPxAmount = 0f
- val event = PanelExpansionChangeEvent(rawFraction, expanded, tracking, dragDownPxAmount)
+ val event = ShadeExpansionChangeEvent(rawFraction, expanded, tracking, dragDownPxAmount)
val inOrder = Mockito.inOrder(wallpaperController)
notificationShadeDepthController.onPanelExpansionChanged(event)
@@ -333,7 +333,7 @@
@Test
fun updateBlurCallback_setsBlur_whenExpanded() {
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
`when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
notificationShadeDepthController.updateBlurCallback.doFrame(0)
@@ -343,7 +343,7 @@
@Test
fun updateBlurCallback_ignoreShadeBlurUntilHidden_overridesZoom() {
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
`when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
notificationShadeDepthController.blursDisabledForAppLaunch = true
@@ -361,7 +361,7 @@
@Test
fun ignoreBlurForUnlock_ignores() {
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
`when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
@@ -378,7 +378,7 @@
@Test
fun ignoreBlurForUnlock_doesNotIgnore() {
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
`when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
@@ -410,7 +410,7 @@
`when`(brightnessSpring.ratio).thenReturn(1f)
// And shade is blurred
notificationShadeDepthController.onPanelExpansionChanged(
- PanelExpansionChangeEvent(
+ ShadeExpansionChangeEvent(
fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
`when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
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/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
index f8a0d2f..9c65fac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
@@ -70,7 +70,7 @@
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
import com.android.systemui.telephony.TelephonyListenerManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
index ed8a3e1..4bed4a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
@@ -38,7 +38,7 @@
import com.android.settingslib.mobile.TelephonyIcons;
import com.android.settingslib.net.DataUsageController;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.util.CarrierConfigTracker;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
index a76676e..d5f5105 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
@@ -43,7 +43,7 @@
import com.android.settingslib.net.DataUsageController;
import com.android.systemui.R;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.util.CarrierConfigTracker;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
index b719c7f..a6381d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
@@ -32,7 +32,6 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import org.junit.Assert;
@@ -58,8 +57,6 @@
mDynamicPrivacyController = new DynamicPrivacyController(
mLockScreenUserManager, mKeyguardStateController,
mock(StatusBarStateController.class));
- mDynamicPrivacyController.setStatusBarKeyguardViewManager(
- mock(StatusBarKeyguardViewManager.class));
mDynamicPrivacyController.addListener(mListener);
// Disable dynamic privacy by default
allowNotificationsInPublic(false);
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/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 82e32b2..09f8a10 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -34,10 +34,12 @@
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
@@ -135,6 +137,7 @@
public void setUp() {
MockitoAnnotations.initMocks(this);
allowTestableLooperAsMainThread();
+ when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
mListBuilder = new ShadeListBuilder(
mDumpManager,
@@ -1995,22 +1998,89 @@
}
@Test
- public void testStableOrdering() {
+ public void testActiveOrdering_withLegacyStability() {
+ when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
+ assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
+ assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
+ assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
+ assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
+ assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
+ }
+
+ @Test
+ public void testStableOrdering_withLegacyStability() {
+ when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
mStabilityManager.setAllowEntryReordering(false);
- assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG"); // X
- assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG"); // no change
- assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG"); // Z and X
- assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG"); // Z and X + gap
- verify(mStabilityManager, times(4)).onEntryReorderSuppressed();
+ assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
+ assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG", false); // X
+ assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // no change
+ assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG", false); // Z and X
+ assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG", false); // Z and X + gap
+ }
+
+ @Test
+ public void testStableOrdering() {
+ when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+ mStabilityManager.setAllowEntryReordering(false);
+ // No input or output
+ assertOrder("", "", "", true);
+ // Remove everything
+ assertOrder("ABCDEFG", "", "", true);
+ // Literally no changes
+ assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true);
+
+ // No stable order
+ assertOrder("", "ABCDEFG", "ABCDEFG", true);
+
+ // F moved after A, and...
+ assertOrder("ABCDEFG", "AFBCDEG", "ABCDEFG", false); // No other changes
+ assertOrder("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false); // Insert X before F
+ assertOrder("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false); // Insert X after F
+ assertOrder("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false); // Insert X where F was
+
+ // B moved after F, and...
+ assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // No other changes
+ assertOrder("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false); // Insert X before B
+ assertOrder("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false); // Insert X after B
+ assertOrder("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false); // Insert X where B was
+
+ // Swap F and B, and...
+ assertOrder("ABCDEFG", "AFCDEBG", "ABCDEFG", false); // No other changes
+ assertOrder("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false); // Insert X before F
+ assertOrder("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false); // Insert X after F
+ assertOrder("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false); // Insert X between CD (or: ABCXDEFG)
+ assertOrder("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false); // Insert X between DE (or: ABCDEFXG)
+ assertOrder("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false); // Insert X before B
+ assertOrder("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false); // Insert X after B
+
+ // Remove a bunch of entries at once
+ assertOrder("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true);
+
+ // Remove a bunch of entries and scramble
+ assertOrder("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false);
+
+ // Add a bunch of entries at once
+ assertOrder("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true);
+
+ // Add a bunch of entries and reverse originals
+ // NOTE: Some of these don't have obviously correct answers
+ assertOrder("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false); // appended
+ assertOrder("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false); // prepended
+ assertOrder("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false); // closer to back: append
+ assertOrder("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false); // closer to front: prepend
+ assertOrder("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false); // split new entries
+
+ // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
+ assertOrder("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false);
}
@Test
public void testActiveOrdering() {
- assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG"); // X
- assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG"); // no change
- assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG"); // Z and X
- assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG"); // Z and X + gap
- verify(mStabilityManager, never()).onEntryReorderSuppressed();
+ when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+ assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
+ assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
+ assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
+ assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
}
@Test
@@ -2062,6 +2132,52 @@
}
@Test
+ public void stableOrderingDisregardedWithSectionChange() {
+ when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+ // GIVEN the first sectioner's packages can be changed from run-to-run
+ List<String> mutableSectionerPackages = new ArrayList<>();
+ mutableSectionerPackages.add(PACKAGE_1);
+ mListBuilder.setSectioners(asList(
+ new PackageSectioner(mutableSectionerPackages, null),
+ new PackageSectioner(List.of(PACKAGE_1, PACKAGE_2, PACKAGE_3), null)));
+ mStabilityManager.setAllowEntryReordering(false);
+
+ // WHEN the list is originally built with reordering disabled (and section changes allowed)
+ addNotif(0, PACKAGE_1).setRank(4);
+ addNotif(1, PACKAGE_1).setRank(5);
+ addNotif(2, PACKAGE_2).setRank(1);
+ addNotif(3, PACKAGE_2).setRank(2);
+ addNotif(4, PACKAGE_3).setRank(3);
+ dispatchBuild();
+
+ // VERIFY the order and that entry reordering has not been suppressed
+ verifyBuiltList(
+ notif(0),
+ notif(1),
+ notif(2),
+ notif(3),
+ notif(4)
+ );
+ verify(mStabilityManager, never()).onEntryReorderSuppressed();
+
+ // WHEN the first section now claims PACKAGE_3 notifications
+ mutableSectionerPackages.add(PACKAGE_3);
+ dispatchBuild();
+
+ // VERIFY the re-sectioned notification is inserted at #1 of the first section, which
+ // is the correct position based on its rank, rather than #3 in the new section simply
+ // because it was #3 in its previous section.
+ verifyBuiltList(
+ notif(4),
+ notif(0),
+ notif(1),
+ notif(2),
+ notif(3)
+ );
+ verify(mStabilityManager, never()).onEntryReorderSuppressed();
+ }
+
+ @Test
public void testStableChildOrdering() {
// WHEN the list is originally built with reordering disabled
mStabilityManager.setAllowEntryReordering(false);
@@ -2112,6 +2228,85 @@
);
}
+ @Test
+ public void groupRevertingToSummaryDoesNotRetainStablePositionWithLegacyIndexLogic() {
+ when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(false);
+
+ // GIVEN a notification group is on screen
+ mStabilityManager.setAllowEntryReordering(false);
+
+ // WHEN the list is originally built with reordering disabled (and section changes allowed)
+ addNotif(0, PACKAGE_1).setRank(2);
+ addNotif(1, PACKAGE_1).setRank(3);
+ addGroupSummary(2, PACKAGE_1, "group").setRank(4);
+ addGroupChild(3, PACKAGE_1, "group").setRank(5);
+ addGroupChild(4, PACKAGE_1, "group").setRank(6);
+ dispatchBuild();
+
+ verifyBuiltList(
+ notif(0),
+ notif(1),
+ group(
+ summary(2),
+ child(3),
+ child(4)
+ )
+ );
+
+ // WHEN the notification summary rank increases and children removed
+ setNewRank(notif(2).entry, 1);
+ mEntrySet.remove(4);
+ mEntrySet.remove(3);
+ dispatchBuild();
+
+ // VERIFY the summary (incorrectly) moves to the top of the section where it is ranked,
+ // despite visual stability being active
+ verifyBuiltList(
+ notif(2),
+ notif(0),
+ notif(1)
+ );
+ }
+
+ @Test
+ public void groupRevertingToSummaryRetainsStablePosition() {
+ when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
+
+ // GIVEN a notification group is on screen
+ mStabilityManager.setAllowEntryReordering(false);
+
+ // WHEN the list is originally built with reordering disabled (and section changes allowed)
+ addNotif(0, PACKAGE_1).setRank(2);
+ addNotif(1, PACKAGE_1).setRank(3);
+ addGroupSummary(2, PACKAGE_1, "group").setRank(4);
+ addGroupChild(3, PACKAGE_1, "group").setRank(5);
+ addGroupChild(4, PACKAGE_1, "group").setRank(6);
+ dispatchBuild();
+
+ verifyBuiltList(
+ notif(0),
+ notif(1),
+ group(
+ summary(2),
+ child(3),
+ child(4)
+ )
+ );
+
+ // WHEN the notification summary rank increases and children removed
+ setNewRank(notif(2).entry, 1);
+ mEntrySet.remove(4);
+ mEntrySet.remove(3);
+ dispatchBuild();
+
+ // VERIFY the summary stays in the same location on rebuild
+ verifyBuiltList(
+ notif(0),
+ notif(1),
+ notif(2)
+ );
+ }
+
private static void setNewRank(NotificationEntry entry, int rank) {
entry.setRanking(new RankingBuilder(entry.getRanking()).setRank(rank).build());
}
@@ -2255,26 +2450,35 @@
return addGroupChildWithTag(index, packageId, groupId, null);
}
- private void assertOrder(String visible, String active, String expected) {
+ private void assertOrder(String visible, String active, String expected,
+ boolean isOrderedCorrectly) {
StringBuilder differenceSb = new StringBuilder();
+ NotifSection section = new NotifSection(mock(NotifSectioner.class), 0);
for (char c : active.toCharArray()) {
if (visible.indexOf(c) < 0) differenceSb.append(c);
}
String difference = differenceSb.toString();
+ int globalIndex = 0;
for (int i = 0; i < visible.length(); i++) {
- addNotif(i, String.valueOf(visible.charAt(i)))
- .setRank(active.indexOf(visible.charAt(i)))
+ final char c = visible.charAt(i);
+ // Skip notifications which aren't active anymore
+ if (!active.contains(String.valueOf(c))) continue;
+ addNotif(globalIndex++, String.valueOf(c))
+ .setRank(active.indexOf(c))
+ .setSection(section)
.setStableIndex(i);
-
}
- for (int i = 0; i < difference.length(); i++) {
- addNotif(i + visible.length(), String.valueOf(difference.charAt(i)))
- .setRank(active.indexOf(difference.charAt(i)))
+ for (char c : difference.toCharArray()) {
+ addNotif(globalIndex++, String.valueOf(c))
+ .setRank(active.indexOf(c))
+ .setSection(section)
.setStableIndex(-1);
}
+ clearInvocations(mStabilityManager);
+
dispatchBuild();
StringBuilder resultSb = new StringBuilder();
for (int i = 0; i < expected.length(); i++) {
@@ -2284,6 +2488,9 @@
assertEquals("visible [" + visible + "] active [" + active + "]",
expected, resultSb.toString());
mEntrySet.clear();
+
+ verify(mStabilityManager, isOrderedCorrectly ? never() : times(1))
+ .onEntryReorderSuppressed();
}
private int nextId(String packageName) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 2970807..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
@@ -47,6 +47,8 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.FakeSystemClock
+import java.util.ArrayList
+import java.util.function.Consumer
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -57,10 +59,8 @@
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.util.ArrayList
-import java.util.function.Consumer
import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -671,8 +671,88 @@
verify(mHeadsUpManager, never()).showNotification(mGroupChild2)
}
+ @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))
+
+ // WHEN a ranking applied update occurs
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN the notification is shown
+ finishBind(mEntry)
+ verify(mHeadsUpManager).showNotification(mEntry)
+ }
+
+ @Test
+ fun testOnRankingApplied_alreadyAlertedEntryShouldNotAlertAgain() {
+ // 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))
+
+ // WHEN a ranking applied update occurs
+ 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())
+ }
+
+ @Test
+ fun testOnRankingApplied_entryUpdatedToHun() {
+ // GIVEN that mEntry is added in a state where it should not HUN
+ setShouldHeadsUp(mEntry, false)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // and it is then updated such that it should now HUN
+ setShouldHeadsUp(mEntry)
+ whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+
+ // WHEN a ranking applied update occurs
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN the notification is shown
+ finishBind(mEntry)
+ 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()))
+ .thenReturn(should)
}
private fun finishBind(entry: NotificationEntry) {
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/collection/listbuilder/SemiStableSortTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt
new file mode 100644
index 0000000..1cdd023
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.util.Log
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class SemiStableSortTest : SysuiTestCase() {
+
+ var shuffleInput: Boolean = false
+ var testStabilizeTo: Boolean = false
+ var sorter: SemiStableSort? = null
+
+ @Before
+ fun setUp() {
+ shuffleInput = false
+ sorter = null
+ }
+
+ private fun stringStabilizeTo(
+ stableOrder: String,
+ activeOrder: String,
+ ): Pair<String, Boolean> {
+ val actives = activeOrder.toMutableList()
+ val result = mutableListOf<Char>()
+ return (sorter ?: SemiStableSort())
+ .stabilizeTo(
+ actives,
+ { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } },
+ result,
+ )
+ .let { ordered -> result.joinToString("") to ordered }
+ }
+
+ private fun stringSort(
+ stableOrder: String,
+ activeOrder: String,
+ ): Pair<String, Boolean> {
+ val actives = activeOrder.toMutableList()
+ if (shuffleInput) {
+ actives.shuffle()
+ }
+ return (sorter ?: SemiStableSort())
+ .sort(
+ actives,
+ { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } },
+ compareBy { activeOrder.indexOf(it) },
+ )
+ .let { ordered -> actives.joinToString("") to ordered }
+ }
+
+ private fun testCase(
+ stableOrder: String,
+ activeOrder: String,
+ expected: String,
+ expectOrdered: Boolean,
+ ) {
+ val (mergeResult, ordered) =
+ if (testStabilizeTo) stringStabilizeTo(stableOrder, activeOrder)
+ else stringSort(stableOrder, activeOrder)
+ val resultPass = expected == mergeResult
+ val orderedPass = ordered == expectOrdered
+ val pass = resultPass && orderedPass
+ val resultSuffix =
+ if (resultPass) "result=$expected" else "expected=$expected got=$mergeResult"
+ val orderedSuffix =
+ if (orderedPass) "ordered=$ordered" else "expected ordered to be $expectOrdered"
+ val readableResult = "stable=$stableOrder active=$activeOrder $resultSuffix $orderedSuffix"
+ Log.d("SemiStableSortTest", "${if (pass) "PASS" else "FAIL"}: $readableResult")
+ if (!pass) {
+ throw AssertionError("Test case failed: $readableResult")
+ }
+ }
+
+ private fun runAllTestCases() {
+ // No input or output
+ testCase("", "", "", true)
+ // Remove everything
+ testCase("ABCDEFG", "", "", true)
+ // Literally no changes
+ testCase("ABCDEFG", "ABCDEFG", "ABCDEFG", true)
+
+ // No stable order
+ testCase("", "ABCDEFG", "ABCDEFG", true)
+
+ // F moved after A, and...
+ testCase("ABCDEFG", "AFBCDEG", "ABCDEFG", false) // No other changes
+ testCase("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false) // Insert X before F
+ testCase("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false) // Insert X after F
+ testCase("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false) // Insert X where F was
+
+ // B moved after F, and...
+ testCase("ABCDEFG", "ACDEFBG", "ABCDEFG", false) // No other changes
+ testCase("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false) // Insert X before B
+ testCase("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false) // Insert X after B
+ testCase("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false) // Insert X where B was
+
+ // Swap F and B, and...
+ testCase("ABCDEFG", "AFCDEBG", "ABCDEFG", false) // No other changes
+ testCase("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false) // Insert X before F
+ testCase("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false) // Insert X after F
+ testCase("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false) // Insert X between CD (Alt: ABCXDEFG)
+ testCase("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false) // Insert X between DE (Alt: ABCDEFXG)
+ testCase("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false) // Insert X before B
+ testCase("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false) // Insert X after B
+
+ // Remove a bunch of entries at once
+ testCase("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true)
+
+ // Remove a bunch of entries and scramble
+ testCase("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false)
+
+ // Add a bunch of entries at once
+ testCase("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true)
+
+ // Add a bunch of entries and reverse originals
+ // NOTE: Some of these don't have obviously correct answers
+ testCase("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false) // appended
+ testCase("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false) // prepended
+ testCase("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false) // closer to back: append
+ testCase("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false) // closer to front: prepend
+ testCase("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false) // split new entries
+
+ // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
+ testCase("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false)
+ }
+
+ @Test
+ fun testSort() {
+ testStabilizeTo = false
+ shuffleInput = false
+ sorter = null
+ runAllTestCases()
+ }
+
+ @Test
+ fun testSortWithSingleInstance() {
+ testStabilizeTo = false
+ shuffleInput = false
+ sorter = SemiStableSort()
+ runAllTestCases()
+ }
+
+ @Test
+ fun testSortWithShuffledInput() {
+ testStabilizeTo = false
+ shuffleInput = true
+ sorter = null
+ runAllTestCases()
+ }
+
+ @Test
+ fun testStabilizeTo() {
+ testStabilizeTo = true
+ sorter = null
+ runAllTestCases()
+ }
+
+ @Test
+ fun testStabilizeToWithSingleInstance() {
+ testStabilizeTo = true
+ sorter = SemiStableSort()
+ runAllTestCases()
+ }
+
+ @Test
+ fun testIsSorted() {
+ val intCmp = Comparator<Int> { x, y -> Integer.compare(x, y) }
+ SemiStableSort.apply {
+ assertTrue(emptyList<Int>().isSorted(intCmp))
+ assertTrue(listOf(1).isSorted(intCmp))
+ assertTrue(listOf(1, 2).isSorted(intCmp))
+ assertTrue(listOf(1, 2, 3).isSorted(intCmp))
+ assertTrue(listOf(1, 2, 3, 4).isSorted(intCmp))
+ assertTrue(listOf(1, 2, 3, 4, 5).isSorted(intCmp))
+ assertTrue(listOf(1, 1, 1, 1, 1).isSorted(intCmp))
+ assertTrue(listOf(1, 1, 2, 2, 3, 3).isSorted(intCmp))
+ assertFalse(listOf(2, 1).isSorted(intCmp))
+ assertFalse(listOf(2, 1, 2).isSorted(intCmp))
+ assertFalse(listOf(1, 2, 1).isSorted(intCmp))
+ assertFalse(listOf(1, 2, 3, 2, 5).isSorted(intCmp))
+ assertFalse(listOf(5, 2, 3, 4, 5).isSorted(intCmp))
+ assertFalse(listOf(1, 2, 3, 4, 1).isSorted(intCmp))
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt
new file mode 100644
index 0000000..2036954
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper.getContiguousSubLists
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class ShadeListBuilderHelperTest : SysuiTestCase() {
+
+ @Test
+ fun testGetContiguousSubLists() {
+ assertThat(getContiguousSubLists("AAAAAA".toList()) { it })
+ .containsExactly(
+ listOf('A', 'A', 'A', 'A', 'A', 'A'),
+ )
+ .inOrder()
+ assertThat(getContiguousSubLists("AAABBB".toList()) { it })
+ .containsExactly(
+ listOf('A', 'A', 'A'),
+ listOf('B', 'B', 'B'),
+ )
+ .inOrder()
+ assertThat(getContiguousSubLists("AAABAA".toList()) { it })
+ .containsExactly(
+ listOf('A', 'A', 'A'),
+ listOf('B'),
+ listOf('A', 'A'),
+ )
+ .inOrder()
+ assertThat(getContiguousSubLists("AAABAA".toList(), minLength = 2) { it })
+ .containsExactly(
+ listOf('A', 'A', 'A'),
+ listOf('A', 'A'),
+ )
+ .inOrder()
+ assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList()) { it })
+ .containsExactly(
+ listOf('A', 'A', 'A'),
+ listOf('B', 'B', 'B', 'B'),
+ listOf('C', 'C'),
+ listOf('D'),
+ listOf('E', 'E', 'E'),
+ )
+ .inOrder()
+ assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList(), minLength = 2) { it })
+ .containsExactly(
+ listOf('A', 'A', 'A'),
+ listOf('B', 'B', 'B', 'B'),
+ listOf('C', 'C'),
+ listOf('E', 'E', 'E'),
+ )
+ .inOrder()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java
index 3f641df..ca65987 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java
@@ -91,6 +91,8 @@
verifyNoMoreInteractions(mLogger);
clearInvocations(mLogger);
+ when(mBindStage.tryGetStageParams(eq(mEntry))).thenReturn(new RowContentBindParams());
+
mViewBinder.unbindHeadsUpView(mEntry);
verify(mLogger).entryContentViewMarkedFreeable(eq(mEntry));
verifyNoMoreInteractions(mLogger);
@@ -139,6 +141,8 @@
verifyNoMoreInteractions(mLogger);
clearInvocations(mLogger);
+ when(mBindStage.tryGetStageParams(eq(mEntry))).thenReturn(new RowContentBindParams());
+
mViewBinder.unbindHeadsUpView(mEntry);
verify(mLogger).currentOngoingBindingAborted(eq(mEntry));
verify(mLogger).entryContentViewMarkedFreeable(eq(mEntry));
@@ -150,4 +154,30 @@
verifyNoMoreInteractions(mLogger);
clearInvocations(mLogger);
}
+
+ @Test
+ public void testLoggingForLateUnbindFlow() {
+ AtomicReference<NotifBindPipeline.BindCallback> callback = new AtomicReference<>();
+ when(mBindStage.requestRebind(any(), any())).then(i -> {
+ callback.set(i.getArgument(1));
+ return new CancellationSignal();
+ });
+
+ mViewBinder.bindHeadsUpView(mEntry, null);
+ verify(mLogger).startBindingHun(eq(mEntry));
+ verifyNoMoreInteractions(mLogger);
+ clearInvocations(mLogger);
+
+ callback.get().onBindFinished(mEntry);
+ verify(mLogger).entryBoundSuccessfully(eq(mEntry));
+ verifyNoMoreInteractions(mLogger);
+ clearInvocations(mLogger);
+
+ when(mBindStage.tryGetStageParams(eq(mEntry))).thenReturn(null);
+
+ mViewBinder.unbindHeadsUpView(mEntry);
+ verify(mLogger).entryBindStageParamsNullOnUnbind(eq(mEntry));
+ verifyNoMoreInteractions(mLogger);
+ clearInvocations(mLogger);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java
index d59cc54..8b7b4de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java
@@ -29,6 +29,7 @@
import static com.android.systemui.util.mockito.KotlinMockitoHelpersKt.argThat;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@@ -305,15 +306,59 @@
}
@Test
- public void hideSilentNotificationsPerUserSetting() {
- when(mKeyguardStateController.isShowing()).thenReturn(true);
+ public void hideSilentOnLockscreenSetting() {
+ // GIVEN an 'unfiltered-keyguard-showing' state and notifications shown on lockscreen
+ setupUnfilteredState(mEntry);
mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true);
+
+ // WHEN the show silent notifs on lockscreen setting is false
mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false);
+
+ // WHEN the notification is not high priority and not ambient
+ mEntry = new NotificationEntryBuilder()
+ .setImportance(IMPORTANCE_LOW)
+ .build();
+ when(mHighPriorityProvider.isHighPriority(any())).thenReturn(false);
+
+ // THEN filter out the entry
+ assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry));
+ }
+
+ @Test
+ public void showSilentOnLockscreenSetting() {
+ // GIVEN an 'unfiltered-keyguard-showing' state and notifications shown on lockscreen
+ setupUnfilteredState(mEntry);
+ mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true);
+
+ // WHEN the show silent notifs on lockscreen setting is true
+ mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, true);
+
+ // WHEN the notification is not high priority and not ambient
+ mEntry = new NotificationEntryBuilder()
+ .setImportance(IMPORTANCE_LOW)
+ .build();
+ when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
+
+ // THEN do not filter out the entry
+ assertFalse(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry));
+ }
+
+ @Test
+ public void defaultSilentOnLockscreenSettingIsHide() {
+ // GIVEN an 'unfiltered-keyguard-showing' state and notifications shown on lockscreen
+ setupUnfilteredState(mEntry);
+ mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true);
+
+ // WHEN the notification is not high priority and not ambient
mEntry = new NotificationEntryBuilder()
.setUser(new UserHandle(NOTIF_USER_ID))
.setImportance(IMPORTANCE_LOW)
.build();
when(mHighPriorityProvider.isHighPriority(any())).thenReturn(false);
+
+ // WhHEN the show silent notifs on lockscreen setting is unset
+ assertNull(mFakeSettings.getString(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS));
+
assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry));
}
@@ -431,25 +476,6 @@
}
@Test
- public void showSilentOnLockscreenSetting() {
- // GIVEN an 'unfiltered-keyguard-showing' state
- setupUnfilteredState(mEntry);
-
- // WHEN the notification is not high priority and not ambient
- mEntry.setRanking(new RankingBuilder()
- .setKey(mEntry.getKey())
- .setImportance(IMPORTANCE_LOW)
- .build());
- when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
-
- // WHEN the show silent notifs on lockscreen setting is true
- mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, true);
-
- // THEN do not filter out the entry
- assertFalse(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry));
- }
-
- @Test
public void notificationVisibilityPublic() {
// GIVEN a VISIBILITY_PUBLIC notification
NotificationEntryBuilder entryBuilder = new NotificationEntryBuilder()
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/row/RowContentBindStageTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java
index ad3bd71..7c99568 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java
@@ -21,6 +21,10 @@
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNotSame;
+import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -31,6 +35,7 @@
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.util.Log;
import androidx.test.filters.SmallTest;
@@ -100,6 +105,67 @@
verify(mBinder).unbindContent(eq(mEntry), any(), eq(flags));
}
+ class CountingWtfHandler implements Log.TerribleFailureHandler {
+ private Log.TerribleFailureHandler mOldHandler = null;
+ private int mWtfCount = 0;
+
+ public void register() {
+ mOldHandler = Log.setWtfHandler(this);
+ }
+
+ public void unregister() {
+ Log.setWtfHandler(mOldHandler);
+ mOldHandler = null;
+ }
+
+ @Override
+ public void onTerribleFailure(String tag, Log.TerribleFailure what, boolean system) {
+ mWtfCount++;
+ }
+
+ public int getWtfCount() {
+ return mWtfCount;
+ }
+ }
+
+ @Test
+ public void testGetStageParamsAfterCleanUp() {
+ // GIVEN an entry whose params have already been deleted.
+ RowContentBindParams originalParams = mRowContentBindStage.getStageParams(mEntry);
+ mRowContentBindStage.deleteStageParams(mEntry);
+
+ // WHEN a caller calls getStageParams.
+ CountingWtfHandler countingWtfHandler = new CountingWtfHandler();
+ countingWtfHandler.register();
+
+ RowContentBindParams blankParams = mRowContentBindStage.getStageParams(mEntry);
+
+ countingWtfHandler.unregister();
+
+ // THEN getStageParams logs a WTF and returns blank params created to avoid a crash.
+ assertEquals(1, countingWtfHandler.getWtfCount());
+ assertNotNull(blankParams);
+ assertNotSame(originalParams, blankParams);
+ }
+
+ @Test
+ public void testTryGetStageParamsAfterCleanUp() {
+ // GIVEN an entry whose params have already been deleted.
+ mRowContentBindStage.deleteStageParams(mEntry);
+
+ // WHEN a caller calls getStageParams.
+ CountingWtfHandler countingWtfHandler = new CountingWtfHandler();
+ countingWtfHandler.register();
+
+ RowContentBindParams nullParams = mRowContentBindStage.tryGetStageParams(mEntry);
+
+ countingWtfHandler.unregister();
+
+ // THEN getStageParams does NOT log a WTF and returns null to indicate missing params.
+ assertEquals(0, countingWtfHandler.getWtfCount());
+ assertNull(nullParams);
+ }
+
@Test
public void testRebindAllContentViews() {
// GIVEN a view with content bound.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
index 11798a7..87f4c32 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
@@ -361,6 +361,22 @@
assertThat(sut.isOnKeyguard).isFalse()
}
// endregion
+
+ // region mIsClosing
+ @Test
+ fun isClosing_whenShadeClosing_shouldReturnTrue() {
+ sut.setIsClosing(true)
+
+ assertThat(sut.isClosing).isTrue()
+ }
+
+ @Test
+ fun isClosing_whenShadeFinishClosing_shouldReturnFalse() {
+ sut.setIsClosing(false)
+
+ assertThat(sut.isClosing).isFalse()
+ }
+ // endregion
}
// region Arrange helper methods.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
index 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 4353036..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);
@@ -728,6 +728,57 @@
verify(mNotificationStackSizeCalculator).computeHeight(any(), anyInt(), anyFloat());
}
+ @Test
+ public void testSetOwnScrollY_shadeNotClosing_scrollYChanges() {
+ // Given: shade is not closing, scrollY is 0
+ mAmbientState.setScrollY(0);
+ assertEquals(0, mAmbientState.getScrollY());
+ mAmbientState.setIsClosing(false);
+
+ // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1
+ mStackScroller.setOwnScrollY(1);
+
+ // Then: scrollY should be set to 1
+ assertEquals(1, mAmbientState.getScrollY());
+
+ // Reset scrollY back to 0 to avoid interfering with other tests
+ mStackScroller.setOwnScrollY(0);
+ assertEquals(0, mAmbientState.getScrollY());
+ }
+
+ @Test
+ public void testSetOwnScrollY_shadeClosing_scrollYDoesNotChange() {
+ // Given: shade is closing, scrollY is 0
+ mAmbientState.setScrollY(0);
+ assertEquals(0, mAmbientState.getScrollY());
+ mAmbientState.setIsClosing(true);
+
+ // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1
+ mStackScroller.setOwnScrollY(1);
+
+ // Then: scrollY should not change, it should still be 0
+ assertEquals(0, mAmbientState.getScrollY());
+
+ // Reset scrollY and mAmbientState.mIsClosing to avoid interfering with other tests
+ mAmbientState.setIsClosing(false);
+ mStackScroller.setOwnScrollY(0);
+ assertEquals(0, mAmbientState.getScrollY());
+ }
+
+ @Test
+ public void onShadeFlingClosingEnd_scrollYShouldBeSetToZero() {
+ // Given: mAmbientState.mIsClosing is set to be true
+ // mIsExpanded is set to be false
+ mAmbientState.setIsClosing(true);
+ mStackScroller.setIsExpanded(false);
+
+ // When: onExpansionStopped is called
+ mStackScroller.onExpansionStopped();
+
+ // Then: mAmbientState.scrollY should be set to be 0
+ assertEquals(mAmbientState.getScrollY(), 0);
+ }
+
private void setBarStateForTest(int state) {
// Can't inject this through the listener or we end up on the actual implementation
// rather than the mock because the spy just coppied the anonymous inner /shruggie.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
new file mode 100644
index 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/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index cd0cc33..6fa2174 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -100,8 +100,6 @@
@Mock
private AuthController mAuthController;
@Mock
- private DozeParameters mDozeParameters;
- @Mock
private MetricsLogger mMetricsLogger;
@Mock
private NotificationMediaManager mNotificationMediaManager;
@@ -127,7 +125,7 @@
public void setUp() {
MockitoAnnotations.initMocks(this);
TestableResources res = getContext().getOrCreateTestableResources();
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true);
when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true);
when(mKeyguardStateController.isUnlocked()).thenReturn(false);
@@ -139,7 +137,7 @@
mBiometricUnlockController = new BiometricUnlockController(mDozeScrimController,
mKeyguardViewMediator, mScrimController, mShadeController,
mNotificationShadeWindowController, mKeyguardStateController, mHandler,
- mUpdateMonitor, res.getResources(), mKeyguardBypassController, mDozeParameters,
+ mUpdateMonitor, res.getResources(), mKeyguardBypassController,
mMetricsLogger, mDumpManager, mPowerManager,
mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle,
mAuthController, mStatusBarStateController, mKeyguardUnlockAnimationController,
@@ -177,7 +175,7 @@
public void onBiometricAuthenticated_whenFingerprintAndNotInteractive_wakeAndUnlock() {
reset(mUpdateMonitor);
reset(mStatusBarKeyguardViewManager);
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
when(mDozeScrimController.isPulsing()).thenReturn(true);
// the value of isStrongBiometric doesn't matter here since we only care about the returned
@@ -194,7 +192,7 @@
public void onBiometricAuthenticated_whenDeviceIsAlreadyUnlocked_wakeAndUnlock() {
reset(mUpdateMonitor);
reset(mStatusBarKeyguardViewManager);
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
when(mKeyguardStateController.isUnlocked()).thenReturn(true);
when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
when(mDozeScrimController.isPulsing()).thenReturn(false);
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 f510e48..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;
@@ -116,6 +118,7 @@
import com.android.systemui.shade.NotificationShadeWindowViewController;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.shade.ShadeControllerImpl;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.shared.plugins.PluginManager;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -150,7 +153,6 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -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));
@@ -413,7 +416,7 @@
mNotificationGutsManager,
notificationLogger,
mNotificationInterruptStateProvider,
- new PanelExpansionStateManager(),
+ new ShadeExpansionStateManager(),
mKeyguardViewMediator,
new DisplayMetrics(),
mMetricsLogger,
@@ -486,7 +489,7 @@
when(mKeyguardViewMediator.registerCentralSurfaces(
any(CentralSurfacesImpl.class),
any(NotificationPanelViewController.class),
- any(PanelExpansionStateManager.class),
+ any(ShadeExpansionStateManager.class),
any(BiometricUnlockController.class),
any(ViewGroup.class),
any(KeyguardBypassController.class)))
@@ -516,32 +519,32 @@
@Test
public void executeRunnableDismissingKeyguard_nullRunnable_showingAndOccluded() {
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(true);
mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false);
}
@Test
public void executeRunnableDismissingKeyguard_nullRunnable_showing() {
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false);
}
@Test
public void executeRunnableDismissingKeyguard_nullRunnable_notShowing() {
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false);
}
@Test
public void executeRunnableDismissingKeyguard_dreaming_notShowing() throws RemoteException {
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardUpdateMonitor.isDreaming()).thenReturn(true);
mCentralSurfaces.executeRunnableDismissingKeyguard(() -> {},
@@ -555,8 +558,8 @@
@Test
public void executeRunnableDismissingKeyguard_notDreaming_notShowing() throws RemoteException {
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardUpdateMonitor.isDreaming()).thenReturn(false);
mCentralSurfaces.executeRunnableDismissingKeyguard(() -> {},
@@ -571,10 +574,10 @@
@Test
public void lockscreenStateMetrics_notShowing() {
// uninteresting state, except that fingerprint must be non-zero
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
// interesting state
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
when(mKeyguardStateController.isMethodSecure()).thenReturn(false);
mCentralSurfaces.onKeyguardViewManagerStatesUpdated();
@@ -589,10 +592,10 @@
@Test
public void lockscreenStateMetrics_notShowing_secure() {
// uninteresting state, except that fingerprint must be non-zero
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
// interesting state
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
@@ -608,10 +611,10 @@
@Test
public void lockscreenStateMetrics_isShowing() {
// uninteresting state, except that fingerprint must be non-zero
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
// interesting state
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
when(mKeyguardStateController.isMethodSecure()).thenReturn(false);
@@ -627,10 +630,10 @@
@Test
public void lockscreenStateMetrics_isShowing_secure() {
// uninteresting state, except that fingerprint must be non-zero
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
// interesting state
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
@@ -646,10 +649,10 @@
@Test
public void lockscreenStateMetrics_isShowingBouncer() {
// uninteresting state, except that fingerprint must be non-zero
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
// interesting state
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
@@ -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);
@@ -1053,9 +1110,9 @@
}
@Test
- public void startActivityDismissingKeyguard_isShowingandIsOccluded() {
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(true);
+ public void startActivityDismissingKeyguard_isShowingAndIsOccluded() {
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(true);
mCentralSurfaces.startActivityDismissingKeyguard(
new Intent(),
/* onlyProvisioned = */false,
@@ -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/FakeKeyguardStateController.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java
new file mode 100644
index 0000000..a986777
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java
@@ -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.statusbar.phone;
+
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+
+/**
+ * Mock implementation of KeyguardStateController which tracks showing and occluded states
+ * based on {@link #notifyKeyguardState(boolean showing, boolean occluded)}}.
+ */
+public class FakeKeyguardStateController implements KeyguardStateController {
+ private boolean mShowing;
+ private boolean mOccluded;
+ private boolean mCanDismissLockScreen;
+
+ @Override
+ public void notifyKeyguardState(boolean showing, boolean occluded) {
+ mShowing = showing;
+ mOccluded = occluded;
+ }
+
+ @Override
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ @Override
+ public boolean isOccluded() {
+ return mOccluded;
+ }
+
+ public void setCanDismissLockScreen(boolean canDismissLockScreen) {
+ mCanDismissLockScreen = canDismissLockScreen;
+ }
+
+ @Override
+ public boolean canDismissLockScreen() {
+ return mCanDismissLockScreen;
+ }
+
+ @Override
+ public boolean isBouncerShowing() {
+ return false;
+ }
+
+ @Override
+ public boolean isKeyguardScreenRotationAllowed() {
+ return false;
+ }
+
+ @Override
+ public boolean isMethodSecure() {
+ return true;
+ }
+
+ @Override
+ public boolean isTrusted() {
+ return false;
+ }
+
+ @Override
+ public boolean isKeyguardGoingAway() {
+ return false;
+ }
+
+ @Override
+ public boolean isKeyguardFadingAway() {
+ return false;
+ }
+
+ @Override
+ public boolean isLaunchTransitionFadingAway() {
+ return false;
+ }
+
+ @Override
+ public long getKeyguardFadingAwayDuration() {
+ return 0;
+ }
+
+ @Override
+ public long getKeyguardFadingAwayDelay() {
+ return 0;
+ }
+
+ @Override
+ public long calculateGoingToFullShadeDelay() {
+ return 0;
+ }
+
+ @Override
+ public float getDismissAmount() {
+ return 0f;
+ }
+
+ @Override
+ public boolean isDismissingFromSwipe() {
+ return false;
+ }
+
+ @Override
+ public boolean isFlingingToDismissKeyguard() {
+ return false;
+ }
+
+ @Override
+ public boolean isFlingingToDismissKeyguardDuringSwipeGesture() {
+ return false;
+ }
+
+ @Override
+ public boolean isSnappingKeyguardBackAfterSwipe() {
+ return false;
+ }
+
+ @Override
+ public void notifyPanelFlingStart(boolean dismiss) {
+ }
+
+ @Override
+ public void notifyPanelFlingEnd() {
+ }
+
+ @Override
+ public void addCallback(Callback listener) {
+ }
+
+ @Override
+ public void removeCallback(Callback listener) {
+ }
+}
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 04ad1f8..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
@@ -28,6 +28,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -61,14 +62,13 @@
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.unfold.SysUIUnfoldComponent;
import com.google.common.truth.Truth;
@@ -89,12 +89,11 @@
@TestableLooper.RunWithLooper
public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
- private static final PanelExpansionChangeEvent EXPANSION_EVENT =
+ private static final ShadeExpansionChangeEvent EXPANSION_EVENT =
expansionEvent(/* fraction= */ 0.5f, /* expanded= */ false, /* tracking= */ true);
@Mock private ViewMediatorCallback mViewMediatorCallback;
@Mock private LockPatternUtils mLockPatternUtils;
- @Mock private KeyguardStateController mKeyguardStateController;
@Mock private CentralSurfaces mCentralSurfaces;
@Mock private ViewGroup mContainer;
@Mock private NotificationPanelViewController mNotificationPanelView;
@@ -118,11 +117,12 @@
@Mock private BouncerCallbackInteractor mBouncerCallbackInteractor;
@Mock private BouncerInteractor mBouncerInteractor;
@Mock private BouncerView mBouncerView;
-// @Mock private WeakReference<BouncerViewDelegate> mBouncerViewDelegateWeakReference;
@Mock private BouncerViewDelegate mBouncerViewDelegate;
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private KeyguardBouncer.BouncerExpansionCallback mBouncerExpansionCallback;
+ private FakeKeyguardStateController mKeyguardStateController =
+ spy(new FakeKeyguardStateController());
@Mock private ViewRootImpl mViewRootImpl;
@Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
@@ -176,11 +176,10 @@
mStatusBarKeyguardViewManager.registerCentralSurfaces(
mCentralSurfaces,
mNotificationPanelView,
- new PanelExpansionStateManager(),
+ new ShadeExpansionStateManager(),
mBiometricUnlockController,
mNotificationContainer,
mBypassController);
- when(mKeyguardStateController.isOccluded()).thenReturn(false);
mStatusBarKeyguardViewManager.show(null);
ArgumentCaptor<KeyguardBouncer.BouncerExpansionCallback> callbackArgumentCaptor =
ArgumentCaptor.forClass(KeyguardBouncer.BouncerExpansionCallback.class);
@@ -253,7 +252,7 @@
@Test
public void onPanelExpansionChanged_showsBouncerWhenSwiping() {
- when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
+ mKeyguardStateController.setCanDismissLockScreen(false);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
verify(mBouncer).show(eq(false), eq(false));
@@ -340,13 +339,12 @@
}
@Test
- public void setOccluded_onKeyguardOccludedChangedCalledCorrectly() {
+ public void setOccluded_onKeyguardOccludedChangedCalled() {
clearInvocations(mKeyguardStateController);
clearInvocations(mKeyguardUpdateMonitor);
- // Should be false to start, so no invocations
mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */);
- verify(mKeyguardStateController, never()).notifyKeyguardState(anyBoolean(), anyBoolean());
+ verify(mKeyguardStateController).notifyKeyguardState(true, false);
clearInvocations(mKeyguardUpdateMonitor);
clearInvocations(mKeyguardStateController);
@@ -357,8 +355,8 @@
clearInvocations(mKeyguardUpdateMonitor);
clearInvocations(mKeyguardStateController);
- mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
- verify(mKeyguardStateController, never()).notifyKeyguardState(anyBoolean(), anyBoolean());
+ mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */);
+ verify(mKeyguardStateController).notifyKeyguardState(true, false);
}
@Test
@@ -426,7 +424,7 @@
when(mAlternateAuthInterceptor.isShowingAlternateAuthBouncer()).thenReturn(true);
assertTrue(
"Is showing not accurate when alternative auth showing",
- mStatusBarKeyguardViewManager.isShowing());
+ mStatusBarKeyguardViewManager.isBouncerShowing());
}
@Test
@@ -520,9 +518,9 @@
Truth.assertThat(mStatusBarKeyguardViewManager.isBouncerInTransit()).isFalse();
}
- private static PanelExpansionChangeEvent expansionEvent(
+ private static ShadeExpansionChangeEvent expansionEvent(
float fraction, boolean expanded, boolean tracking) {
- return new PanelExpansionChangeEvent(
+ return new ShadeExpansionChangeEvent(
fraction, expanded, tracking, /* dragDownPxAmount= */ 0f);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
index 1ee8875..78a4db1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
@@ -20,7 +20,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogcatEchoTracker
import com.android.systemui.statusbar.DisableFlagsLogger
import com.google.common.truth.Truth.assertThat
import org.junit.Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index a3c6e95..438271c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -49,11 +49,12 @@
import com.android.systemui.SysuiBaseFragmentTest;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogcatEchoTracker;
import com.android.systemui.plugins.DarkIconDispatcher;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogcatEchoTracker;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.DisableFlagsLogger;
import com.android.systemui.statusbar.OperatorNameViewController;
@@ -66,7 +67,6 @@
import com.android.systemui.statusbar.phone.StatusBarLocationPublisher;
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.CarrierConfigTracker;
import com.android.systemui.util.concurrency.FakeExecutor;
@@ -441,7 +441,7 @@
mAnimationScheduler,
mLocationPublisher,
mMockNotificationAreaController,
- new PanelExpansionStateManager(),
+ new ShadeExpansionStateManager(),
mock(FeatureFlags.class),
mStatusBarIconController,
mIconManagerFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
deleted file mode 100644
index c4f8049..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
+++ /dev/null
@@ -1,209 +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.statusbar.phone.panelstate
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-
-@SmallTest
-class PanelExpansionStateManagerTest : SysuiTestCase() {
-
- private lateinit var panelExpansionStateManager: PanelExpansionStateManager
-
- @Before
- fun setUp() {
- panelExpansionStateManager = PanelExpansionStateManager()
- }
-
- @Test
- fun onPanelExpansionChanged_listenerNotified() {
- val listener = TestPanelExpansionListener()
- panelExpansionStateManager.addExpansionListener(listener)
- val fraction = 0.6f
- val expanded = true
- val tracking = true
- val dragDownAmount = 1234f
-
- panelExpansionStateManager.onPanelExpansionChanged(
- fraction, expanded, tracking, dragDownAmount)
-
- assertThat(listener.fraction).isEqualTo(fraction)
- assertThat(listener.expanded).isEqualTo(expanded)
- assertThat(listener.tracking).isEqualTo(tracking)
- assertThat(listener.dragDownAmountPx).isEqualTo(dragDownAmount)
- }
-
- @Test
- fun addExpansionListener_listenerNotifiedOfCurrentValues() {
- val fraction = 0.6f
- val expanded = true
- val tracking = true
- val dragDownAmount = 1234f
- panelExpansionStateManager.onPanelExpansionChanged(
- fraction, expanded, tracking, dragDownAmount)
- val listener = TestPanelExpansionListener()
-
- panelExpansionStateManager.addExpansionListener(listener)
-
- assertThat(listener.fraction).isEqualTo(fraction)
- assertThat(listener.expanded).isEqualTo(expanded)
- assertThat(listener.tracking).isEqualTo(tracking)
- assertThat(listener.dragDownAmountPx).isEqualTo(dragDownAmount)
- }
-
- @Test
- fun updateState_listenerNotified() {
- val listener = TestPanelStateListener()
- panelExpansionStateManager.addStateListener(listener)
-
- panelExpansionStateManager.updateState(STATE_OPEN)
-
- assertThat(listener.state).isEqualTo(STATE_OPEN)
- }
-
- /* ***** [PanelExpansionStateManager.onPanelExpansionChanged] test cases *******/
-
- /* Fraction < 1 test cases */
-
- @Test
- fun onPEC_fractionLessThanOne_expandedTrue_trackingFalse_becomesStateOpening() {
- val listener = TestPanelStateListener()
- panelExpansionStateManager.addStateListener(listener)
-
- panelExpansionStateManager.onPanelExpansionChanged(
- fraction = 0.5f, expanded = true, tracking = false, dragDownPxAmount = 0f)
-
- assertThat(listener.state).isEqualTo(STATE_OPENING)
- }
-
- @Test
- fun onPEC_fractionLessThanOne_expandedTrue_trackingTrue_becomesStateOpening() {
- val listener = TestPanelStateListener()
- panelExpansionStateManager.addStateListener(listener)
-
- panelExpansionStateManager.onPanelExpansionChanged(
- fraction = 0.5f, expanded = true, tracking = true, dragDownPxAmount = 0f)
-
- assertThat(listener.state).isEqualTo(STATE_OPENING)
- }
-
- @Test
- fun onPEC_fractionLessThanOne_expandedFalse_trackingFalse_becomesStateClosed() {
- val listener = TestPanelStateListener()
- panelExpansionStateManager.addStateListener(listener)
- // Start out on a different state
- panelExpansionStateManager.updateState(STATE_OPEN)
-
- panelExpansionStateManager.onPanelExpansionChanged(
- fraction = 0.5f, expanded = false, tracking = false, dragDownPxAmount = 0f)
-
- assertThat(listener.state).isEqualTo(STATE_CLOSED)
- }
-
- @Test
- fun onPEC_fractionLessThanOne_expandedFalse_trackingTrue_doesNotBecomeStateClosed() {
- val listener = TestPanelStateListener()
- panelExpansionStateManager.addStateListener(listener)
- // Start out on a different state
- panelExpansionStateManager.updateState(STATE_OPEN)
-
- panelExpansionStateManager.onPanelExpansionChanged(
- fraction = 0.5f, expanded = false, tracking = true, dragDownPxAmount = 0f)
-
- assertThat(listener.state).isEqualTo(STATE_OPEN)
- }
-
- /* Fraction = 1 test cases */
-
- @Test
- fun onPEC_fractionOne_expandedTrue_trackingFalse_becomesStateOpeningThenStateOpen() {
- val listener = TestPanelStateListener()
- panelExpansionStateManager.addStateListener(listener)
-
- panelExpansionStateManager.onPanelExpansionChanged(
- fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)
-
- assertThat(listener.previousState).isEqualTo(STATE_OPENING)
- assertThat(listener.state).isEqualTo(STATE_OPEN)
- }
-
- @Test
- fun onPEC_fractionOne_expandedTrue_trackingTrue_becomesStateOpening() {
- val listener = TestPanelStateListener()
- panelExpansionStateManager.addStateListener(listener)
-
- panelExpansionStateManager.onPanelExpansionChanged(
- fraction = 1f, expanded = true, tracking = true, dragDownPxAmount = 0f)
-
- assertThat(listener.state).isEqualTo(STATE_OPENING)
- }
-
- @Test
- fun onPEC_fractionOne_expandedFalse_trackingFalse_becomesStateClosed() {
- val listener = TestPanelStateListener()
- panelExpansionStateManager.addStateListener(listener)
- // Start out on a different state
- panelExpansionStateManager.updateState(STATE_OPEN)
-
- panelExpansionStateManager.onPanelExpansionChanged(
- fraction = 1f, expanded = false, tracking = false, dragDownPxAmount = 0f)
-
- assertThat(listener.state).isEqualTo(STATE_CLOSED)
- }
-
- @Test
- fun onPEC_fractionOne_expandedFalse_trackingTrue_doesNotBecomeStateClosed() {
- val listener = TestPanelStateListener()
- panelExpansionStateManager.addStateListener(listener)
- // Start out on a different state
- panelExpansionStateManager.updateState(STATE_OPEN)
-
- panelExpansionStateManager.onPanelExpansionChanged(
- fraction = 1f, expanded = false, tracking = true, dragDownPxAmount = 0f)
-
- assertThat(listener.state).isEqualTo(STATE_OPEN)
- }
-
- /* ***** end [PanelExpansionStateManager.onPanelExpansionChanged] test cases ******/
-
- class TestPanelExpansionListener : PanelExpansionListener {
- var fraction: Float = 0f
- var expanded: Boolean = false
- var tracking: Boolean = false
- var dragDownAmountPx: Float = 0f
-
- override fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) {
- this.fraction = event.fraction
- this.expanded = event.expanded
- this.tracking = event.tracking
- this.dragDownAmountPx = event.dragDownPxAmount
- }
- }
-
- class TestPanelStateListener : PanelStateListener {
- @PanelState var previousState: Int = STATE_CLOSED
- @PanelState var state: Int = STATE_CLOSED
-
- override fun onPanelStateChanged(state: Int) {
- this.previousState = this.state
- this.state = state
- }
- }
-}
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/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
new file mode 100644
index 0000000..6ff7b7c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileConnectionRepository : MobileConnectionRepository {
+ private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel())
+ override val subscriptionModelFlow: Flow<MobileSubscriptionModel> = _subscriptionsModelFlow
+
+ fun setMobileSubscriptionModel(model: MobileSubscriptionModel) {
+ _subscriptionsModelFlow.value = model
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
similarity index 66%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 0d15268..c88d468 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -18,11 +18,11 @@
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.settingslib.mobile.MobileMappings.Config
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-class FakeMobileSubscriptionRepository : MobileSubscriptionRepository {
+class FakeMobileConnectionsRepository : MobileConnectionsRepository {
private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf())
override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow
@@ -30,22 +30,27 @@
MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
- private val subIdFlows = mutableMapOf<Int, MutableStateFlow<MobileSubscriptionModel>>()
- override fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> {
- return subIdFlows[subId]
- ?: MutableStateFlow(MobileSubscriptionModel()).also { subIdFlows[subId] = it }
+ private val _defaultDataSubRatConfig = MutableStateFlow(Config())
+ override val defaultDataSubRatConfig = _defaultDataSubRatConfig
+
+ private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>()
+ override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+ return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it }
}
fun setSubscriptions(subs: List<SubscriptionInfo>) {
_subscriptionsFlow.value = subs
}
+ fun setDefaultDataSubRatConfig(config: Config) {
+ _defaultDataSubRatConfig.value = config
+ }
+
fun setActiveMobileDataSubscriptionId(subId: Int) {
_activeMobileDataSubscriptionId.value = subId
}
- fun setMobileSubscriptionModel(model: MobileSubscriptionModel, subId: Int) {
- val subscription = subIdFlows[subId] ?: throw Exception("no flow exists for this subId yet")
- subscription.value = model
+ fun setMobileConnectionRepositoryForId(subId: Int, repo: MobileConnectionRepository) {
+ subIdRepos[subId] = repo
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
new file mode 100644
index 0000000..775e6db
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ServiceStateListener
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA
+import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileConnectionRepositoryTest : SysuiTestCase() {
+ private lateinit var underTest: MobileConnectionRepositoryImpl
+
+ @Mock private lateinit var subscriptionManager: SubscriptionManager
+ @Mock private lateinit var telephonyManager: TelephonyManager
+ @Mock private lateinit var logger: ConnectivityPipelineLogger
+
+ private val scope = CoroutineScope(IMMEDIATE)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
+
+ underTest =
+ MobileConnectionRepositoryImpl(
+ SUB_1_ID,
+ telephonyManager,
+ IMMEDIATE,
+ logger,
+ scope,
+ )
+ }
+
+ @After
+ fun tearDown() {
+ scope.cancel()
+ }
+
+ @Test
+ fun testFlowForSubId_default() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileSubscriptionModel? = null
+ val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(MobileSubscriptionModel())
+
+ job.cancel()
+ }
+
+ @Test
+ fun testFlowForSubId_emergencyOnly() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileSubscriptionModel? = null
+ val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+ val serviceState = ServiceState()
+ serviceState.isEmergencyOnly = true
+
+ getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+
+ assertThat(latest?.isEmergencyOnly).isEqualTo(true)
+
+ job.cancel()
+ }
+
+ @Test
+ fun testFlowForSubId_emergencyOnly_toggles() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileSubscriptionModel? = null
+ val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+ val callback = getTelephonyCallbackForType<ServiceStateListener>()
+ val serviceState = ServiceState()
+ serviceState.isEmergencyOnly = true
+ callback.onServiceStateChanged(serviceState)
+ serviceState.isEmergencyOnly = false
+ callback.onServiceStateChanged(serviceState)
+
+ assertThat(latest?.isEmergencyOnly).isEqualTo(false)
+
+ job.cancel()
+ }
+
+ @Test
+ fun testFlowForSubId_signalStrengths_levelsUpdate() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileSubscriptionModel? = null
+ val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
+ val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
+ callback.onSignalStrengthsChanged(strength)
+
+ assertThat(latest?.isGsm).isEqualTo(true)
+ assertThat(latest?.primaryLevel).isEqualTo(1)
+ assertThat(latest?.cdmaLevel).isEqualTo(2)
+
+ job.cancel()
+ }
+
+ @Test
+ fun testFlowForSubId_dataConnectionState() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileSubscriptionModel? = null
+ val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+ val callback =
+ getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(100, 200 /* unused */)
+
+ assertThat(latest?.dataConnectionState).isEqualTo(100)
+
+ job.cancel()
+ }
+
+ @Test
+ fun testFlowForSubId_dataActivity() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileSubscriptionModel? = null
+ val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DataActivityListener>()
+ callback.onDataActivity(3)
+
+ assertThat(latest?.dataActivityDirection).isEqualTo(3)
+
+ job.cancel()
+ }
+
+ @Test
+ fun testFlowForSubId_carrierNetworkChange() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileSubscriptionModel? = null
+ val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>()
+ callback.onCarrierNetworkChange(true)
+
+ assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true)
+
+ job.cancel()
+ }
+
+ @Test
+ fun subscriptionFlow_networkType_default() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileSubscriptionModel? = null
+ val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+ val type = NETWORK_TYPE_UNKNOWN
+ val expected = DefaultNetworkType(type)
+
+ assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun subscriptionFlow_networkType_updatesUsingDefault() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileSubscriptionModel? = null
+ val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+ val type = NETWORK_TYPE_LTE
+ val expected = DefaultNetworkType(type)
+ val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) }
+ callback.onDisplayInfoChanged(ti)
+
+ assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun subscriptionFlow_networkType_updatesUsingOverride() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileSubscriptionModel? = null
+ val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+ val type = OVERRIDE_NETWORK_TYPE_LTE_CA
+ val expected = OverrideNetworkType(type)
+ val ti =
+ mock<TelephonyDisplayInfo>().also {
+ whenever(it.overrideNetworkType).thenReturn(type)
+ }
+ callback.onDisplayInfoChanged(ti)
+
+ assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ private fun getTelephonyCallbacks(): List<TelephonyCallback> {
+ val callbackCaptor = argumentCaptor<TelephonyCallback>()
+ Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
+ return callbackCaptor.allValues
+ }
+
+ private inline fun <reified T> getTelephonyCallbackForType(): T {
+ val cbs = getTelephonyCallbacks().filterIsInstance<T>()
+ assertThat(cbs.size).isEqualTo(1)
+ return cbs[0]
+ }
+
+ /** Convenience constructor for SignalStrength */
+ private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
+ val signalStrength = mock<SignalStrength>()
+ whenever(signalStrength.isGsm).thenReturn(isGsm)
+ whenever(signalStrength.level).thenReturn(gsmLevel)
+ val cdmaStrength =
+ mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
+ whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
+ .thenReturn(listOf(cdmaStrength))
+
+ return signalStrength
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ private const val SUB_1_ID = 1
+ private val SUB_1 =
+ mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
new file mode 100644
index 0000000..326e0d281
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileConnectionsRepositoryTest : SysuiTestCase() {
+ private lateinit var underTest: MobileConnectionsRepositoryImpl
+
+ @Mock private lateinit var subscriptionManager: SubscriptionManager
+ @Mock private lateinit var telephonyManager: TelephonyManager
+ @Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+
+ private val scope = CoroutineScope(IMMEDIATE)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(
+ broadcastDispatcher.broadcastFlow(
+ any(),
+ nullable(),
+ ArgumentMatchers.anyInt(),
+ nullable(),
+ )
+ )
+ .thenReturn(flowOf(Unit))
+
+ underTest =
+ MobileConnectionsRepositoryImpl(
+ subscriptionManager,
+ telephonyManager,
+ logger,
+ broadcastDispatcher,
+ context,
+ IMMEDIATE,
+ scope,
+ mock(),
+ )
+ }
+
+ @After
+ fun tearDown() {
+ scope.cancel()
+ }
+
+ @Test
+ fun testSubscriptions_initiallyEmpty() =
+ runBlocking(IMMEDIATE) {
+ assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>())
+ }
+
+ @Test
+ fun testSubscriptions_listUpdates() =
+ runBlocking(IMMEDIATE) {
+ var latest: List<SubscriptionInfo>? = null
+
+ val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_2))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+
+ job.cancel()
+ }
+
+ @Test
+ fun testSubscriptions_removingSub_updatesList() =
+ runBlocking(IMMEDIATE) {
+ var latest: List<SubscriptionInfo>? = null
+
+ val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+ // WHEN 2 networks show up
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_2))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ // WHEN one network is removed
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_2))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ // THEN the subscriptions list represents the newest change
+ assertThat(latest).isEqualTo(listOf(SUB_2))
+
+ job.cancel()
+ }
+
+ @Test
+ fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
+ runBlocking(IMMEDIATE) {
+ assertThat(underTest.activeMobileDataSubscriptionId.value)
+ .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+ }
+
+ @Test
+ fun testActiveDataSubscriptionId_updates() =
+ runBlocking(IMMEDIATE) {
+ var active: Int? = null
+
+ val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this)
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+ .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+ assertThat(active).isEqualTo(SUB_2_ID)
+
+ job.cancel()
+ }
+
+ @Test
+ fun testConnectionRepository_validSubId_isCached() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptionsFlow.launchIn(this)
+
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+ val repo2 = underTest.getRepoForSubId(SUB_1_ID)
+
+ assertThat(repo1).isSameInstanceAs(repo2)
+
+ job.cancel()
+ }
+
+ @Test
+ fun testConnectionCache_clearsInvalidSubscriptions() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptionsFlow.launchIn(this)
+
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_2))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ // Get repos to trigger caching
+ val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+ val repo2 = underTest.getRepoForSubId(SUB_2_ID)
+
+ assertThat(underTest.getSubIdRepoCache())
+ .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2)
+
+ // SUB_2 disappears
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1)
+
+ job.cancel()
+ }
+
+ @Test
+ fun testConnectionRepository_invalidSubId_throws() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptionsFlow.launchIn(this)
+
+ assertThrows(IllegalArgumentException::class.java) {
+ underTest.getRepoForSubId(SUB_1_ID)
+ }
+
+ job.cancel()
+ }
+
+ private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
+ val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
+ verify(subscriptionManager)
+ .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
+ return callbackCaptor.value!!
+ }
+
+ private fun getTelephonyCallbacks(): List<TelephonyCallback> {
+ val callbackCaptor = argumentCaptor<TelephonyCallback>()
+ verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
+ return callbackCaptor.allValues
+ }
+
+ private inline fun <reified T> getTelephonyCallbackForType(): T {
+ val cbs = getTelephonyCallbacks().filterIsInstance<T>()
+ assertThat(cbs.size).isEqualTo(1)
+ return cbs[0]
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ private const val SUB_1_ID = 1
+ private val SUB_1 =
+ mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+ private const val SUB_2_ID = 2
+ private val SUB_2 =
+ mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt
deleted file mode 100644
index 316b795..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt
+++ /dev/null
@@ -1,360 +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.statusbar.pipeline.mobile.data.repository
-
-import android.telephony.CellSignalStrengthCdma
-import android.telephony.ServiceState
-import android.telephony.SignalStrength
-import android.telephony.SubscriptionInfo
-import android.telephony.SubscriptionManager
-import android.telephony.TelephonyCallback
-import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
-import android.telephony.TelephonyCallback.CarrierNetworkListener
-import android.telephony.TelephonyCallback.DataActivityListener
-import android.telephony.TelephonyCallback.DataConnectionStateListener
-import android.telephony.TelephonyCallback.DisplayInfoListener
-import android.telephony.TelephonyCallback.ServiceStateListener
-import android.telephony.TelephonyCallback.SignalStrengthsListener
-import android.telephony.TelephonyDisplayInfo
-import android.telephony.TelephonyManager
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-class MobileSubscriptionRepositoryTest : SysuiTestCase() {
- private lateinit var underTest: MobileSubscriptionRepositoryImpl
-
- @Mock private lateinit var subscriptionManager: SubscriptionManager
- @Mock private lateinit var telephonyManager: TelephonyManager
- private val scope = CoroutineScope(IMMEDIATE)
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
-
- underTest =
- MobileSubscriptionRepositoryImpl(
- subscriptionManager,
- telephonyManager,
- IMMEDIATE,
- scope,
- )
- }
-
- @After
- fun tearDown() {
- scope.cancel()
- }
-
- @Test
- fun testSubscriptions_initiallyEmpty() =
- runBlocking(IMMEDIATE) {
- assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>())
- }
-
- @Test
- fun testSubscriptions_listUpdates() =
- runBlocking(IMMEDIATE) {
- var latest: List<SubscriptionInfo>? = null
-
- val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
-
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_2))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
-
- job.cancel()
- }
-
- @Test
- fun testSubscriptions_removingSub_updatesList() =
- runBlocking(IMMEDIATE) {
- var latest: List<SubscriptionInfo>? = null
-
- val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
-
- // WHEN 2 networks show up
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_2))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- // WHEN one network is removed
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_2))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- // THEN the subscriptions list represents the newest change
- assertThat(latest).isEqualTo(listOf(SUB_2))
-
- job.cancel()
- }
-
- @Test
- fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
- runBlocking(IMMEDIATE) {
- assertThat(underTest.activeMobileDataSubscriptionId.value)
- .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
- }
-
- @Test
- fun testActiveDataSubscriptionId_updates() =
- runBlocking(IMMEDIATE) {
- var active: Int? = null
-
- val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this)
-
- getActiveDataSubscriptionCallback().onActiveDataSubscriptionIdChanged(SUB_2_ID)
-
- assertThat(active).isEqualTo(SUB_2_ID)
-
- job.cancel()
- }
-
- @Test
- fun testFlowForSubId_default() =
- runBlocking(IMMEDIATE) {
- whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
- var latest: MobileSubscriptionModel? = null
- val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
- assertThat(latest).isEqualTo(MobileSubscriptionModel())
-
- job.cancel()
- }
-
- @Test
- fun testFlowForSubId_emergencyOnly() =
- runBlocking(IMMEDIATE) {
- whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
- var latest: MobileSubscriptionModel? = null
- val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
- val serviceState = ServiceState()
- serviceState.isEmergencyOnly = true
-
- getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
-
- assertThat(latest?.isEmergencyOnly).isEqualTo(true)
-
- job.cancel()
- }
-
- @Test
- fun testFlowForSubId_emergencyOnly_toggles() =
- runBlocking(IMMEDIATE) {
- whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
- var latest: MobileSubscriptionModel? = null
- val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
- val callback = getTelephonyCallbackForType<ServiceStateListener>()
- val serviceState = ServiceState()
- serviceState.isEmergencyOnly = true
- callback.onServiceStateChanged(serviceState)
- serviceState.isEmergencyOnly = false
- callback.onServiceStateChanged(serviceState)
-
- assertThat(latest?.isEmergencyOnly).isEqualTo(false)
-
- job.cancel()
- }
-
- @Test
- fun testFlowForSubId_signalStrengths_levelsUpdate() =
- runBlocking(IMMEDIATE) {
- whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
- var latest: MobileSubscriptionModel? = null
- val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
- val callback = getTelephonyCallbackForType<SignalStrengthsListener>()
- val strength = signalStrength(1, 2, true)
- callback.onSignalStrengthsChanged(strength)
-
- assertThat(latest?.isGsm).isEqualTo(true)
- assertThat(latest?.primaryLevel).isEqualTo(1)
- assertThat(latest?.cdmaLevel).isEqualTo(2)
-
- job.cancel()
- }
-
- @Test
- fun testFlowForSubId_dataConnectionState() =
- runBlocking(IMMEDIATE) {
- whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
- var latest: MobileSubscriptionModel? = null
- val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
- val callback = getTelephonyCallbackForType<DataConnectionStateListener>()
- callback.onDataConnectionStateChanged(100, 200 /* unused */)
-
- assertThat(latest?.dataConnectionState).isEqualTo(100)
-
- job.cancel()
- }
-
- @Test
- fun testFlowForSubId_dataActivity() =
- runBlocking(IMMEDIATE) {
- whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
- var latest: MobileSubscriptionModel? = null
- val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
- val callback = getTelephonyCallbackForType<DataActivityListener>()
- callback.onDataActivity(3)
-
- assertThat(latest?.dataActivityDirection).isEqualTo(3)
-
- job.cancel()
- }
-
- @Test
- fun testFlowForSubId_carrierNetworkChange() =
- runBlocking(IMMEDIATE) {
- whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
- var latest: MobileSubscriptionModel? = null
- val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
- val callback = getTelephonyCallbackForType<CarrierNetworkListener>()
- callback.onCarrierNetworkChange(true)
-
- assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true)
-
- job.cancel()
- }
-
- @Test
- fun testFlowForSubId_displayInfo() =
- runBlocking(IMMEDIATE) {
- whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
- var latest: MobileSubscriptionModel? = null
- val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
- val callback = getTelephonyCallbackForType<DisplayInfoListener>()
- val ti = mock<TelephonyDisplayInfo>()
- callback.onDisplayInfoChanged(ti)
-
- assertThat(latest?.displayInfo).isEqualTo(ti)
-
- job.cancel()
- }
-
- @Test
- fun testFlowForSubId_isCached() =
- runBlocking(IMMEDIATE) {
- whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
- val state1 = underTest.getFlowForSubId(SUB_1_ID)
- val state2 = underTest.getFlowForSubId(SUB_1_ID)
-
- assertThat(state1).isEqualTo(state2)
- }
-
- @Test
- fun testFlowForSubId_isRemovedAfterFinish() =
- runBlocking(IMMEDIATE) {
- whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
- var latest: MobileSubscriptionModel? = null
-
- // Start collecting on some flow
- val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
- // There should be once cached flow now
- assertThat(underTest.getSubIdFlowCache().size).isEqualTo(1)
-
- // When the job is canceled, the cache should be cleared
- job.cancel()
-
- assertThat(underTest.getSubIdFlowCache().size).isEqualTo(0)
- }
-
- private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
- val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
- verify(subscriptionManager)
- .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
- return callbackCaptor.value!!
- }
-
- private fun getActiveDataSubscriptionCallback(): ActiveDataSubscriptionIdListener =
- getTelephonyCallbackForType()
-
- private fun getTelephonyCallbacks(): List<TelephonyCallback> {
- val callbackCaptor = argumentCaptor<TelephonyCallback>()
- verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
- return callbackCaptor.allValues
- }
-
- private inline fun <reified T> getTelephonyCallbackForType(): T {
- val cbs = getTelephonyCallbacks().filterIsInstance<T>()
- assertThat(cbs.size).isEqualTo(1)
- return cbs[0]
- }
-
- /** Convenience constructor for SignalStrength */
- private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
- val signalStrength = mock<SignalStrength>()
- whenever(signalStrength.isGsm).thenReturn(isGsm)
- whenever(signalStrength.level).thenReturn(gsmLevel)
- val cdmaStrength =
- mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
- whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
- .thenReturn(listOf(cdmaStrength))
-
- return signalStrength
- }
-
- companion object {
- private val IMMEDIATE = Dispatchers.Main.immediate
- private const val SUB_1_ID = 1
- private val SUB_1 =
- mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
-
- private const val SUB_2_ID = 2
- private val SUB_2 =
- mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index 8ec68f3..cd4dbeb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -23,7 +23,7 @@
class FakeMobileIconInteractor : MobileIconInteractor {
private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN)
- override val iconGroup = _iconGroup
+ override val networkTypeIconGroup = _iconGroup
private val _isEmergencyOnly = MutableStateFlow<Boolean>(false)
override val isEmergencyOnly = _isEmergencyOnly
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
new file mode 100644
index 0000000..2bd2286
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO
+import android.telephony.TelephonyManager.NETWORK_TYPE_GSM
+import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
+import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) :
+ MobileIconsInteractor {
+ val THREE_G_KEY = mobileMappings.toIconKey(THREE_G)
+ val LTE_KEY = mobileMappings.toIconKey(LTE)
+ val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G)
+ val FIVE_G_OVERRIDE_KEY = mobileMappings.toIconKeyOverride(FIVE_G_OVERRIDE)
+
+ /**
+ * To avoid a reliance on [MobileMappings], we'll build a simpler map from network type to
+ * mobile icon. See TelephonyManager.NETWORK_TYPES for a list of types and [TelephonyIcons] for
+ * the exhaustive set of icons
+ */
+ val TEST_MAPPING: Map<String, MobileIconGroup> =
+ mapOf(
+ THREE_G_KEY to TelephonyIcons.THREE_G,
+ LTE_KEY to TelephonyIcons.LTE,
+ FOUR_G_KEY to TelephonyIcons.FOUR_G,
+ FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G,
+ )
+
+ private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf())
+ override val filteredSubscriptions = _filteredSubscriptions
+
+ private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
+ override val defaultMobileIconMapping = _defaultMobileIconMapping
+
+ private val _defaultMobileIconGroup = MutableStateFlow(DEFAULT_ICON)
+ override val defaultMobileIconGroup = _defaultMobileIconGroup
+
+ private val _isUserSetup = MutableStateFlow(true)
+ override val isUserSetup = _isUserSetup
+
+ /** Always returns a new fake interactor */
+ override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor {
+ return FakeMobileIconInteractor()
+ }
+
+ companion object {
+ val DEFAULT_ICON = TelephonyIcons.G
+
+ // Use [MobileMappings] to define some simple definitions
+ const val THREE_G = NETWORK_TYPE_GSM
+ const val LTE = NETWORK_TYPE_LTE
+ const val FOUR_G = NETWORK_TYPE_UMTS
+ const val FIVE_G_OVERRIDE = OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 2f07d9c..ff44af4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -18,10 +18,19 @@
import android.telephony.CellSignalStrength
import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import androidx.test.filters.SmallTest
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.THREE_G
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -29,26 +38,33 @@
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
import org.junit.Before
import org.junit.Test
@SmallTest
class MobileIconInteractorTest : SysuiTestCase() {
private lateinit var underTest: MobileIconInteractor
- private val mobileSubscriptionRepository = FakeMobileSubscriptionRepository()
- private val sub1Flow = mobileSubscriptionRepository.getFlowForSubId(SUB_1_ID)
+ private val mobileMappingsProxy = FakeMobileMappingsProxy()
+ private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy)
+ private val connectionRepository = FakeMobileConnectionRepository()
@Before
fun setUp() {
- underTest = MobileIconInteractorImpl(sub1Flow)
+ underTest =
+ MobileIconInteractorImpl(
+ mobileIconsInteractor.defaultMobileIconMapping,
+ mobileIconsInteractor.defaultMobileIconGroup,
+ mobileMappingsProxy,
+ connectionRepository,
+ )
}
@Test
fun gsm_level_default_unknown() =
runBlocking(IMMEDIATE) {
- mobileSubscriptionRepository.setMobileSubscriptionModel(
+ connectionRepository.setMobileSubscriptionModel(
MobileSubscriptionModel(isGsm = true),
- SUB_1_ID
)
var latest: Int? = null
@@ -62,13 +78,12 @@
@Test
fun gsm_usesGsmLevel() =
runBlocking(IMMEDIATE) {
- mobileSubscriptionRepository.setMobileSubscriptionModel(
+ connectionRepository.setMobileSubscriptionModel(
MobileSubscriptionModel(
isGsm = true,
primaryLevel = GSM_LEVEL,
cdmaLevel = CDMA_LEVEL
),
- SUB_1_ID
)
var latest: Int? = null
@@ -82,9 +97,8 @@
@Test
fun cdma_level_default_unknown() =
runBlocking(IMMEDIATE) {
- mobileSubscriptionRepository.setMobileSubscriptionModel(
+ connectionRepository.setMobileSubscriptionModel(
MobileSubscriptionModel(isGsm = false),
- SUB_1_ID
)
var latest: Int? = null
@@ -97,13 +111,12 @@
@Test
fun cdma_usesCdmaLevel() =
runBlocking(IMMEDIATE) {
- mobileSubscriptionRepository.setMobileSubscriptionModel(
+ connectionRepository.setMobileSubscriptionModel(
MobileSubscriptionModel(
isGsm = false,
primaryLevel = GSM_LEVEL,
cdmaLevel = CDMA_LEVEL
),
- SUB_1_ID
)
var latest: Int? = null
@@ -114,6 +127,75 @@
job.cancel()
}
+ @Test
+ fun iconGroup_three_g() =
+ runBlocking(IMMEDIATE) {
+ connectionRepository.setMobileSubscriptionModel(
+ MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)),
+ )
+
+ var latest: MobileIconGroup? = null
+ val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(TelephonyIcons.THREE_G)
+
+ job.cancel()
+ }
+
+ @Test
+ fun iconGroup_updates_on_change() =
+ runBlocking(IMMEDIATE) {
+ connectionRepository.setMobileSubscriptionModel(
+ MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)),
+ )
+
+ var latest: MobileIconGroup? = null
+ val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+ connectionRepository.setMobileSubscriptionModel(
+ MobileSubscriptionModel(
+ resolvedNetworkType = DefaultNetworkType(FOUR_G),
+ ),
+ )
+ yield()
+
+ assertThat(latest).isEqualTo(TelephonyIcons.FOUR_G)
+
+ job.cancel()
+ }
+
+ @Test
+ fun iconGroup_5g_override_type() =
+ runBlocking(IMMEDIATE) {
+ connectionRepository.setMobileSubscriptionModel(
+ MobileSubscriptionModel(resolvedNetworkType = OverrideNetworkType(FIVE_G_OVERRIDE)),
+ )
+
+ var latest: MobileIconGroup? = null
+ val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(TelephonyIcons.NR_5G)
+
+ job.cancel()
+ }
+
+ @Test
+ fun iconGroup_default_if_no_lookup() =
+ runBlocking(IMMEDIATE) {
+ connectionRepository.setMobileSubscriptionModel(
+ MobileSubscriptionModel(
+ resolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN),
+ ),
+ )
+
+ var latest: MobileIconGroup? = null
+ val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(FakeMobileIconsInteractor.DEFAULT_ICON)
+
+ job.cancel()
+ }
+
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
@@ -123,9 +205,5 @@
private const val SUB_1_ID = 1
private val SUB_1 =
mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
-
- private const val SUB_2_ID = 2
- private val SUB_2 =
- mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 89ad9cb..b01efd1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -19,12 +19,14 @@
import android.telephony.SubscriptionInfo
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -39,7 +41,9 @@
class MobileIconsInteractorTest : SysuiTestCase() {
private lateinit var underTest: MobileIconsInteractor
private val userSetupRepository = FakeUserSetupRepository()
- private val subscriptionsRepository = FakeMobileSubscriptionRepository()
+ private val subscriptionsRepository = FakeMobileConnectionsRepository()
+ private val mobileMappingsProxy = FakeMobileMappingsProxy()
+ private val scope = CoroutineScope(IMMEDIATE)
@Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
@@ -47,10 +51,12 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
underTest =
- MobileIconsInteractor(
+ MobileIconsInteractorImpl(
subscriptionsRepository,
carrierConfigTracker,
+ mobileMappingsProxy,
userSetupRepository,
+ scope
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
new file mode 100644
index 0000000..6d8d902
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.util
+
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.settingslib.mobile.TelephonyIcons
+
+class FakeMobileMappingsProxy : MobileMappingsProxy {
+ private var iconMap = mapOf<String, MobileIconGroup>()
+ private var defaultIcons = TelephonyIcons.THREE_G
+
+ fun setIconMap(map: Map<String, MobileIconGroup>) {
+ iconMap = map
+ }
+ override fun mapIconSets(config: Config): Map<String, MobileIconGroup> = iconMap
+ fun getIconMap() = iconMap
+
+ fun setDefaultIcons(group: MobileIconGroup) {
+ defaultIcons = group
+ }
+ override fun getDefaultIcons(config: Config): MobileIconGroup = defaultIcons
+ fun getDefaultIcons(): MobileIconGroup = defaultIcons
+
+ override fun toIconKey(networkType: Int): String {
+ return networkType.toString()
+ }
+
+ override fun toIconKeyOverride(networkType: Int): String {
+ return toIconKey(networkType) + "_override"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
index 0e75c74..b32058f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
@@ -22,7 +22,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogcatEchoTracker
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
index 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 929e529..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.statusbar.connectivity.WifiIcons
+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,26 +142,24 @@
} 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,
/** A function that, given a context, calculates the correct content description string. */
val contentDescription: (Context) -> String,
- )
+
+ /** A human-readable description used for the test names. */
+ val description: String,
+ ) {
+ override fun toString() = description
+ }
// Note: We use default values for the boolean parameters to reflect a "typical configuration"
// for wifi. This allows each TestCase to only define the parameter values that are critical
@@ -154,16 +169,27 @@
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. */
val expected: Expected?
- )
+ ) {
+ override fun toString(): String {
+ return "when INPUT(enabled=$enabled, " +
+ "forceHidden=$forceHidden, " +
+ "showWhenEnabled=$alwaysShowIconWhenEnabled, " +
+ "hasDataCaps=$hasDataCapabilities, " +
+ "isDefault=$isDefault, " +
+ "network=$network) then " +
+ "EXPECTED($expected)"
+ }
+ }
companion object {
- @Parameters(name = "{0}")
- @JvmStatic
- fun data(): Collection<TestCase> =
+ @Parameters(name = "{0}") @JvmStatic fun data(): Collection<TestCase> = testData
+
+ private val testData: List<TestCase> =
listOf(
// Enabled = false => no networks shown
TestCase(
@@ -215,11 +241,12 @@
network = WifiNetworkModel.Inactive,
expected =
Expected(
- iconResource = WifiIcons.WIFI_NO_NETWORK,
+ iconResource = WIFI_NO_NETWORK,
contentDescription = { context ->
"${context.getString(WIFI_NO_CONNECTION)}," +
context.getString(NO_INTERNET)
- }
+ },
+ description = "No network icon",
),
),
TestCase(
@@ -231,7 +258,8 @@
contentDescription = { context ->
"${context.getString(WIFI_CONNECTION_STRENGTH[4])}," +
context.getString(NO_INTERNET)
- }
+ },
+ description = "No internet level 4 icon",
),
),
TestCase(
@@ -242,7 +270,8 @@
iconResource = WIFI_FULL_ICONS[2],
contentDescription = { context ->
context.getString(WIFI_CONNECTION_STRENGTH[2])
- }
+ },
+ description = "Full internet level 2 icon",
),
),
@@ -252,11 +281,12 @@
network = WifiNetworkModel.Inactive,
expected =
Expected(
- iconResource = WifiIcons.WIFI_NO_NETWORK,
+ iconResource = WIFI_NO_NETWORK,
contentDescription = { context ->
"${context.getString(WIFI_NO_CONNECTION)}," +
context.getString(NO_INTERNET)
- }
+ },
+ description = "No network icon",
),
),
TestCase(
@@ -268,7 +298,8 @@
contentDescription = { context ->
"${context.getString(WIFI_CONNECTION_STRENGTH[2])}," +
context.getString(NO_INTERNET)
- }
+ },
+ description = "No internet level 2 icon",
),
),
TestCase(
@@ -279,7 +310,48 @@
iconResource = WIFI_FULL_ICONS[0],
contentDescription = { context ->
context.getString(WIFI_CONNECTION_STRENGTH[0])
- }
+ },
+ description = "Full internet level 0 icon",
+ ),
+ ),
+
+ // 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",
),
),
@@ -309,7 +381,8 @@
iconResource = WIFI_FULL_ICONS[4],
contentDescription = { context ->
context.getString(WIFI_CONNECTION_STRENGTH[4])
- }
+ },
+ description = "Full internet level 4 icon",
),
),
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/statusbar/policy/UserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt
index 76ecc1c..169f4fb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt
@@ -57,14 +57,18 @@
import com.android.systemui.shade.NotificationShadeWindowView
import com.android.systemui.telephony.TelephonyListenerManager
import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
+import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
@@ -123,7 +127,7 @@
private val ownerId = UserHandle.USER_SYSTEM
private val ownerInfo = UserInfo(ownerId, "Owner", null,
UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL or UserInfo.FLAG_INITIALIZED or
- UserInfo.FLAG_PRIMARY or UserInfo.FLAG_SYSTEM,
+ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_SYSTEM or UserInfo.FLAG_ADMIN,
UserManager.USER_TYPE_FULL_SYSTEM)
private val guestId = 1234
private val guestInfo = UserInfo(guestId, "Guest", null,
@@ -597,6 +601,76 @@
}
@Test
+ fun testCanManageUser_userSwitcherEnabled_addUserWhenLocked() {
+ `when`(
+ globalSettings.getIntForUser(
+ eq(Settings.Global.USER_SWITCHER_ENABLED),
+ anyInt(),
+ eq(UserHandle.USER_SYSTEM)
+ )
+ ).thenReturn(1)
+
+ `when`(
+ globalSettings.getIntForUser(
+ eq(Settings.Global.ADD_USERS_WHEN_LOCKED),
+ anyInt(),
+ eq(UserHandle.USER_SYSTEM)
+ )
+ ).thenReturn(1)
+ setupController()
+ assertTrue(userSwitcherController.canManageUsers())
+ }
+
+ @Test
+ fun testCanManageUser_userSwitcherDisabled_addUserWhenLocked() {
+ `when`(
+ globalSettings.getIntForUser(
+ eq(Settings.Global.USER_SWITCHER_ENABLED),
+ anyInt(),
+ eq(UserHandle.USER_SYSTEM)
+ )
+ ).thenReturn(0)
+
+ `when`(
+ globalSettings.getIntForUser(
+ eq(Settings.Global.ADD_USERS_WHEN_LOCKED),
+ anyInt(),
+ eq(UserHandle.USER_SYSTEM)
+ )
+ ).thenReturn(1)
+ setupController()
+ assertFalse(userSwitcherController.canManageUsers())
+ }
+
+ @Test
+ fun testCanManageUser_userSwitcherEnabled_isAdmin() {
+ `when`(
+ globalSettings.getIntForUser(
+ eq(Settings.Global.USER_SWITCHER_ENABLED),
+ anyInt(),
+ eq(UserHandle.USER_SYSTEM)
+ )
+ ).thenReturn(1)
+
+ setupController()
+ assertTrue(userSwitcherController.canManageUsers())
+ }
+
+ @Test
+ fun testCanManageUser_userSwitcherDisabled_isAdmin() {
+ `when`(
+ globalSettings.getIntForUser(
+ eq(Settings.Global.USER_SWITCHER_ENABLED),
+ anyInt(),
+ eq(UserHandle.USER_SYSTEM)
+ )
+ ).thenReturn(0)
+
+ setupController()
+ assertFalse(userSwitcherController.canManageUsers())
+ }
+
+ @Test
fun addUserSwitchCallback() {
val broadcastReceiverCaptor = argumentCaptor<BroadcastReceiver>()
verify(broadcastDispatcher).registerReceiver(
@@ -632,4 +706,22 @@
bgExecutor.runAllReady()
verify(userManager).createGuest(context)
}
+
+ @Test
+ fun onUserItemClicked_manageUsers() {
+ val manageUserRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ ownerId,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ isRestricted = false,
+ isSwitchToEnabled = true
+ )
+
+ userSwitcherController.onUserListItemClicked(manageUserRecord, null)
+ val intentCaptor = kotlinArgumentCaptor<Intent>()
+ verify(activityStarter).startActivity(intentCaptor.capture(),
+ eq(true)
+ )
+ Truth.assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
index c4abedd..b68eb88 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -63,8 +63,6 @@
@Mock
private lateinit var powerManager: PowerManager
- private var shouldIgnoreViewRemoval: Boolean = false
-
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@@ -209,26 +207,6 @@
verify(windowManager, never()).removeView(any())
}
- @Test
- fun removeView_shouldIgnoreRemovalFalse_viewRemoved() {
- shouldIgnoreViewRemoval = false
- underTest.displayView(getState())
-
- underTest.removeView("reason")
-
- verify(windowManager).removeView(any())
- }
-
- @Test
- fun removeView_shouldIgnoreRemovalTrue_viewNotRemoved() {
- shouldIgnoreViewRemoval = true
- underTest.displayView(getState())
-
- underTest.removeView("reason")
-
- verify(windowManager, never()).removeView(any())
- }
-
private fun getState(name: String = "name") = ViewInfo(name)
private fun getConfigurationListener(): ConfigurationListener {
@@ -253,7 +231,7 @@
accessibilityManager,
configurationController,
powerManager,
- R.layout.media_ttt_chip,
+ R.layout.chipbar,
"Window Title",
"WAKE_REASON",
) {
@@ -267,10 +245,6 @@
mostRecentViewInfo = newInfo
}
- override fun shouldIgnoreViewRemoval(info: ViewInfo, removalReason: String): Boolean {
- return shouldIgnoreViewRemoval
- }
-
override fun getTouchableRegion(view: View, outRect: Rect) {
outRect.setEmpty()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
index c9f2b4d..13e9f60 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
@@ -19,9 +19,9 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
new file mode 100644
index 0000000..9fbf159
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -0,0 +1,352 @@
+/*
+ * 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.temporarydisplay.chipbar
+
+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
+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.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.time.FakeSystemClock
+import com.android.systemui.util.view.ViewUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class ChipbarCoordinatorTest : SysuiTestCase() {
+ private lateinit var underTest: FakeChipbarCoordinator
+
+ @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
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
+
+ fakeClock = FakeSystemClock()
+ fakeExecutor = FakeExecutor(fakeClock)
+
+ uiEventLoggerFake = UiEventLoggerFake()
+
+ underTest =
+ FakeChipbarCoordinator(
+ context,
+ logger,
+ windowManager,
+ fakeExecutor,
+ accessibilityManager,
+ configurationController,
+ powerManager,
+ falsingManager,
+ falsingCollector,
+ viewUtil,
+ vibratorHelper,
+ )
+ underTest.start()
+ }
+
+ @Test
+ fun displayView_loadedIcon_correctlyRendered() {
+ val drawable = context.getDrawable(R.drawable.ic_celebration)!!
+
+ underTest.displayView(
+ ChipbarInfo(
+ Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
+ Text.Loaded("text"),
+ endItem = null,
+ )
+ )
+
+ val iconView = getChipbarView().getStartIconView()
+ assertThat(iconView.drawable).isEqualTo(drawable)
+ assertThat(iconView.contentDescription).isEqualTo("loadedCD")
+ }
+
+ @Test
+ 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,
+ )
+ )
+
+ val iconView = getChipbarView().getStartIconView()
+ assertThat(iconView.contentDescription)
+ .isEqualTo(contentDescription.loadContentDescription(context))
+ }
+
+ @Test
+ fun displayView_loadedText_correctlyRendered() {
+ underTest.displayView(
+ ChipbarInfo(
+ Icon.Resource(R.id.check_box, null),
+ Text.Loaded("display view text here"),
+ endItem = null,
+ )
+ )
+
+ assertThat(getChipbarView().getChipText()).isEqualTo("display view text here")
+ }
+
+ @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.getLoadingIcon(): View = this.requireViewById(R.id.loading)
+
+ private fun ViewGroup.getEndButton(): TextView = this.requireViewById(R.id.end_button)
+
+ private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error)
+
+ private fun getChipbarView(): ViewGroup {
+ val viewCaptor = ArgumentCaptor.forClass(View::class.java)
+ verify(windowManager).addView(viewCaptor.capture(), any())
+ return viewCaptor.value as ViewGroup
+ }
+}
+
+private const val TIMEOUT = 10000
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
new file mode 100644
index 0000000..17d4023
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.content.Context
+import android.os.PowerManager
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.view.accessibility.AccessibilityManager
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger
+import com.android.systemui.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
+
+/** A fake implementation of [ChipbarCoordinator] for testing. */
+class FakeChipbarCoordinator(
+ context: Context,
+ @MediaTttReceiverLogger logger: MediaTttLogger,
+ windowManager: WindowManager,
+ mainExecutor: DelayableExecutor,
+ accessibilityManager: AccessibilityManager,
+ configurationController: ConfigurationController,
+ powerManager: PowerManager,
+ falsingManager: FalsingManager,
+ falsingCollector: FalsingCollector,
+ viewUtil: ViewUtil,
+ vibratorHelper: VibratorHelper,
+) :
+ ChipbarCoordinator(
+ context,
+ logger,
+ windowManager,
+ mainExecutor,
+ accessibilityManager,
+ configurationController,
+ powerManager,
+ falsingManager,
+ falsingCollector,
+ viewUtil,
+ vibratorHelper,
+ ) {
+ override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ // Just bypass the animation in tests
+ onAnimationEnd.run()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
index 7e07040..e18dd3a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
@@ -25,16 +25,21 @@
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.system.ActivityManagerActivityTypeProvider
import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
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.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@@ -48,6 +53,12 @@
@Mock
private lateinit var handler: Handler
+ @Mock
+ private lateinit var rotationChangeProvider: RotationChangeProvider
+
+ @Captor
+ private lateinit var rotationListener: ArgumentCaptor<RotationListener>
+
private val foldProvider = TestFoldProvider()
private val screenOnStatusProvider = TestScreenOnStatusProvider()
private val testHingeAngleProvider = TestHingeAngleProvider()
@@ -76,6 +87,7 @@
screenOnStatusProvider,
foldProvider,
activityTypeProvider,
+ rotationChangeProvider,
context.mainExecutor,
handler
)
@@ -92,6 +104,8 @@
})
foldStateProvider.start()
+ verify(rotationChangeProvider).addCallback(capture(rotationListener))
+
whenever(handler.postDelayed(any<Runnable>(), any())).then { invocationOnMock ->
scheduledRunnable = invocationOnMock.getArgument<Runnable>(0)
scheduledRunnableDelay = invocationOnMock.getArgument<Long>(1)
@@ -372,6 +386,27 @@
assertThat(testHingeAngleProvider.isStarted).isFalse()
}
+ @Test
+ fun onRotationChanged_whileInProgress_cancelled() {
+ setFoldState(folded = false)
+ assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING)
+
+ rotationListener.value.onRotationChanged(1)
+
+ assertThat(foldUpdates).containsExactly(
+ FOLD_UPDATE_START_OPENING, FOLD_UPDATE_FINISH_HALF_OPEN)
+ }
+
+ @Test
+ fun onRotationChanged_whileNotInProgress_noUpdates() {
+ setFoldState(folded = true)
+ assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED)
+
+ rotationListener.value.onRotationChanged(1)
+
+ assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED)
+ }
+
private fun setupForegroundActivityType(isHomeActivity: Boolean?) {
whenever(activityTypeProvider.isHomeActivity).thenReturn(isHomeActivity)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt
new file mode 100644
index 0000000..85cfef7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.unfold.updates
+
+import android.testing.AndroidTestingRunner
+import android.view.IRotationWatcher
+import android.view.IWindowManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class RotationChangeProviderTest : SysuiTestCase() {
+
+ private lateinit var rotationChangeProvider: RotationChangeProvider
+
+ @Mock lateinit var windowManagerInterface: IWindowManager
+ @Mock lateinit var listener: RotationListener
+ @Captor lateinit var rotationWatcher: ArgumentCaptor<IRotationWatcher>
+ private val fakeExecutor = FakeExecutor(FakeSystemClock())
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ rotationChangeProvider =
+ RotationChangeProvider(windowManagerInterface, context, fakeExecutor)
+ rotationChangeProvider.addCallback(listener)
+ fakeExecutor.runAllReady()
+ verify(windowManagerInterface).watchRotation(rotationWatcher.capture(), anyInt())
+ }
+
+ @Test
+ fun onRotationChanged_rotationUpdated_listenerReceivesIt() {
+ sendRotationUpdate(42)
+
+ verify(listener).onRotationChanged(42)
+ }
+
+ @Test
+ fun onRotationChanged_subscribersRemoved_noRotationChangeReceived() {
+ sendRotationUpdate(42)
+ verify(listener).onRotationChanged(42)
+
+ rotationChangeProvider.removeCallback(listener)
+ fakeExecutor.runAllReady()
+ sendRotationUpdate(43)
+
+ verify(windowManagerInterface).removeRotationWatcher(any())
+ verifyNoMoreInteractions(listener)
+ }
+
+ private fun sendRotationUpdate(newRotation: Int) {
+ rotationWatcher.value.onRotationChanged(newRotation)
+ fakeExecutor.runAllReady()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
index b2cedbf..a25469b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
@@ -16,18 +16,19 @@
package com.android.systemui.unfold.util
import android.testing.AndroidTestingRunner
-import android.view.IRotationWatcher
-import android.view.IWindowManager
import android.view.Surface
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.unfold.TestUnfoldTransitionProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
-import com.android.systemui.util.mockito.any
+import com.android.systemui.unfold.updates.RotationChangeProvider
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
+import com.android.systemui.util.mockito.capture
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.clearInvocations
import org.mockito.Mockito.never
@@ -38,32 +39,26 @@
@SmallTest
class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
- @Mock
- lateinit var windowManager: IWindowManager
+ @Mock lateinit var rotationChangeProvider: RotationChangeProvider
private val sourceProvider = TestUnfoldTransitionProvider()
- @Mock
- lateinit var transitionListener: TransitionProgressListener
+ @Mock lateinit var transitionListener: TransitionProgressListener
+
+ @Captor private lateinit var rotationListenerCaptor: ArgumentCaptor<RotationListener>
lateinit var progressProvider: NaturalRotationUnfoldProgressProvider
- private val rotationWatcherCaptor =
- ArgumentCaptor.forClass(IRotationWatcher.Stub::class.java)
-
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- progressProvider = NaturalRotationUnfoldProgressProvider(
- context,
- windowManager,
- sourceProvider
- )
+ progressProvider =
+ NaturalRotationUnfoldProgressProvider(context, rotationChangeProvider, sourceProvider)
progressProvider.init()
- verify(windowManager).watchRotation(rotationWatcherCaptor.capture(), any())
+ verify(rotationChangeProvider).addCallback(capture(rotationListenerCaptor))
progressProvider.addCallback(transitionListener)
}
@@ -127,6 +122,6 @@
}
private fun onRotationChanged(rotation: Int) {
- rotationWatcherCaptor.value.onRotationChanged(rotation)
+ rotationListenerCaptor.value.onRotationChanged(rotation)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/UserSwitcherActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/UserSwitcherActivityTest.kt
deleted file mode 100644
index 3968bb7..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/user/UserSwitcherActivityTest.kt
+++ /dev/null
@@ -1,152 +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.user
-
-import android.app.Application
-import android.os.UserManager
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper.RunWithLooper
-import android.view.LayoutInflater
-import android.view.View
-import android.view.Window
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.policy.UserSwitcherController
-import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito.`when`
-import org.mockito.Mockito.any
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.doNothing
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.util.concurrent.Executor
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@RunWithLooper(setAsMainLooper = true)
-class UserSwitcherActivityTest : SysuiTestCase() {
- @Mock
- private lateinit var activity: UserSwitcherActivity
- @Mock
- private lateinit var userSwitcherController: UserSwitcherController
- @Mock
- private lateinit var broadcastDispatcher: BroadcastDispatcher
- @Mock
- private lateinit var layoutInflater: LayoutInflater
- @Mock
- private lateinit var falsingCollector: FalsingCollector
- @Mock
- private lateinit var falsingManager: FalsingManager
- @Mock
- private lateinit var userManager: UserManager
- @Mock
- private lateinit var userTracker: UserTracker
- @Mock
- private lateinit var flags: FeatureFlags
- @Mock
- private lateinit var viewModelFactoryLazy: dagger.Lazy<UserSwitcherViewModel.Factory>
- @Mock
- private lateinit var onBackDispatcher: OnBackInvokedDispatcher
- @Mock
- private lateinit var decorView: View
- @Mock
- private lateinit var window: Window
- @Mock
- private lateinit var userSwitcherRootView: UserSwitcherRootView
- @Captor
- private lateinit var onBackInvokedCallback: ArgumentCaptor<OnBackInvokedCallback>
- var isFinished = false
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- activity = spy(object : UserSwitcherActivity(
- userSwitcherController,
- broadcastDispatcher,
- falsingCollector,
- falsingManager,
- userManager,
- userTracker,
- flags,
- viewModelFactoryLazy,
- ) {
- override fun getOnBackInvokedDispatcher() = onBackDispatcher
- override fun getMainExecutor(): Executor = FakeExecutor(FakeSystemClock())
- override fun finish() {
- isFinished = true
- }
- })
- `when`(activity.window).thenReturn(window)
- `when`(window.decorView).thenReturn(decorView)
- `when`(activity.findViewById<UserSwitcherRootView>(R.id.user_switcher_root))
- .thenReturn(userSwitcherRootView)
- `when`(activity.findViewById<View>(R.id.cancel)).thenReturn(mock(View::class.java))
- `when`(activity.findViewById<View>(R.id.add)).thenReturn(mock(View::class.java))
- `when`(activity.application).thenReturn(mock(Application::class.java))
- doNothing().`when`(activity).setContentView(anyInt())
- }
-
- @Test
- fun testMaxColumns() {
- assertThat(activity.getMaxColumns(3)).isEqualTo(4)
- assertThat(activity.getMaxColumns(4)).isEqualTo(4)
- assertThat(activity.getMaxColumns(5)).isEqualTo(3)
- assertThat(activity.getMaxColumns(6)).isEqualTo(3)
- assertThat(activity.getMaxColumns(7)).isEqualTo(4)
- assertThat(activity.getMaxColumns(9)).isEqualTo(5)
- }
-
- @Test
- fun onCreate_callbackRegistration() {
- activity.createActivity()
- verify(onBackDispatcher).registerOnBackInvokedCallback(
- eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), any())
-
- activity.destroyActivity()
- verify(onBackDispatcher).unregisterOnBackInvokedCallback(any())
- }
-
- @Test
- fun onBackInvokedCallback_finishesActivity() {
- activity.createActivity()
- verify(onBackDispatcher).registerOnBackInvokedCallback(
- eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), onBackInvokedCallback.capture())
-
- onBackInvokedCallback.value.onBackInvoked()
- assertThat(isFinished).isTrue()
- }
-}
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/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
index d4b41c1..a363a03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
@@ -97,6 +97,7 @@
createUserRecord(2),
createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+ createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT),
)
)
var models: List<UserModel>? = null
@@ -176,15 +177,17 @@
createUserRecord(2),
createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+ createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT),
)
)
var models: List<UserActionModel>? = null
val job = underTest.actions.onEach { models = it }.launchIn(this)
- assertThat(models).hasSize(3)
+ assertThat(models).hasSize(4)
assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
+ assertThat(models?.get(3)).isEqualTo(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
job.cancel()
}
@@ -200,6 +203,7 @@
isAddUser = action == UserActionModel.ADD_USER,
isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
isGuest = action == UserActionModel.ENTER_GUEST_MODE,
+ isManageUsers = action == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
index 37c378c..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
@@ -202,6 +203,7 @@
fun `actions - device unlocked`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
+
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
@@ -215,6 +217,7 @@
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
@@ -276,6 +279,7 @@
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
@@ -283,7 +287,7 @@
}
@Test
- fun `actions - device locked - only guest action is shown`() =
+ fun `actions - device locked - only guest action and manage user is shown`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
@@ -293,7 +297,13 @@
var value: List<UserActionModel>? = null
val job = underTest.actions.onEach { value = it }.launchIn(this)
- assertThat(value).isEqualTo(listOf(UserActionModel.ENTER_GUEST_MODE))
+ assertThat(value)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
+ )
+ )
job.cancel()
}
@@ -307,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,
)
)
@@ -330,7 +342,7 @@
underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
val intentCaptor = kotlinArgumentCaptor<Intent>()
- verify(activityStarter).startActivity(intentCaptor.capture(), eq(false))
+ verify(activityStarter).startActivity(intentCaptor.capture(), eq(true))
assertThat(intentCaptor.value.action)
.isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER)
assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE)
@@ -342,7 +354,7 @@
underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
val intentCaptor = kotlinArgumentCaptor<Intent>()
- verify(activityStarter).startActivity(intentCaptor.capture(), eq(false))
+ verify(activityStarter).startActivity(intentCaptor.capture(), eq(true))
assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
}
@@ -561,6 +573,7 @@
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
),
)
}
@@ -705,7 +718,7 @@
name,
/* iconPath= */ "",
/* flags= */ if (isPrimary) {
- UserInfo.FLAG_PRIMARY
+ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN
} else {
0
},
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
index c3a9705..6a17c8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
@@ -64,13 +64,7 @@
@Test
fun `actions - not actionable when locked and not locked`() =
runBlocking(IMMEDIATE) {
- userRepository.setActions(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- )
- )
+ setActions()
userRepository.setActionableWhenLocked(false)
keyguardRepository.setKeyguardShowing(false)
@@ -92,13 +86,7 @@
@Test
fun `actions - actionable when locked and not locked`() =
runBlocking(IMMEDIATE) {
- userRepository.setActions(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- )
- )
+ setActions()
userRepository.setActionableWhenLocked(true)
keyguardRepository.setKeyguardShowing(false)
@@ -120,13 +108,7 @@
@Test
fun `actions - actionable when locked and locked`() =
runBlocking(IMMEDIATE) {
- userRepository.setActions(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- )
- )
+ setActions()
userRepository.setActionableWhenLocked(true)
keyguardRepository.setKeyguardShowing(true)
@@ -182,6 +164,10 @@
verify(activityStarter).startActivity(any(), anyBoolean())
}
+ private fun setActions() {
+ userRepository.setActions(UserActionModel.values().toList())
+ }
+
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index 0344e3f..c12a868 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -268,6 +268,26 @@
}
@Test
+ fun `menu actions`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(UserActionModel.values().toList())
+ var actions: List<UserActionViewModel>? = null
+ val job = underTest.menu.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions?.map { it.viewKey })
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(),
+ UserActionModel.ADD_USER.ordinal.toLong(),
+ UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(),
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(),
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
fun `isFinishRequested - finishes when user is switched`() =
runBlocking(IMMEDIATE) {
setUsers(count = 2)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt
deleted file mode 100644
index 5e09b81..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.util.collection
-
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertSame
-import org.junit.Assert.assertThrows
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class RingBufferTest : SysuiTestCase() {
-
- private val buffer = RingBuffer(5) { TestElement() }
-
- private val history = mutableListOf<TestElement>()
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- }
-
- @Test
- fun testBarelyFillBuffer() {
- fillBuffer(5)
-
- assertEquals(0, buffer[0].id)
- assertEquals(1, buffer[1].id)
- assertEquals(2, buffer[2].id)
- assertEquals(3, buffer[3].id)
- assertEquals(4, buffer[4].id)
- }
-
- @Test
- fun testPartiallyFillBuffer() {
- fillBuffer(3)
-
- assertEquals(3, buffer.size)
-
- assertEquals(0, buffer[0].id)
- assertEquals(1, buffer[1].id)
- assertEquals(2, buffer[2].id)
-
- assertThrows(IndexOutOfBoundsException::class.java) { buffer[3] }
- assertThrows(IndexOutOfBoundsException::class.java) { buffer[4] }
- }
-
- @Test
- fun testSpinBuffer() {
- fillBuffer(277)
-
- assertEquals(272, buffer[0].id)
- assertEquals(273, buffer[1].id)
- assertEquals(274, buffer[2].id)
- assertEquals(275, buffer[3].id)
- assertEquals(276, buffer[4].id)
- assertThrows(IndexOutOfBoundsException::class.java) { buffer[5] }
-
- assertEquals(5, buffer.size)
- }
-
- @Test
- fun testElementsAreRecycled() {
- fillBuffer(23)
-
- assertSame(history[4], buffer[1])
- assertSame(history[9], buffer[1])
- assertSame(history[14], buffer[1])
- assertSame(history[19], buffer[1])
- }
-
- @Test
- fun testIterator() {
- fillBuffer(13)
-
- val iterator = buffer.iterator()
-
- for (i in 0 until 5) {
- assertEquals(history[8 + i], iterator.next())
- }
- assertFalse(iterator.hasNext())
- assertThrows(NoSuchElementException::class.java) { iterator.next() }
- }
-
- @Test
- fun testForEach() {
- fillBuffer(13)
- var i = 8
-
- buffer.forEach {
- assertEquals(history[i], it)
- i++
- }
- assertEquals(13, i)
- }
-
- private fun fillBuffer(count: Int) {
- for (i in 0 until count) {
- val elem = buffer.advance()
- elem.id = history.size
- history.add(elem)
- }
- }
-}
-
-private class TestElement(var id: Int = 0) {
- override fun toString(): String {
- return "{TestElement $id}"
- }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
index 15ba672..4ca1fd3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
@@ -26,6 +26,7 @@
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Assert.assertTrue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@@ -35,6 +36,7 @@
private val serializer = IpcSerializer()
+ @Ignore("b/253046405")
@Test
fun serializeManyIncomingIpcs(): Unit = runBlocking(Dispatchers.Main.immediate) {
val processor = launch(start = CoroutineStart.LAZY) { serializer.process() }
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/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 725b1f4..0c12680 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -18,6 +18,7 @@
package com.android.systemui.keyguard.data.repository
import com.android.systemui.common.shared.model.Position
+import com.android.systemui.keyguard.shared.model.StatusBarState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -44,6 +45,9 @@
private val _dozeAmount = MutableStateFlow(0f)
override val dozeAmount: Flow<Float> = _dozeAmount
+ private val _statusBarState = MutableStateFlow(StatusBarState.SHADE)
+ override val statusBarState: Flow<StatusBarState> = _statusBarState
+
override fun isKeyguardShowing(): Boolean {
return _isKeyguardShowing.value
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
index 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/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
index a5ec0a4..5a868a4 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
@@ -20,10 +20,12 @@
import android.content.Context
import android.hardware.SensorManager
import android.os.Handler
+import android.view.IWindowManager
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.dagger.UnfoldBackground
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.updates.FoldProvider
+import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import com.android.systemui.unfold.util.UnfoldTransitionATracePrefix
@@ -39,11 +41,11 @@
*
* This component is meant to be used for places that don't use dagger. By providing those
* parameters to the factory, all dagger objects are correctly instantiated. See
- * [createUnfoldTransitionProgressProvider] for an example.
+ * [createUnfoldSharedComponent] for an example.
*/
@Singleton
@Component(modules = [UnfoldSharedModule::class])
-internal interface UnfoldSharedComponent {
+interface UnfoldSharedComponent {
@Component.Factory
interface Factory {
@@ -58,9 +60,11 @@
@BindsInstance @UnfoldMain executor: Executor,
@BindsInstance @UnfoldBackground backgroundExecutor: Executor,
@BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String,
+ @BindsInstance windowManager: IWindowManager,
@BindsInstance contentResolver: ContentResolver = context.contentResolver
): UnfoldSharedComponent
}
val unfoldTransitionProvider: Optional<UnfoldTransitionProgressProvider>
+ val rotationChangeProvider: RotationChangeProvider
}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
index 402dd84..a1ed178 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
@@ -20,6 +20,7 @@
import android.content.Context
import android.hardware.SensorManager
import android.os.Handler
+import android.view.IWindowManager
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
@@ -27,14 +28,15 @@
import java.util.concurrent.Executor
/**
- * Factory for [UnfoldTransitionProgressProvider].
+ * Factory for [UnfoldSharedComponent].
*
- * This is needed as Launcher has to create the object manually. If dagger is available, this object
- * is provided in [UnfoldSharedModule].
+ * This wraps the autogenerated factory (for discoverability), and is needed as Launcher has to
+ * create the object manually. If dagger is available, this object is provided in
+ * [UnfoldSharedModule].
*
* This should **never** be called from sysui, as the object is already provided in that process.
*/
-fun createUnfoldTransitionProgressProvider(
+fun createUnfoldSharedComponent(
context: Context,
config: UnfoldTransitionConfig,
screenStatusProvider: ScreenStatusProvider,
@@ -44,8 +46,9 @@
mainHandler: Handler,
mainExecutor: Executor,
backgroundExecutor: Executor,
- tracingTagPrefix: String
-): UnfoldTransitionProgressProvider =
+ tracingTagPrefix: String,
+ windowManager: IWindowManager,
+): UnfoldSharedComponent =
DaggerUnfoldSharedComponent.factory()
.create(
context,
@@ -57,9 +60,6 @@
mainHandler,
mainExecutor,
backgroundExecutor,
- tracingTagPrefix)
- .unfoldTransitionProvider
- .orElse(null)
- ?: throw IllegalStateException(
- "Trying to create " +
- "UnfoldTransitionProgressProvider when the transition is disabled")
+ tracingTagPrefix,
+ windowManager,
+ )
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
index d54481c..7117aaf 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
@@ -26,7 +26,8 @@
*
* onTransitionProgress callback could be called on each frame.
*
- * Use [createUnfoldTransitionProgressProvider] to create instances of this interface
+ * Use [createUnfoldSharedComponent] to create instances of this interface when dagger is not
+ * available.
*/
interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgressListener> {
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
index 19cfc80..07473b3 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
@@ -24,6 +24,7 @@
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
import com.android.systemui.unfold.updates.hinge.FULLY_CLOSED_DEGREES
import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES
import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
@@ -40,22 +41,24 @@
private val screenStatusProvider: ScreenStatusProvider,
private val foldProvider: FoldProvider,
private val activityTypeProvider: CurrentActivityTypeProvider,
+ private val rotationChangeProvider: RotationChangeProvider,
@UnfoldMain private val mainExecutor: Executor,
@UnfoldMain private val handler: Handler
) : FoldStateProvider {
private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf()
- @FoldUpdate
- private var lastFoldUpdate: Int? = null
+ @FoldUpdate private var lastFoldUpdate: Int? = null
- @FloatRange(from = 0.0, to = 180.0)
- private var lastHingeAngle: Float = 0f
+ @FloatRange(from = 0.0, to = 180.0) private var lastHingeAngle: Float = 0f
private val hingeAngleListener = HingeAngleListener()
private val screenListener = ScreenStatusListener()
private val foldStateListener = FoldStateListener()
- private val timeoutRunnable = TimeoutRunnable()
+ private val timeoutRunnable = Runnable { cancelAnimation() }
+ private val rotationListener = RotationListener {
+ if (isTransitionInProgress) cancelAnimation()
+ }
/**
* Time after which [FOLD_UPDATE_FINISH_HALF_OPEN] is emitted following a
@@ -72,6 +75,7 @@
foldProvider.registerCallback(foldStateListener, mainExecutor)
screenStatusProvider.addCallback(screenListener)
hingeAngleProvider.addCallback(hingeAngleListener)
+ rotationChangeProvider.addCallback(rotationListener)
}
override fun stop() {
@@ -79,6 +83,7 @@
foldProvider.unregisterCallback(foldStateListener)
hingeAngleProvider.removeCallback(hingeAngleListener)
hingeAngleProvider.stop()
+ rotationChangeProvider.removeCallback(rotationListener)
}
override fun addCallback(listener: FoldUpdatesListener) {
@@ -90,14 +95,15 @@
}
override val isFinishedOpening: Boolean
- get() = !isFolded &&
+ get() =
+ !isFolded &&
(lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN ||
- lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN)
+ lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN)
private val isTransitionInProgress: Boolean
get() =
lastFoldUpdate == FOLD_UPDATE_START_OPENING ||
- lastFoldUpdate == FOLD_UPDATE_START_CLOSING
+ lastFoldUpdate == FOLD_UPDATE_START_CLOSING
private fun onHingeAngle(angle: Float) {
if (DEBUG) {
@@ -168,7 +174,7 @@
private fun notifyFoldUpdate(@FoldUpdate update: Int) {
if (DEBUG) {
- Log.d(TAG, stateToString(update))
+ Log.d(TAG, update.name())
}
outputListeners.forEach { it.onFoldUpdate(update) }
lastFoldUpdate = update
@@ -185,6 +191,8 @@
handler.removeCallbacks(timeoutRunnable)
}
+ private fun cancelAnimation(): Unit = notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
+
private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener {
override fun onScreenTurnedOn() {
@@ -225,16 +233,10 @@
onHingeAngle(angle)
}
}
-
- private inner class TimeoutRunnable : Runnable {
- override fun run() {
- notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
- }
- }
}
-private fun stateToString(@FoldUpdate update: Int): String {
- return when (update) {
+fun @receiver:FoldUpdate Int.name() =
+ when (this) {
FOLD_UPDATE_START_OPENING -> "START_OPENING"
FOLD_UPDATE_START_CLOSING -> "START_CLOSING"
FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> "UNFOLDED_SCREEN_AVAILABLE"
@@ -243,15 +245,12 @@
FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED"
else -> "UNKNOWN"
}
-}
private const val TAG = "DeviceFoldProvider"
private const val DEBUG = false
/** Threshold after which we consider the device fully unfolded. */
-@VisibleForTesting
-const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
+@VisibleForTesting const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
/** Fold animation on top of apps only when the angle exceeds this threshold. */
-@VisibleForTesting
-const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60
+@VisibleForTesting const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
new file mode 100644
index 0000000..0cf8224
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.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.unfold.updates
+
+import android.content.Context
+import android.os.RemoteException
+import android.view.IRotationWatcher
+import android.view.IWindowManager
+import android.view.Surface.Rotation
+import com.android.systemui.unfold.dagger.UnfoldMain
+import com.android.systemui.unfold.util.CallbackController
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * Allows to subscribe to rotation changes.
+ *
+ * This is needed as rotation updates from [IWindowManager] are received in a binder thread, while
+ * most of the times we want them in the main one. Updates are provided for the display associated
+ * to [context].
+ */
+class RotationChangeProvider
+@Inject
+constructor(
+ private val windowManagerInterface: IWindowManager,
+ private val context: Context,
+ @UnfoldMain private val mainExecutor: Executor,
+) : CallbackController<RotationChangeProvider.RotationListener> {
+
+ private val listeners = mutableListOf<RotationListener>()
+
+ private val rotationWatcher = RotationWatcher()
+
+ override fun addCallback(listener: RotationListener) {
+ mainExecutor.execute {
+ if (listeners.isEmpty()) {
+ subscribeToRotation()
+ }
+ listeners += listener
+ }
+ }
+
+ override fun removeCallback(listener: RotationListener) {
+ mainExecutor.execute {
+ listeners -= listener
+ if (listeners.isEmpty()) {
+ unsubscribeToRotation()
+ }
+ }
+ }
+
+ private fun subscribeToRotation() {
+ try {
+ windowManagerInterface.watchRotation(rotationWatcher, context.displayId)
+ } catch (e: RemoteException) {
+ throw e.rethrowFromSystemServer()
+ }
+ }
+
+ private fun unsubscribeToRotation() {
+ try {
+ windowManagerInterface.removeRotationWatcher(rotationWatcher)
+ } catch (e: RemoteException) {
+ throw e.rethrowFromSystemServer()
+ }
+ }
+
+ /** Gets notified of rotation changes. */
+ fun interface RotationListener {
+ /** Called once rotation changes. */
+ fun onRotationChanged(@Rotation newRotation: Int)
+ }
+
+ private inner class RotationWatcher : IRotationWatcher.Stub() {
+ override fun onRotationChanged(rotation: Int) {
+ mainExecutor.execute { listeners.forEach { it.onRotationChanged(rotation) } }
+ }
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 6417db0..29194c5 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -657,25 +657,27 @@
userState.mBindingServices.removeIf(filter);
userState.mCrashedServices.removeIf(filter);
final Iterator<ComponentName> it = userState.mEnabledServices.iterator();
+ boolean anyServiceRemoved = false;
while (it.hasNext()) {
final ComponentName comp = it.next();
final String compPkg = comp.getPackageName();
if (compPkg.equals(packageName)) {
it.remove();
- // Update the enabled services setting.
- persistComponentNamesToSettingLocked(
- Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
- userState.mEnabledServices, userId);
- // Update the touch exploration granted services setting.
userState.mTouchExplorationGrantedServices.remove(comp);
- persistComponentNamesToSettingLocked(
- Settings.Secure.
- TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
- userState.mTouchExplorationGrantedServices, userId);
- onUserStateChangedLocked(userState);
- return;
+ anyServiceRemoved = true;
}
}
+ if (anyServiceRemoved) {
+ // Update the enabled services setting.
+ persistComponentNamesToSettingLocked(
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ userState.mEnabledServices, userId);
+ // Update the touch exploration granted services setting.
+ persistComponentNamesToSettingLocked(
+ Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
+ userState.mTouchExplorationGrantedServices, userId);
+ onUserStateChangedLocked(userState);
+ }
}
}
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/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index ca7fe0c..14cfce7 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -3722,21 +3722,34 @@
Slog.w(TAG, "agentDisconnected: the backup agent for " + packageName
+ " died: cancel current operations");
- // handleCancel() causes the PerformFullTransportBackupTask to go on to
- // tearDownAgentAndKill: that will unbindBackupAgent in the Activity Manager, so
- // that the package being backed up doesn't get stuck in restricted mode until the
- // backup time-out elapses.
- for (int token : mOperationStorage.operationTokensForPackage(packageName)) {
- if (MORE_DEBUG) {
- Slog.d(TAG, "agentDisconnected: will handleCancel(all) for token:"
- + Integer.toHexString(token));
+ // Offload operation cancellation off the main thread as the cancellation callbacks
+ // might call out to BackupTransport. Other operations started on the same package
+ // before the cancellation callback has executed will also be cancelled by the callback.
+ Runnable cancellationRunnable = () -> {
+ // handleCancel() causes the PerformFullTransportBackupTask to go on to
+ // tearDownAgentAndKill: that will unbindBackupAgent in the Activity Manager, so
+ // that the package being backed up doesn't get stuck in restricted mode until the
+ // backup time-out elapses.
+ for (int token : mOperationStorage.operationTokensForPackage(packageName)) {
+ if (MORE_DEBUG) {
+ Slog.d(TAG, "agentDisconnected: will handleCancel(all) for token:"
+ + Integer.toHexString(token));
+ }
+ handleCancel(token, true /* cancelAll */);
}
- handleCancel(token, true /* cancelAll */);
- }
+ };
+ getThreadForAsyncOperation(/* operationName */ "agent-disconnected",
+ cancellationRunnable).start();
+
mAgentConnectLock.notifyAll();
}
}
+ @VisibleForTesting
+ Thread getThreadForAsyncOperation(String operationName, Runnable operation) {
+ return new Thread(operation, operationName);
+ }
+
/**
* An application being installed will need a restore pass, then the {@link PackageManager} will
* need to be told when the restore is finished.
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 8d878af..fcc1ef2a 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -152,6 +152,8 @@
// flag set by resource, whether to start dream immediately upon docking even if unlocked.
private boolean mStartDreamImmediatelyOnDock = true;
+ // flag set by resource, whether to disable dreams when ambient mode suppression is enabled.
+ private boolean mDreamsDisabledByAmbientModeSuppression = false;
// flag set by resource, whether to enable Car dock launch when starting car mode.
private boolean mEnableCarDockLaunch = true;
// flag set by resource, whether to lock UI mode to the default one or not.
@@ -364,6 +366,11 @@
mStartDreamImmediatelyOnDock = startDreamImmediatelyOnDock;
}
+ @VisibleForTesting
+ void setDreamsDisabledByAmbientModeSuppression(boolean disabledByAmbientModeSuppression) {
+ mDreamsDisabledByAmbientModeSuppression = disabledByAmbientModeSuppression;
+ }
+
@Override
public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
mCurrentUser = to.getUserIdentifier();
@@ -424,6 +431,8 @@
final Resources res = context.getResources();
mStartDreamImmediatelyOnDock = res.getBoolean(
com.android.internal.R.bool.config_startDreamImmediatelyOnDock);
+ mDreamsDisabledByAmbientModeSuppression = res.getBoolean(
+ com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig);
mNightMode = res.getInteger(
com.android.internal.R.integer.config_defaultNightMode);
mDefaultUiModeType = res.getInteger(
@@ -1827,10 +1836,14 @@
// Send the new configuration.
applyConfigurationExternallyLocked();
+ final boolean dreamsSuppressed = mDreamsDisabledByAmbientModeSuppression
+ && mLocalPowerManager.isAmbientDisplaySuppressed();
+
// If we did not start a dock app, then start dreaming if appropriate.
- if (category != null && !dockAppStarted && (mStartDreamImmediatelyOnDock
- || mWindowManager.isKeyguardShowingAndNotOccluded()
- || !mPowerManager.isInteractive())) {
+ if (category != null && !dockAppStarted && !dreamsSuppressed && (
+ mStartDreamImmediatelyOnDock
+ || mWindowManager.isKeyguardShowingAndNotOccluded()
+ || !mPowerManager.isInteractive())) {
mInjector.startDreamWhenDockedIfAppropriate(getContext());
}
}
diff --git a/services/core/java/com/android/server/biometrics/log/ALSProbe.java b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
index 62f94ed..1a5f31c 100644
--- a/services/core/java/com/android/server/biometrics/log/ALSProbe.java
+++ b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
@@ -30,7 +30,10 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.sensors.BaseClientMonitor;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
/** Probe for ambient light. */
final class ALSProbe implements Probe {
@@ -47,12 +50,18 @@
private boolean mEnabled = false;
private boolean mDestroyed = false;
+ private boolean mDestroyRequested = false;
+ private boolean mDisableRequested = false;
+ private volatile NextConsumer mNextConsumer = null;
private volatile float mLastAmbientLux = -1;
private final SensorEventListener mLightSensorListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
mLastAmbientLux = event.values[0];
+ if (mNextConsumer != null) {
+ completeNextConsumer(mLastAmbientLux);
+ }
}
@Override
@@ -102,29 +111,84 @@
@Override
public synchronized void enable() {
- if (!mDestroyed) {
+ if (!mDestroyed && !mDestroyRequested) {
+ mDisableRequested = false;
enableLightSensorLoggingLocked();
}
}
@Override
public synchronized void disable() {
- if (!mDestroyed) {
+ mDisableRequested = true;
+
+ // if a final consumer is set it will call destroy/disable on the next value if requested
+ if (!mDestroyed && mNextConsumer == null) {
disableLightSensorLoggingLocked();
}
}
@Override
public synchronized void destroy() {
- disable();
- mDestroyed = true;
+ mDestroyRequested = true;
+
+ // if a final consumer is set it will call destroy/disable on the next value if requested
+ if (!mDestroyed && mNextConsumer == null) {
+ disable();
+ mDestroyed = true;
+ }
}
/** The most recent lux reading. */
- public float getCurrentLux() {
+ public float getMostRecentLux() {
return mLastAmbientLux;
}
+ /**
+ * Register a listener for the next available ALS reading, which will be reported to the given
+ * consumer even if this probe is {@link #disable()}'ed or {@link #destroy()}'ed before a value
+ * is available.
+ *
+ * This method is intended to be used for event logs that occur when the screen may be
+ * off and sampling may have been {@link #disable()}'ed. In these cases, this method will turn
+ * on the sensor (if needed), fetch & report the first value, and then destroy or disable this
+ * probe (if needed).
+ *
+ * @param consumer consumer to notify when the data is available
+ * @param handler handler for notifying the consumer, or null
+ */
+ public synchronized void awaitNextLux(@NonNull Consumer<Float> consumer,
+ @Nullable Handler handler) {
+ final NextConsumer nextConsumer = new NextConsumer(consumer, handler);
+ final float current = mLastAmbientLux;
+ if (current > 0) {
+ nextConsumer.consume(current);
+ } else if (mDestroyed) {
+ nextConsumer.consume(-1f);
+ } else if (mNextConsumer != null) {
+ mNextConsumer.add(nextConsumer);
+ } else {
+ mNextConsumer = nextConsumer;
+ enableLightSensorLoggingLocked();
+ }
+ }
+
+ private synchronized void completeNextConsumer(float value) {
+ Slog.v(TAG, "Finishing next consumer");
+
+ final NextConsumer consumer = mNextConsumer;
+ mNextConsumer = null;
+
+ if (mDestroyRequested) {
+ destroy();
+ } else if (mDisableRequested) {
+ disable();
+ }
+
+ if (consumer != null) {
+ consumer.consume(value);
+ }
+ }
+
private void enableLightSensorLoggingLocked() {
if (!mEnabled) {
mEnabled = true;
@@ -160,4 +224,30 @@
+ mLightSensorListener.hashCode());
disable();
}
+
+ private static class NextConsumer {
+ @NonNull private final Consumer<Float> mConsumer;
+ @Nullable private final Handler mHandler;
+ @NonNull private final List<NextConsumer> mOthers = new ArrayList<>();
+
+ private NextConsumer(@NonNull Consumer<Float> consumer, @Nullable Handler handler) {
+ mConsumer = consumer;
+ mHandler = handler;
+ }
+
+ public void consume(float value) {
+ if (mHandler != null) {
+ mHandler.post(() -> mConsumer.accept(value));
+ } else {
+ mConsumer.accept(value);
+ }
+ for (NextConsumer c : mOthers) {
+ c.consume(value);
+ }
+ }
+
+ public void add(NextConsumer consumer) {
+ mOthers.add(consumer);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
index d6ca8a6..27a70c5 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
@@ -62,8 +62,7 @@
/** {@see FrameworkStatsLog.BIOMETRIC_AUTHENTICATED}. */
public void authenticate(OperationContext operationContext,
int statsModality, int statsAction, int statsClient, boolean isDebug, long latency,
- int authState, boolean requireConfirmation,
- int targetUserId, float ambientLightLux) {
+ int authState, boolean requireConfirmation, int targetUserId, float ambientLightLux) {
FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_AUTHENTICATED,
statsModality,
targetUserId,
@@ -80,6 +79,16 @@
operationContext.isAod);
}
+ /** {@see FrameworkStatsLog.BIOMETRIC_AUTHENTICATED}. */
+ public void authenticate(OperationContext operationContext,
+ int statsModality, int statsAction, int statsClient, boolean isDebug, long latency,
+ int authState, boolean requireConfirmation, int targetUserId, ALSProbe alsProbe) {
+ alsProbe.awaitNextLux((ambientLightLux) -> {
+ authenticate(operationContext, statsModality, statsAction, statsClient, isDebug,
+ latency, authState, requireConfirmation, targetUserId, ambientLightLux);
+ }, null /* handler */);
+ }
+
/** {@see FrameworkStatsLog.BIOMETRIC_ENROLLED}. */
public void enroll(int statsModality, int statsAction, int statsClient,
int targetUserId, long latency, boolean enrollSuccessful, float ambientLightLux) {
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
index 02b350e..55fe854 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
@@ -220,7 +220,7 @@
+ ", RequireConfirmation: " + requireConfirmation
+ ", State: " + authState
+ ", Latency: " + latency
- + ", Lux: " + mALSProbe.getCurrentLux());
+ + ", Lux: " + mALSProbe.getMostRecentLux());
} else {
Slog.v(TAG, "Authentication latency: " + latency);
}
@@ -231,7 +231,7 @@
mSink.authenticate(operationContext, mStatsModality, mStatsAction, mStatsClient,
Utils.isDebugEnabled(context, targetUserId),
- latency, authState, requireConfirmation, targetUserId, mALSProbe.getCurrentLux());
+ latency, authState, requireConfirmation, targetUserId, mALSProbe);
}
/** Log enrollment outcome. */
@@ -245,7 +245,7 @@
+ ", User: " + targetUserId
+ ", Client: " + mStatsClient
+ ", Latency: " + latency
- + ", Lux: " + mALSProbe.getCurrentLux()
+ + ", Lux: " + mALSProbe.getMostRecentLux()
+ ", Success: " + enrollSuccessful);
} else {
Slog.v(TAG, "Enroll latency: " + latency);
@@ -256,7 +256,7 @@
}
mSink.enroll(mStatsModality, mStatsAction, mStatsClient,
- targetUserId, latency, enrollSuccessful, mALSProbe.getCurrentLux());
+ targetUserId, latency, enrollSuccessful, mALSProbe.getMostRecentLux());
}
/** Report unexpected enrollment reported by the HAL. */
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index b3f42be..a778b57 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -16,6 +16,7 @@
package com.android.server.biometrics.sensors.fingerprint.aidl;
+import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;
import android.annotation.NonNull;
@@ -57,6 +58,7 @@
import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler;
import com.android.server.biometrics.sensors.fingerprint.Udfps;
+import java.time.Clock;
import java.util.ArrayList;
import java.util.function.Supplier;
@@ -88,7 +90,9 @@
private long mWaitForAuthKeyguard;
private long mWaitForAuthBp;
private long mIgnoreAuthFor;
+ private long mSideFpsLastAcquireStartTime;
private Runnable mAuthSuccessRunnable;
+ private final Clock mClock;
FingerprintAuthenticationClient(
@NonNull Context context,
@@ -112,7 +116,8 @@
@Nullable ISidefpsController sidefpsController,
boolean allowBackgroundAuthentication,
@NonNull FingerprintSensorPropertiesInternal sensorProps,
- @NonNull Handler handler) {
+ @NonNull Handler handler,
+ @NonNull Clock clock) {
super(
context,
lazyDaemon,
@@ -154,6 +159,8 @@
mSkipWaitForPowerVendorAcquireMessage =
context.getResources().getInteger(
R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage);
+ mSideFpsLastAcquireStartTime = -1;
+ mClock = clock;
if (mSensorProps.isAnySidefpsType()) {
if (Build.isDebuggable()) {
@@ -235,8 +242,14 @@
return;
}
delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp;
- Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps waiting for power for: "
- + delay + "ms");
+
+ if (mSideFpsLastAcquireStartTime != -1) {
+ delay = Math.max(0,
+ delay - (mClock.millis() - mSideFpsLastAcquireStartTime));
+ }
+
+ Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps "
+ + "waiting for power until: " + delay + "ms");
}
if (mHandler.hasMessages(MESSAGE_FINGER_UP)) {
@@ -260,13 +273,15 @@
mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo));
super.onAcquired(acquiredInfo, vendorCode);
if (mSensorProps.isAnySidefpsType()) {
+ if (acquiredInfo == FINGERPRINT_ACQUIRED_START) {
+ mSideFpsLastAcquireStartTime = mClock.millis();
+ }
final boolean shouldLookForVendor =
mSkipWaitForPowerAcquireMessage == FINGERPRINT_ACQUIRED_VENDOR;
final boolean acquireMessageMatch = acquiredInfo == mSkipWaitForPowerAcquireMessage;
final boolean vendorMessageMatch = vendorCode == mSkipWaitForPowerVendorAcquireMessage;
final boolean ignorePowerPress =
- (acquireMessageMatch && !shouldLookForVendor) || (shouldLookForVendor
- && acquireMessageMatch && vendorMessageMatch);
+ acquireMessageMatch && (!shouldLookForVendor || vendorMessageMatch);
if (ignorePowerPress) {
Slog.d(TAG, "(sideFPS) onFingerUp");
@@ -333,6 +348,9 @@
mALSProbeCallback.getProbe().disable();
}
});
+ if (getBiometricContext().isAwake()) {
+ mALSProbeCallback.getProbe().enable();
+ }
if (session.hasContextMethods()) {
return session.getSession().authenticateWithContext(mOperationId, opContext);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index 6f6c09b..dabb2cf 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -47,6 +47,7 @@
import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.SystemClock;
import android.os.UserManager;
import android.util.Slog;
import android.util.SparseArray;
@@ -447,7 +448,8 @@
mBiometricContext, isStrongBiometric,
mTaskStackListener, mSensors.get(sensorId).getLockoutCache(),
mUdfpsOverlayController, mSidefpsController, allowBackgroundAuthentication,
- mSensors.get(sensorId).getSensorProperties(), mHandler);
+ mSensors.get(sensorId).getSensorProperties(), mHandler,
+ SystemClock.elapsedRealtimeClock());
scheduleForSensor(sensorId, client, mBiometricStateCallback);
});
}
diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java
index 17215e5..8f59ffd 100644
--- a/services/core/java/com/android/server/display/BrightnessTracker.java
+++ b/services/core/java/com/android/server/display/BrightnessTracker.java
@@ -220,6 +220,11 @@
}
private void backgroundStart(float initialBrightness) {
+ synchronized (mDataCollectionLock) {
+ if (mStarted) {
+ return;
+ }
+ }
if (DEBUG) {
Slog.d(TAG, "Background start");
}
@@ -250,6 +255,11 @@
/** Stop listening for events */
void stop() {
+ synchronized (mDataCollectionLock) {
+ if (!mStarted) {
+ return;
+ }
+ }
if (DEBUG) {
Slog.d(TAG, "Stop");
}
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 1ae37bb..687d03d 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -150,7 +150,7 @@
* <quirk>canSetBrightnessViaHwc</quirk>
* </quirks>
*
- * <autoBrightness>
+ * <autoBrightness enable="true">
* <brighteningLightDebounceMillis>
* 2000
* </brighteningLightDebounceMillis>
@@ -507,6 +507,11 @@
private long mAutoBrightnessDarkeningLightDebounce =
INVALID_AUTO_BRIGHTNESS_LIGHT_DEBOUNCE;
+ // This setting allows non-default displays to have autobrightness enabled.
+ private boolean mAutoBrightnessAvailable = false;
+ // This stores the raw value loaded from the config file - true if not written.
+ private boolean mDdcAutoBrightnessAvailable = true;
+
// Brightness Throttling data may be updated via the DeviceConfig. Here we store the original
// data, which comes from the ddc, and the current one, which may be the DeviceConfig
// overwritten value.
@@ -1119,6 +1124,10 @@
return mProximitySensor;
}
+ boolean isAutoBrightnessAvailable() {
+ return mAutoBrightnessAvailable;
+ }
+
/**
* @param quirkValue The quirk to test.
* @return {@code true} if the specified quirk is present in this configuration, {@code false}
@@ -1184,6 +1193,60 @@
return mBrightnessLevelsNits;
}
+ /**
+ * @return Default peak refresh rate of the associated display
+ */
+ public int getDefaultPeakRefreshRate() {
+ return mContext.getResources().getInteger(R.integer.config_defaultPeakRefreshRate);
+ }
+
+ /**
+ * @return Default refresh rate of the associated display
+ */
+ public int getDefaultRefreshRate() {
+ return mContext.getResources().getInteger(R.integer.config_defaultRefreshRate);
+ }
+
+ /**
+ * @return An array of lower display brightness thresholds. This, in combination with lower
+ * ambient brightness thresholds help define buckets in which the refresh rate switching is not
+ * allowed
+ */
+ public int[] getLowDisplayBrightnessThresholds() {
+ return mContext.getResources().getIntArray(
+ R.array.config_brightnessThresholdsOfPeakRefreshRate);
+ }
+
+ /**
+ * @return An array of lower ambient brightness thresholds. This, in combination with lower
+ * display brightness thresholds help define buckets in which the refresh rate switching is not
+ * allowed
+ */
+ public int[] getLowAmbientBrightnessThresholds() {
+ return mContext.getResources().getIntArray(
+ R.array.config_ambientThresholdsOfPeakRefreshRate);
+ }
+
+ /**
+ * @return An array of high display brightness thresholds. This, in combination with high
+ * ambient brightness thresholds help define buckets in which the refresh rate switching is not
+ * allowed
+ */
+ public int[] getHighDisplayBrightnessThresholds() {
+ return mContext.getResources().getIntArray(
+ R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate);
+ }
+
+ /**
+ * @return An array of high ambient brightness thresholds. This, in combination with high
+ * display brightness thresholds help define buckets in which the refresh rate switching is not
+ * allowed
+ */
+ public int[] getHighAmbientBrightnessThresholds() {
+ return mContext.getResources().getIntArray(
+ R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate);
+ }
+
@Override
public String toString() {
return "DisplayDeviceConfig{"
@@ -1271,6 +1334,8 @@
+ mAutoBrightnessDarkeningLightDebounce
+ ", mBrightnessLevelsLux= " + Arrays.toString(mBrightnessLevelsLux)
+ ", mBrightnessLevelsNits= " + Arrays.toString(mBrightnessLevelsNits)
+ + ", mDdcAutoBrightnessAvailable= " + mDdcAutoBrightnessAvailable
+ + ", mAutoBrightnessAvailable= " + mAutoBrightnessAvailable
+ "}";
}
@@ -1349,6 +1414,7 @@
loadBrightnessChangeThresholdsFromXml();
setProxSensorUnspecified();
loadAutoBrightnessConfigsFromConfigXml();
+ loadAutoBrightnessAvailableFromConfigXml();
mLoadedFrom = "<config.xml>";
}
@@ -1367,6 +1433,7 @@
setSimpleMappingStrategyValues();
loadAmbientLightSensorFromConfigXml();
setProxSensorUnspecified();
+ loadAutoBrightnessAvailableFromConfigXml();
}
private void copyUninitializedValuesFromSecondaryConfig(DisplayConfiguration defaultConfig) {
@@ -1559,9 +1626,11 @@
}
private void loadAutoBrightnessConfigValues(DisplayConfiguration config) {
- loadAutoBrightnessBrighteningLightDebounce(config.getAutoBrightness());
- loadAutoBrightnessDarkeningLightDebounce(config.getAutoBrightness());
- loadAutoBrightnessDisplayBrightnessMapping(config.getAutoBrightness());
+ final AutoBrightness autoBrightness = config.getAutoBrightness();
+ loadAutoBrightnessBrighteningLightDebounce(autoBrightness);
+ loadAutoBrightnessDarkeningLightDebounce(autoBrightness);
+ loadAutoBrightnessDisplayBrightnessMapping(autoBrightness);
+ loadEnableAutoBrightness(autoBrightness);
}
/**
@@ -1623,6 +1692,11 @@
}
}
+ private void loadAutoBrightnessAvailableFromConfigXml() {
+ mAutoBrightnessAvailable = mContext.getResources().getBoolean(
+ R.bool.config_automatic_brightness_available);
+ }
+
private void loadBrightnessMapFromConfigXml() {
// Use the config.xml mapping
final Resources res = mContext.getResources();
@@ -2263,6 +2337,20 @@
return levels;
}
+ private void loadEnableAutoBrightness(AutoBrightness autobrightness) {
+ // mDdcAutoBrightnessAvailable is initialised to true, so that we fallback to using the
+ // config.xml values if the autobrightness tag is not defined in the ddc file.
+ // Autobrightness can still be turned off globally via config_automatic_brightness_available
+ mDdcAutoBrightnessAvailable = true;
+ if (autobrightness != null) {
+ mDdcAutoBrightnessAvailable = autobrightness.getEnabled();
+ }
+
+ mAutoBrightnessAvailable = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_automatic_brightness_available)
+ && mDdcAutoBrightnessAvailable;
+ }
+
static class SensorData {
public String type;
public String name;
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index b153c1b..b8ff6ed 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1519,6 +1519,7 @@
display.setUserDisabledHdrTypes(mUserDisabledHdrTypes);
}
if (isDefault) {
+ notifyDefaultDisplayDeviceUpdated(display);
recordStableDisplayStatsIfNeededLocked(display);
recordTopInsetLocked(display);
}
@@ -1612,6 +1613,11 @@
mHandler.post(work);
}
final int displayId = display.getDisplayIdLocked();
+
+ if (displayId == Display.DEFAULT_DISPLAY) {
+ notifyDefaultDisplayDeviceUpdated(display);
+ }
+
DisplayPowerController dpc = mDisplayPowerControllers.get(displayId);
if (dpc != null) {
dpc.onDisplayChanged();
@@ -1621,6 +1627,11 @@
handleLogicalDisplayChangedLocked(display);
}
+ private void notifyDefaultDisplayDeviceUpdated(LogicalDisplay display) {
+ mDisplayModeDirector.defaultDisplayDeviceUpdated(display.getPrimaryDisplayDeviceLocked()
+ .mDisplayDeviceConfig);
+ }
+
private void handleLogicalDisplayDeviceStateTransitionLocked(@NonNull LogicalDisplay display) {
final int displayId = display.getDisplayIdLocked();
final DisplayPowerController dpc = mDisplayPowerControllers.get(displayId);
@@ -1759,9 +1770,13 @@
if (displayDevice == null) {
return;
}
- mPersistentDataStore.setUserPreferredResolution(
- displayDevice, resolutionWidth, resolutionHeight);
- mPersistentDataStore.setUserPreferredRefreshRate(displayDevice, refreshRate);
+ try {
+ mPersistentDataStore.setUserPreferredResolution(
+ displayDevice, resolutionWidth, resolutionHeight);
+ mPersistentDataStore.setUserPreferredRefreshRate(displayDevice, refreshRate);
+ } finally {
+ mPersistentDataStore.saveIfNeeded();
+ }
}
private void setUserPreferredModeForDisplayLocked(int displayId, Display.Mode mode) {
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index ac72b17..912b1b2 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -79,6 +79,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Objects;
+import java.util.concurrent.Callable;
/**
* The DisplayModeDirector is responsible for determining what modes are allowed to be automatically
@@ -153,8 +154,10 @@
mSupportedModesByDisplay = new SparseArray<>();
mDefaultModeByDisplay = new SparseArray<>();
mAppRequestObserver = new AppRequestObserver();
- mSettingsObserver = new SettingsObserver(context, handler);
mDisplayObserver = new DisplayObserver(context, handler);
+ mDeviceConfig = injector.getDeviceConfig();
+ mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings();
+ mSettingsObserver = new SettingsObserver(context, handler);
mBrightnessObserver = new BrightnessObserver(context, handler, injector);
mUdfpsObserver = new UdfpsObserver();
final BallotBox ballotBox = (displayId, priority, vote) -> {
@@ -164,10 +167,8 @@
};
mSensorObserver = new SensorObserver(context, ballotBox, injector);
mSkinThermalStatusObserver = new SkinThermalStatusObserver(injector, ballotBox);
- mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings();
mHbmObserver = new HbmObserver(injector, ballotBox, BackgroundThread.getHandler(),
mDeviceConfigDisplaySettings);
- mDeviceConfig = injector.getDeviceConfig();
mAlwaysRespectAppRequest = false;
}
@@ -518,6 +519,15 @@
}
/**
+ * A utility to make this class aware of the new display configs whenever the default display is
+ * changed
+ */
+ public void defaultDisplayDeviceUpdated(DisplayDeviceConfig displayDeviceConfig) {
+ mSettingsObserver.setRefreshRates(displayDeviceConfig);
+ mBrightnessObserver.updateBlockingZoneThresholds(displayDeviceConfig);
+ }
+
+ /**
* When enabled the app requested display mode is always selected and all
* other votes will be ignored. This is used for testing purposes.
*/
@@ -1132,10 +1142,19 @@
SettingsObserver(@NonNull Context context, @NonNull Handler handler) {
super(handler);
mContext = context;
- mDefaultPeakRefreshRate = (float) context.getResources().getInteger(
- R.integer.config_defaultPeakRefreshRate);
+ setRefreshRates(/* displayDeviceConfig= */ null);
+ }
+
+ /**
+ * This is used to update the refresh rate configs from the DeviceConfig, which
+ * if missing from DisplayDeviceConfig, and finally fallback to config.xml.
+ */
+ public void setRefreshRates(DisplayDeviceConfig displayDeviceConfig) {
+ setDefaultPeakRefreshRate(displayDeviceConfig);
mDefaultRefreshRate =
- (float) context.getResources().getInteger(R.integer.config_defaultRefreshRate);
+ (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger(
+ R.integer.config_defaultRefreshRate)
+ : (float) displayDeviceConfig.getDefaultRefreshRate();
}
public void observe() {
@@ -1196,6 +1215,23 @@
}
}
+ private void setDefaultPeakRefreshRate(DisplayDeviceConfig displayDeviceConfig) {
+ Float defaultPeakRefreshRate = null;
+ try {
+ defaultPeakRefreshRate =
+ mDeviceConfigDisplaySettings.getDefaultPeakRefreshRate();
+ } catch (Exception exception) {
+ // Do nothing
+ }
+ if (defaultPeakRefreshRate == null) {
+ defaultPeakRefreshRate =
+ (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger(
+ R.integer.config_defaultPeakRefreshRate)
+ : (float) displayDeviceConfig.getDefaultPeakRefreshRate();
+ }
+ mDefaultPeakRefreshRate = defaultPeakRefreshRate;
+ }
+
private void updateLowPowerModeSettingLocked() {
boolean inLowPowerMode = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.LOW_POWER_MODE, 0 /*default*/) != 0;
@@ -1508,12 +1544,31 @@
mContext = context;
mHandler = handler;
mInjector = injector;
+ updateBlockingZoneThresholds(/* displayDeviceConfig= */ null);
+ mRefreshRateInHighZone = context.getResources().getInteger(
+ R.integer.config_fixedRefreshRateInHighZone);
+ }
- mLowDisplayBrightnessThresholds = context.getResources().getIntArray(
- R.array.config_brightnessThresholdsOfPeakRefreshRate);
- mLowAmbientBrightnessThresholds = context.getResources().getIntArray(
- R.array.config_ambientThresholdsOfPeakRefreshRate);
+ /**
+ * This is used to update the blocking zone thresholds from the DeviceConfig, which
+ * if missing from DisplayDeviceConfig, and finally fallback to config.xml.
+ */
+ public void updateBlockingZoneThresholds(DisplayDeviceConfig displayDeviceConfig) {
+ loadLowBrightnessThresholds(displayDeviceConfig);
+ loadHighBrightnessThresholds(displayDeviceConfig);
+ }
+ private void loadLowBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig) {
+ mLowDisplayBrightnessThresholds = loadBrightnessThresholds(
+ () -> mDeviceConfigDisplaySettings.getLowDisplayBrightnessThresholds(),
+ () -> displayDeviceConfig.getLowDisplayBrightnessThresholds(),
+ R.array.config_brightnessThresholdsOfPeakRefreshRate,
+ displayDeviceConfig);
+ mLowAmbientBrightnessThresholds = loadBrightnessThresholds(
+ () -> mDeviceConfigDisplaySettings.getLowAmbientBrightnessThresholds(),
+ () -> displayDeviceConfig.getLowAmbientBrightnessThresholds(),
+ R.array.config_ambientThresholdsOfPeakRefreshRate,
+ displayDeviceConfig);
if (mLowDisplayBrightnessThresholds.length != mLowAmbientBrightnessThresholds.length) {
throw new RuntimeException("display low brightness threshold array and ambient "
+ "brightness threshold array have different length: "
@@ -1522,11 +1577,19 @@
+ ", ambientBrightnessThresholds="
+ Arrays.toString(mLowAmbientBrightnessThresholds));
}
+ }
- mHighDisplayBrightnessThresholds = context.getResources().getIntArray(
- R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate);
- mHighAmbientBrightnessThresholds = context.getResources().getIntArray(
- R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate);
+ private void loadHighBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig) {
+ mHighDisplayBrightnessThresholds = loadBrightnessThresholds(
+ () -> mDeviceConfigDisplaySettings.getHighDisplayBrightnessThresholds(),
+ () -> displayDeviceConfig.getHighDisplayBrightnessThresholds(),
+ R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate,
+ displayDeviceConfig);
+ mHighAmbientBrightnessThresholds = loadBrightnessThresholds(
+ () -> mDeviceConfigDisplaySettings.getHighAmbientBrightnessThresholds(),
+ () -> displayDeviceConfig.getHighAmbientBrightnessThresholds(),
+ R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate,
+ displayDeviceConfig);
if (mHighDisplayBrightnessThresholds.length
!= mHighAmbientBrightnessThresholds.length) {
throw new RuntimeException("display high brightness threshold array and ambient "
@@ -1536,8 +1599,32 @@
+ ", ambientBrightnessThresholds="
+ Arrays.toString(mHighAmbientBrightnessThresholds));
}
- mRefreshRateInHighZone = context.getResources().getInteger(
- R.integer.config_fixedRefreshRateInHighZone);
+ }
+
+ private int[] loadBrightnessThresholds(
+ Callable<int[]> loadFromDeviceConfigDisplaySettingsCallable,
+ Callable<int[]> loadFromDisplayDeviceConfigCallable,
+ int brightnessThresholdOfFixedRefreshRateKey,
+ DisplayDeviceConfig displayDeviceConfig) {
+ int[] brightnessThresholds = null;
+ try {
+ brightnessThresholds =
+ loadFromDeviceConfigDisplaySettingsCallable.call();
+ } catch (Exception exception) {
+ // Do nothing
+ }
+ if (brightnessThresholds == null) {
+ try {
+ brightnessThresholds =
+ (displayDeviceConfig == null) ? mContext.getResources().getIntArray(
+ brightnessThresholdOfFixedRefreshRateKey)
+ : loadFromDisplayDeviceConfigCallable.call();
+ } catch (Exception e) {
+ Slog.e(TAG, "Unexpectedly failed to load display brightness threshold");
+ e.printStackTrace();
+ }
+ }
+ return brightnessThresholds;
}
/**
@@ -1590,7 +1677,6 @@
mLowAmbientBrightnessThresholds = lowAmbientBrightnessThresholds;
}
-
int[] highDisplayBrightnessThresholds =
mDeviceConfigDisplaySettings.getHighDisplayBrightnessThresholds();
int[] highAmbientBrightnessThresholds =
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 458cf1a..95dc23f 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -558,13 +558,6 @@
mScreenBrightnessForVrRangeMinimum = clampAbsoluteBrightness(
pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM_VR));
- // Check the setting, but also verify that it is the default display. Only the default
- // display has an automatic brightness controller running.
- // TODO: b/179021925 - Fix to work with multiple displays
- mUseSoftwareAutoBrightnessConfig = resources.getBoolean(
- com.android.internal.R.bool.config_automatic_brightness_available)
- && mDisplayId == Display.DEFAULT_DISPLAY;
-
mAllowAutoBrightnessWhileDozingConfig = resources.getBoolean(
com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing);
@@ -938,6 +931,8 @@
}
private void setUpAutoBrightness(Resources resources, Handler handler) {
+ mUseSoftwareAutoBrightnessConfig = mDisplayDeviceConfig.isAutoBrightnessAvailable();
+
if (!mUseSoftwareAutoBrightnessConfig) {
return;
}
diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java
index b9a0738..a11f172 100644
--- a/services/core/java/com/android/server/display/PersistentDataStore.java
+++ b/services/core/java/com/android/server/display/PersistentDataStore.java
@@ -75,6 +75,11 @@
* </brightness-curve>
* </brightness-configuration>
* </brightness-configurations>
+ * <display-mode>0<
+ * <resolution-width>1080</resolution-width>
+ * <resolution-height>1920</resolution-height>
+ * <refresh-rate>60</refresh-rate>
+ * </display-mode>
* </display>
* </display-states>
* <stable-device-values>
@@ -121,6 +126,10 @@
private static final String ATTR_PACKAGE_NAME = "package-name";
private static final String ATTR_TIME_STAMP = "timestamp";
+ private static final String TAG_RESOLUTION_WIDTH = "resolution-width";
+ private static final String TAG_RESOLUTION_HEIGHT = "resolution-height";
+ private static final String TAG_REFRESH_RATE = "refresh-rate";
+
// Remembered Wifi display devices.
private ArrayList<WifiDisplay> mRememberedWifiDisplays = new ArrayList<WifiDisplay>();
@@ -696,6 +705,18 @@
case TAG_BRIGHTNESS_CONFIGURATIONS:
mDisplayBrightnessConfigurations.loadFromXml(parser);
break;
+ case TAG_RESOLUTION_WIDTH:
+ String width = parser.nextText();
+ mWidth = Integer.parseInt(width);
+ break;
+ case TAG_RESOLUTION_HEIGHT:
+ String height = parser.nextText();
+ mHeight = Integer.parseInt(height);
+ break;
+ case TAG_REFRESH_RATE:
+ String refreshRate = parser.nextText();
+ mRefreshRate = Float.parseFloat(refreshRate);
+ break;
}
}
}
@@ -712,6 +733,18 @@
serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
mDisplayBrightnessConfigurations.saveToXml(serializer);
serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
+
+ serializer.startTag(null, TAG_RESOLUTION_WIDTH);
+ serializer.text(Integer.toString(mWidth));
+ serializer.endTag(null, TAG_RESOLUTION_WIDTH);
+
+ serializer.startTag(null, TAG_RESOLUTION_HEIGHT);
+ serializer.text(Integer.toString(mHeight));
+ serializer.endTag(null, TAG_RESOLUTION_HEIGHT);
+
+ serializer.startTag(null, TAG_REFRESH_RATE);
+ serializer.text(Float.toString(mRefreshRate));
+ serializer.endTag(null, TAG_REFRESH_RATE);
}
public void dump(final PrintWriter pw, final String prefix) {
@@ -719,6 +752,8 @@
pw.println(prefix + "BrightnessValue=" + mBrightness);
pw.println(prefix + "DisplayBrightnessConfigurations: ");
mDisplayBrightnessConfigurations.dump(pw, prefix);
+ pw.println(prefix + "Resolution=" + mWidth + " " + mHeight);
+ pw.println(prefix + "RefreshRate=" + mRefreshRate);
}
}
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index 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 7964fd5..5589673 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -246,8 +246,8 @@
// Because napping could cause the screen to turn off immediately if the dream
// cannot be started, we keep one eye open and gently poke user activity.
long time = SystemClock.uptimeMillis();
- mPowerManager.userActivity(time, true /*noChangeLights*/);
- mPowerManager.nap(time);
+ mPowerManager.userActivity(time, /* noChangeLights= */ true);
+ mPowerManagerInternal.nap(time, /* allowWake= */ true);
}
private void requestAwakenInternal(String reason) {
@@ -493,8 +493,6 @@
return;
}
- stopDreamLocked(true /*immediate*/, "starting new dream");
-
Slog.i(TAG, "Entering dreamland.");
mCurrentDream = new DreamRecord(name, userId, isPreviewMode, canDoze);
@@ -637,7 +635,7 @@
@Nullable FileDescriptor err,
@NonNull String[] args, @Nullable ShellCallback callback,
@NonNull ResultReceiver resultReceiver) throws RemoteException {
- new DreamShellCommand(DreamManagerService.this, mPowerManager)
+ new DreamShellCommand(DreamManagerService.this)
.exec(this, in, out, err, args, callback, resultReceiver);
}
diff --git a/services/core/java/com/android/server/dreams/DreamShellCommand.java b/services/core/java/com/android/server/dreams/DreamShellCommand.java
index eae7e80..ab84ae4 100644
--- a/services/core/java/com/android/server/dreams/DreamShellCommand.java
+++ b/services/core/java/com/android/server/dreams/DreamShellCommand.java
@@ -18,10 +18,8 @@
import android.annotation.NonNull;
import android.os.Binder;
-import android.os.PowerManager;
import android.os.Process;
import android.os.ShellCommand;
-import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Slog;
@@ -34,11 +32,9 @@
private static final boolean DEBUG = true;
private static final String TAG = "DreamShellCommand";
private final @NonNull DreamManagerService mService;
- private final @NonNull PowerManager mPowerManager;
- DreamShellCommand(@NonNull DreamManagerService service, @NonNull PowerManager powerManager) {
+ DreamShellCommand(@NonNull DreamManagerService service) {
mService = service;
- mPowerManager = powerManager;
}
@Override
@@ -67,8 +63,6 @@
}
private int startDreaming() {
- mPowerManager.wakeUp(SystemClock.uptimeMillis(),
- PowerManager.WAKE_REASON_PLUGGED_IN, "shell:cmd:android.service.dreams:DREAM");
mService.requestStartDreamFromShell();
return 0;
}
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 63862e4..37f980d 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;
@@ -4873,6 +4874,13 @@
}
@Override
+ public int getHintsFromListenerNoToken() {
+ synchronized (mNotificationLock) {
+ return mListenerHints;
+ }
+ }
+
+ @Override
public void requestInterruptionFilterFromListener(INotificationListener token,
int interruptionFilter) throws RemoteException {
final long identity = Binder.clearCallingIdentity();
@@ -10646,10 +10654,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
@@ -10674,10 +10690,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/power/AmbientDisplaySuppressionController.java b/services/core/java/com/android/server/power/AmbientDisplaySuppressionController.java
index aad7b14..7440fc7 100644
--- a/services/core/java/com/android/server/power/AmbientDisplaySuppressionController.java
+++ b/services/core/java/com/android/server/power/AmbientDisplaySuppressionController.java
@@ -40,13 +40,24 @@
public class AmbientDisplaySuppressionController {
private static final String TAG = "AmbientDisplaySuppressionController";
- private final Context mContext;
private final Set<Pair<String, Integer>> mSuppressionTokens;
+ private final AmbientDisplaySuppressionChangedCallback mCallback;
private IStatusBarService mStatusBarService;
- AmbientDisplaySuppressionController(Context context) {
- mContext = requireNonNull(context);
+ /** Interface to get a list of available logical devices. */
+ interface AmbientDisplaySuppressionChangedCallback {
+ /**
+ * Called when the suppression state changes.
+ *
+ * @param isSuppressed Whether ambient is suppressed.
+ */
+ void onSuppressionChanged(boolean isSuppressed);
+ }
+
+ AmbientDisplaySuppressionController(
+ @NonNull AmbientDisplaySuppressionChangedCallback callback) {
mSuppressionTokens = Collections.synchronizedSet(new ArraySet<>());
+ mCallback = requireNonNull(callback);
}
/**
@@ -58,6 +69,7 @@
*/
public void suppress(@NonNull String token, int callingUid, boolean suppress) {
Pair<String, Integer> suppressionToken = Pair.create(requireNonNull(token), callingUid);
+ final boolean wasSuppressed = isSuppressed();
if (suppress) {
mSuppressionTokens.add(suppressionToken);
@@ -65,9 +77,14 @@
mSuppressionTokens.remove(suppressionToken);
}
+ final boolean isSuppressed = isSuppressed();
+ if (isSuppressed != wasSuppressed) {
+ mCallback.onSuppressionChanged(isSuppressed);
+ }
+
try {
synchronized (mSuppressionTokens) {
- getStatusBar().suppressAmbientDisplay(isSuppressed());
+ getStatusBar().suppressAmbientDisplay(isSuppressed);
}
} catch (RemoteException e) {
Slog.e(TAG, "Failed to suppress ambient display", e);
diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java
index 9fe53fb..431cf38 100644
--- a/services/core/java/com/android/server/power/PowerGroup.java
+++ b/services/core/java/com/android/server/power/PowerGroup.java
@@ -229,8 +229,8 @@
}
}
- boolean dreamLocked(long eventTime, int uid) {
- if (eventTime < mLastWakeTime || mWakefulness != WAKEFULNESS_AWAKE) {
+ boolean dreamLocked(long eventTime, int uid, boolean allowWake) {
+ if (eventTime < mLastWakeTime || (!allowWake && mWakefulness != WAKEFULNESS_AWAKE)) {
return false;
}
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index f200564..725fb3f 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -127,6 +127,7 @@
import com.android.server.lights.LightsManager;
import com.android.server.lights.LogicalLight;
import com.android.server.policy.WindowManagerPolicy;
+import com.android.server.power.AmbientDisplaySuppressionController.AmbientDisplaySuppressionChangedCallback;
import com.android.server.power.batterysaver.BatterySaverController;
import com.android.server.power.batterysaver.BatterySaverPolicy;
import com.android.server.power.batterysaver.BatterySaverStateMachine;
@@ -467,6 +468,9 @@
// True if the device should wake up when plugged or unplugged.
private boolean mWakeUpWhenPluggedOrUnpluggedConfig;
+ // True if the device should keep dreaming when undocked.
+ private boolean mKeepDreamingWhenUndockingConfig;
+
// True if the device should wake up when plugged or unplugged in theater mode.
private boolean mWakeUpWhenPluggedOrUnpluggedInTheaterModeConfig;
@@ -504,6 +508,9 @@
// effectively and terminate the dream. Use -1 to disable this safety feature.
private int mDreamsBatteryLevelDrainCutoffConfig;
+ // Whether dreams should be disabled when ambient mode is suppressed.
+ private boolean mDreamsDisabledByAmbientModeSuppressionConfig;
+
// True if dreams are enabled by the user.
private boolean mDreamsEnabledSetting;
@@ -961,8 +968,8 @@
}
AmbientDisplaySuppressionController createAmbientDisplaySuppressionController(
- Context context) {
- return new AmbientDisplaySuppressionController(context);
+ @NonNull AmbientDisplaySuppressionChangedCallback callback) {
+ return new AmbientDisplaySuppressionController(callback);
}
InattentiveSleepWarningController createInattentiveSleepWarningController() {
@@ -1041,7 +1048,8 @@
mConstants = new Constants(mHandler);
mAmbientDisplayConfiguration = mInjector.createAmbientDisplayConfiguration(context);
mAmbientDisplaySuppressionController =
- mInjector.createAmbientDisplaySuppressionController(context);
+ mInjector.createAmbientDisplaySuppressionController(
+ mAmbientSuppressionChangedCallback);
mAttentionDetector = new AttentionDetector(this::onUserAttention, mLock);
mFaceDownDetector = new FaceDownDetector(this::onFlip);
mScreenUndimDetector = new ScreenUndimDetector();
@@ -1374,6 +1382,8 @@
com.android.internal.R.bool.config_powerDecoupleInteractiveModeFromDisplay);
mWakeUpWhenPluggedOrUnpluggedConfig = resources.getBoolean(
com.android.internal.R.bool.config_unplugTurnsOnScreen);
+ mKeepDreamingWhenUndockingConfig = resources.getBoolean(
+ com.android.internal.R.bool.config_keepDreamingWhenUndocking);
mWakeUpWhenPluggedOrUnpluggedInTheaterModeConfig = resources.getBoolean(
com.android.internal.R.bool.config_allowTheaterModeWakeFromUnplug);
mSuspendWhenScreenOffDueToProximityConfig = resources.getBoolean(
@@ -1398,6 +1408,8 @@
com.android.internal.R.integer.config_dreamsBatteryLevelMinimumWhenNotPowered);
mDreamsBatteryLevelDrainCutoffConfig = resources.getInteger(
com.android.internal.R.integer.config_dreamsBatteryLevelDrainCutoff);
+ mDreamsDisabledByAmbientModeSuppressionConfig = resources.getBoolean(
+ com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig);
mDozeAfterScreenOff = resources.getBoolean(
com.android.internal.R.bool.config_dozeAfterScreenOffByDefault);
mMinimumScreenOffTimeoutConfig = resources.getInteger(
@@ -1919,6 +1931,13 @@
}
}
+ private void napInternal(long eventTime, int uid, boolean allowWake) {
+ synchronized (mLock) {
+ dreamPowerGroupLocked(mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP),
+ eventTime, uid, allowWake);
+ }
+ }
+
private void onUserAttention() {
synchronized (mLock) {
if (userActivityNoUpdateLocked(mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP),
@@ -2034,7 +2053,8 @@
}
@GuardedBy("mLock")
- private boolean dreamPowerGroupLocked(PowerGroup powerGroup, long eventTime, int uid) {
+ private boolean dreamPowerGroupLocked(PowerGroup powerGroup, long eventTime, int uid,
+ boolean allowWake) {
if (DEBUG_SPEW) {
Slog.d(TAG, "dreamPowerGroup: groupId=" + powerGroup.getGroupId() + ", eventTime="
+ eventTime + ", uid=" + uid);
@@ -2042,7 +2062,7 @@
if (!mBootCompleted || !mSystemReady) {
return false;
}
- return powerGroup.dreamLocked(eventTime, uid);
+ return powerGroup.dreamLocked(eventTime, uid, allowWake);
}
@GuardedBy("mLock")
@@ -2487,6 +2507,14 @@
return false;
}
+ // Don't wake when undocking while dreaming if configured not to.
+ if (mKeepDreamingWhenUndockingConfig
+ && getGlobalWakefulnessLocked() == WAKEFULNESS_DREAMING
+ && wasPowered && !mIsPowered
+ && oldPlugType == BatteryManager.BATTERY_PLUGGED_DOCK) {
+ return false;
+ }
+
// Don't wake when undocked from wireless charger.
// See WirelessChargerDetector for justification.
if (wasPowered && !mIsPowered
@@ -3086,7 +3114,8 @@
changed = sleepPowerGroupLocked(powerGroup, time,
PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE, Process.SYSTEM_UID);
} else if (shouldNapAtBedTimeLocked()) {
- changed = dreamPowerGroupLocked(powerGroup, time, Process.SYSTEM_UID);
+ changed = dreamPowerGroupLocked(powerGroup, time,
+ Process.SYSTEM_UID, /* allowWake= */ false);
} else {
changed = dozePowerGroupLocked(powerGroup, time,
PowerManager.GO_TO_SLEEP_REASON_TIMEOUT, Process.SYSTEM_UID);
@@ -3307,7 +3336,7 @@
}
// Doze has ended or will be stopped. Update the power state.
- sleepPowerGroupLocked(powerGroup, now, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT,
+ sleepPowerGroupLocked(powerGroup, now, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT,
Process.SYSTEM_UID);
}
}
@@ -3318,12 +3347,32 @@
}
}
+ @GuardedBy("mLock")
+ private void onDreamSuppressionChangedLocked(final boolean isSuppressed) {
+ if (!mDreamsDisabledByAmbientModeSuppressionConfig) {
+ return;
+ }
+ if (!isSuppressed && mIsPowered && mDreamsSupportedConfig && mDreamsEnabledSetting
+ && shouldNapAtBedTimeLocked() && isItBedTimeYetLocked(
+ mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP))) {
+ napInternal(SystemClock.uptimeMillis(), Process.SYSTEM_UID, /* allowWake= */ true);
+ } else if (isSuppressed) {
+ mDirty |= DIRTY_SETTINGS;
+ updatePowerStateLocked();
+ }
+ }
+
+
/**
* Returns true if the {@code groupId} is allowed to dream in its current state.
*/
@GuardedBy("mLock")
private boolean canDreamLocked(final PowerGroup powerGroup) {
+ final boolean dreamsSuppressed = mDreamsDisabledByAmbientModeSuppressionConfig
+ && mAmbientDisplaySuppressionController.isSuppressed();
+
if (!mBootCompleted
+ || dreamsSuppressed
|| getGlobalWakefulnessLocked() != WAKEFULNESS_DREAMING
|| !mDreamsSupportedConfig
|| !mDreamsEnabledSetting
@@ -4408,6 +4457,8 @@
+ mWakeUpWhenPluggedOrUnpluggedInTheaterModeConfig);
pw.println(" mTheaterModeEnabled="
+ mTheaterModeEnabled);
+ pw.println(" mKeepDreamingWhenUndockingConfig="
+ + mKeepDreamingWhenUndockingConfig);
pw.println(" mSuspendWhenScreenOffDueToProximityConfig="
+ mSuspendWhenScreenOffDueToProximityConfig);
pw.println(" mDreamsSupportedConfig=" + mDreamsSupportedConfig);
@@ -5013,6 +5064,16 @@
}
};
+ private final AmbientDisplaySuppressionChangedCallback mAmbientSuppressionChangedCallback =
+ new AmbientDisplaySuppressionChangedCallback() {
+ @Override
+ public void onSuppressionChanged(boolean isSuppressed) {
+ synchronized (mLock) {
+ onDreamSuppressionChangedLocked(isSuppressed);
+ }
+ }
+ };
+
/**
* Callback for asynchronous operations performed by the power manager.
*/
@@ -5701,10 +5762,7 @@
final int uid = Binder.getCallingUid();
final long ident = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- dreamPowerGroupLocked(mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP),
- eventTime, uid);
- }
+ napInternal(eventTime, uid, /* allowWake= */ false);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -6632,6 +6690,16 @@
public boolean interceptPowerKeyDown(KeyEvent event) {
return interceptPowerKeyDownInternal(event);
}
+
+ @Override
+ public void nap(long eventTime, boolean allowWake) {
+ napInternal(eventTime, Process.SYSTEM_UID, allowWake);
+ }
+
+ @Override
+ public boolean isAmbientDisplaySuppressed() {
+ return mAmbientDisplaySuppressionController.isSuppressed();
+ }
}
/**
diff --git a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java
index fd6ec06..f744d00 100644
--- a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java
+++ b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java
@@ -46,6 +46,8 @@
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener,
SensorEventListener {
@@ -275,7 +277,7 @@
public void onSensorChanged(SensorEvent event) {
// Using log space to represent human sensation (Fechner's Law) instead of lux
// because lux values causes bright flashes to skew the average very high.
- addElement(event.timestamp, Math.max(0,
+ addElement(TimeUnit.NANOSECONDS.toMillis(event.timestamp), Math.max(0,
(int) (Math.log(event.values[0]) * LIGHT_VALUE_MULTIPLIER)));
updateLightSession();
mHandler.removeCallbacksAndMessages(mDelayedUpdateToken);
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/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index f25929c..189b86f 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -1166,7 +1166,7 @@
try {
connection.mService.attach(connection, mToken, TYPE_WALLPAPER, false,
wpdData.mWidth, wpdData.mHeight,
- wpdData.mPadding, mDisplayId);
+ wpdData.mPadding, mDisplayId, FLAG_SYSTEM | FLAG_LOCK);
} catch (RemoteException e) {
Slog.w(TAG, "Failed attaching wallpaper on display", e);
if (wallpaper != null && !wallpaper.wallpaperUpdating
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 9c080e8..2eb2cf6 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -4076,7 +4076,11 @@
// to the restarted activity.
nowVisible = mVisibleRequested;
}
- mTransitionController.requestCloseTransitionIfNeeded(this);
+ // upgrade transition trigger to task if this is the last activity since it means we are
+ // closing the task.
+ final WindowContainer trigger = remove && task != null && task.getChildCount() == 1
+ ? task : this;
+ mTransitionController.requestCloseTransitionIfNeeded(trigger);
cleanUp(true /* cleanServices */, true /* setState */);
if (remove) {
if (mStartingData != null && mVisible && task != null) {
@@ -7634,6 +7638,31 @@
ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */);
}
+ /**
+ * Returns the requested {@link Configuration.Orientation} for the current activity.
+ *
+ * <p>When The current orientation is set to {@link SCREEN_ORIENTATION_BEHIND} it returns the
+ * requested orientation for the activity below which is the first activity with an explicit
+ * (different from {@link SCREEN_ORIENTATION_UNSET}) orientation which is not {@link
+ * SCREEN_ORIENTATION_BEHIND}.
+ */
+ @Configuration.Orientation
+ @Override
+ int getRequestedConfigurationOrientation(boolean forDisplay) {
+ if (mOrientation == SCREEN_ORIENTATION_BEHIND && task != null) {
+ // We use Task here because we want to be consistent with what happens in
+ // multi-window mode where other tasks orientations are ignored.
+ final ActivityRecord belowCandidate = task.getActivity(
+ a -> a.mOrientation != SCREEN_ORIENTATION_UNSET && !a.finishing
+ && a.mOrientation != ActivityInfo.SCREEN_ORIENTATION_BEHIND, this,
+ false /* includeBoundary */, true /* traverseTopToBottom */);
+ if (belowCandidate != null) {
+ return belowCandidate.getRequestedConfigurationOrientation(forDisplay);
+ }
+ }
+ return super.getRequestedConfigurationOrientation(forDisplay);
+ }
+
@Override
void onCancelFixedRotationTransform(int originalDisplayRotation) {
if (this != mDisplayContent.getLastOrientationSource()) {
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 30454d4..9f502ab 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -389,9 +389,9 @@
SafeActivityOptions bottomOptions = null;
if (options != null) {
// To ensure the first N-1 activities (N == total # of activities) are also launched
- // into the correct display, use a copy of the passed-in options (keeping only
- // display-related info) for these activities.
- bottomOptions = options.selectiveCloneDisplayOptions();
+ // into the correct display and root task, use a copy of the passed-in options (keeping
+ // only display-related and launch-root-task information) for these activities.
+ bottomOptions = options.selectiveCloneLaunchOptions();
}
try {
intents = ArrayUtils.filterNotNull(intents, Intent[]::new);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index e3916cb..fe691c6 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -2593,6 +2593,9 @@
// activity lifecycle transaction to make sure the override pending app
// transition will be applied immediately.
targetActivity.applyOptionsAnimation();
+ if (activityOptions != null && activityOptions.getLaunchCookie() != null) {
+ targetActivity.mLaunchCookie = activityOptions.getLaunchCookie();
+ }
} finally {
mActivityMetricsLogger.notifyActivityLaunched(launchingState,
START_TASK_TO_FRONT, false /* newActivityCreated */,
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
new file mode 100644
index 0000000..5e44d6c
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.util.DisplayMetrics.DENSITY_DEFAULT;
+
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo;
+import android.graphics.Rect;
+import android.os.SystemProperties;
+import android.util.Slog;
+
+import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
+
+/**
+ * The class that defines default launch params for tasks in desktop mode
+ */
+public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
+
+ private static final String TAG =
+ TAG_WITH_CLASS_NAME ? "DesktopModeLaunchParamsModifier" : TAG_ATM;
+ private static final boolean DEBUG = false;
+
+ // Desktop mode feature flag.
+ static final boolean DESKTOP_MODE_SUPPORTED = SystemProperties.getBoolean(
+ "persist.wm.debug.desktop_mode", false);
+ // Override default freeform task width when desktop mode is enabled. In dips.
+ private static final int DESKTOP_MODE_DEFAULT_WIDTH_DP = SystemProperties.getInt(
+ "persist.wm.debug.desktop_mode.default_width", 840);
+ // Override default freeform task height when desktop mode is enabled. In dips.
+ private static final int DESKTOP_MODE_DEFAULT_HEIGHT_DP = SystemProperties.getInt(
+ "persist.wm.debug.desktop_mode.default_height", 630);
+
+ private StringBuilder mLogBuilder;
+
+ @Override
+ public int onCalculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
+ @Nullable ActivityRecord activity, @Nullable ActivityRecord source,
+ @Nullable ActivityOptions options, @Nullable ActivityStarter.Request request, int phase,
+ LaunchParamsController.LaunchParams currentParams,
+ LaunchParamsController.LaunchParams outParams) {
+
+ initLogBuilder(task, activity);
+ int result = calculate(task, layout, activity, source, options, request, phase,
+ currentParams, outParams);
+ outputLog();
+ return result;
+ }
+
+ private int calculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
+ @Nullable ActivityRecord activity, @Nullable ActivityRecord source,
+ @Nullable ActivityOptions options, @Nullable ActivityStarter.Request request, int phase,
+ LaunchParamsController.LaunchParams currentParams,
+ LaunchParamsController.LaunchParams outParams) {
+
+ if (task == null) {
+ appendLog("task null, skipping");
+ return RESULT_SKIP;
+ }
+ if (phase != PHASE_BOUNDS) {
+ appendLog("not in bounds phase, skipping");
+ return RESULT_SKIP;
+ }
+ if (!task.inFreeformWindowingMode()) {
+ appendLog("not a freeform task, skipping");
+ return RESULT_SKIP;
+ }
+ if (!currentParams.mBounds.isEmpty()) {
+ appendLog("currentParams has bounds set, not overriding");
+ return RESULT_SKIP;
+ }
+
+ // Copy over any values
+ outParams.set(currentParams);
+
+ // Update width and height with default desktop mode values
+ float density = (float) task.getConfiguration().densityDpi / DENSITY_DEFAULT;
+ final int width = (int) (DESKTOP_MODE_DEFAULT_WIDTH_DP * density + 0.5f);
+ final int height = (int) (DESKTOP_MODE_DEFAULT_HEIGHT_DP * density + 0.5f);
+ outParams.mBounds.right = width;
+ outParams.mBounds.bottom = height;
+
+ // Center the task in window bounds
+ Rect windowBounds = task.getWindowConfiguration().getBounds();
+ outParams.mBounds.offset(windowBounds.centerX() - outParams.mBounds.centerX(),
+ windowBounds.centerY() - outParams.mBounds.centerY());
+
+ appendLog("setting desktop mode task bounds to %s", outParams.mBounds);
+
+ return RESULT_DONE;
+ }
+
+ private void initLogBuilder(Task task, ActivityRecord activity) {
+ if (DEBUG) {
+ mLogBuilder = new StringBuilder(
+ "DesktopModeLaunchParamsModifier: task=" + task + " activity=" + activity);
+ }
+ }
+
+ private void appendLog(String format, Object... args) {
+ if (DEBUG) mLogBuilder.append(" ").append(String.format(format, args));
+ }
+
+ private void outputLog() {
+ if (DEBUG) Slog.d(TAG, mLogBuilder.toString());
+ }
+}
diff --git a/services/core/java/com/android/server/wm/DeviceStateController.java b/services/core/java/com/android/server/wm/DeviceStateController.java
new file mode 100644
index 0000000..a6f8557
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DeviceStateController.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.util.function.Consumer;
+
+/**
+ * Class that registers callbacks with the {@link DeviceStateManager} and
+ * responds to fold state changes by forwarding such events to a delegate.
+ */
+final class DeviceStateController {
+ private final DeviceStateManager mDeviceStateManager;
+ private final Context mContext;
+
+ private FoldStateListener mDeviceStateListener;
+
+ public enum FoldState {
+ UNKNOWN, OPEN, FOLDED, HALF_FOLDED
+ }
+
+ DeviceStateController(Context context, Handler handler, Consumer<FoldState> delegate) {
+ mContext = context;
+ mDeviceStateManager = mContext.getSystemService(DeviceStateManager.class);
+ if (mDeviceStateManager != null) {
+ mDeviceStateListener = new FoldStateListener(mContext, delegate);
+ mDeviceStateManager
+ .registerCallback(new HandlerExecutor(handler),
+ mDeviceStateListener);
+ }
+ }
+
+ void unregisterFromDeviceStateManager() {
+ if (mDeviceStateListener != null) {
+ mDeviceStateManager.unregisterCallback(mDeviceStateListener);
+ }
+ }
+
+ /**
+ * A listener for half-fold device state events that dispatches state changes to a delegate.
+ */
+ static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
+
+ private final int[] mHalfFoldedDeviceStates;
+ private final int[] mFoldedDeviceStates;
+
+ @Nullable
+ private FoldState mLastResult;
+ private final Consumer<FoldState> mDelegate;
+
+ FoldStateListener(Context context, Consumer<FoldState> delegate) {
+ mFoldedDeviceStates = context.getResources().getIntArray(
+ com.android.internal.R.array.config_foldedDeviceStates);
+ mHalfFoldedDeviceStates = context.getResources().getIntArray(
+ com.android.internal.R.array.config_halfFoldedDeviceStates);
+ mDelegate = delegate;
+ }
+
+ @Override
+ public void onStateChanged(int state) {
+ final boolean halfFolded = ArrayUtils.contains(mHalfFoldedDeviceStates, state);
+ FoldState result;
+ if (halfFolded) {
+ result = FoldState.HALF_FOLDED;
+ } else {
+ final boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state);
+ result = folded ? FoldState.FOLDED : FoldState.OPEN;
+ }
+ if (mLastResult == null || !mLastResult.equals(result)) {
+ mLastResult = result;
+ mDelegate.accept(result);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 71c80fb..38f6a53 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -564,6 +564,7 @@
final FixedRotationTransitionListener mFixedRotationTransitionListener =
new FixedRotationTransitionListener();
+ private final DeviceStateController mDeviceStateController;
private final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher;
final RemoteDisplayChangeController mRemoteDisplayChangeController;
@@ -1119,6 +1120,13 @@
mDisplayPolicy = new DisplayPolicy(mWmService, this);
mDisplayRotation = new DisplayRotation(mWmService, this);
+
+ mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH,
+ newFoldState -> {
+ mDisplaySwitchTransitionLauncher.foldStateChanged(newFoldState);
+ mDisplayRotation.foldStateChanged(newFoldState);
+ });
+
mCloseToSquareMaxAspectRatio = mWmService.mContext.getResources().getFloat(
R.dimen.config_closeToSquareDisplayMaxAspectRatio);
if (isDefaultDisplay) {
@@ -3218,7 +3226,7 @@
mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener);
handleAnimatingStoppedAndTransition();
mWmService.stopFreezingDisplayLocked();
- mDisplaySwitchTransitionLauncher.destroy();
+ mDeviceStateController.unregisterFromDeviceStateManager();
super.removeImmediately();
if (DEBUG_DISPLAY) Slog.v(TAG_WM, "Removing display=" + this);
mPointerEventDispatcher.dispose();
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 97609a7..a8d13c5 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -40,6 +40,7 @@
import android.annotation.AnimRes;
import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
@@ -108,6 +109,8 @@
private OrientationListener mOrientationListener;
private StatusBarManagerInternal mStatusBarManagerInternal;
private SettingsObserver mSettingsObserver;
+ @Nullable
+ private FoldController mFoldController;
@ScreenOrientation
private int mCurrentAppOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
@@ -238,6 +241,10 @@
mOrientationListener.setCurrentRotation(mRotation);
mSettingsObserver = new SettingsObserver(uiHandler);
mSettingsObserver.observe();
+ if (mSupportAutoRotation && mContext.getResources().getBoolean(
+ R.bool.config_windowManagerHalfFoldAutoRotateOverride)) {
+ mFoldController = new FoldController();
+ }
}
}
@@ -436,7 +443,17 @@
final int oldRotation = mRotation;
final int lastOrientation = mLastOrientation;
- final int rotation = rotationForOrientation(lastOrientation, oldRotation);
+ int rotation = rotationForOrientation(lastOrientation, oldRotation);
+ // Use the saved rotation for tabletop mode, if set.
+ if (mFoldController != null && mFoldController.shouldRevertOverriddenRotation()) {
+ int prevRotation = rotation;
+ rotation = mFoldController.revertOverriddenRotation();
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Reverting orientation. Rotating to %s from %s rather than %s.",
+ Surface.rotationToString(rotation),
+ Surface.rotationToString(oldRotation),
+ Surface.rotationToString(prevRotation));
+ }
ProtoLog.v(WM_DEBUG_ORIENTATION,
"Computed rotation=%s (%d) for display id=%d based on lastOrientation=%s (%d) and "
+ "oldRotation=%s (%d)",
@@ -1138,7 +1155,8 @@
// If we don't support auto-rotation then bail out here and ignore
// the sensor and any rotation lock settings.
preferredRotation = -1;
- } else if ((mUserRotationMode == WindowManagerPolicy.USER_ROTATION_FREE
+ } else if (((mUserRotationMode == WindowManagerPolicy.USER_ROTATION_FREE
+ || isTabletopAutoRotateOverrideEnabled())
&& (orientation == ActivityInfo.SCREEN_ORIENTATION_USER
|| orientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|| orientation == ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
@@ -1292,10 +1310,17 @@
return false;
}
+ private boolean isTabletopAutoRotateOverrideEnabled() {
+ return mFoldController != null && mFoldController.overrideFrozenRotation();
+ }
+
private boolean isRotationChoicePossible(int orientation) {
// Rotation choice is only shown when the user is in locked mode.
if (mUserRotationMode != WindowManagerPolicy.USER_ROTATION_LOCKED) return false;
+ // Don't show rotation choice if we are in tabletop or book modes.
+ if (isTabletopAutoRotateOverrideEnabled()) return false;
+
// We should only enable rotation choice if the rotation isn't forced by the lid, dock,
// demo, hdmi, vr, etc mode.
@@ -1496,6 +1521,74 @@
proto.end(token);
}
+ /**
+ * Called by the DeviceStateManager callback when the device state changes.
+ */
+ void foldStateChanged(DeviceStateController.FoldState foldState) {
+ if (mFoldController != null) {
+ synchronized (mLock) {
+ mFoldController.foldStateChanged(foldState);
+ }
+ }
+ }
+
+ private class FoldController {
+ @Surface.Rotation
+ private int mHalfFoldSavedRotation = -1; // No saved rotation
+ private DeviceStateController.FoldState mFoldState =
+ DeviceStateController.FoldState.UNKNOWN;
+
+ boolean overrideFrozenRotation() {
+ return mFoldState == DeviceStateController.FoldState.HALF_FOLDED;
+ }
+
+ boolean shouldRevertOverriddenRotation() {
+ return mFoldState == DeviceStateController.FoldState.OPEN // When transitioning to open.
+ && mHalfFoldSavedRotation != -1 // Ignore if we've already reverted.
+ && mUserRotationMode
+ == WindowManagerPolicy.USER_ROTATION_LOCKED; // Ignore if we're unlocked.
+ }
+
+ int revertOverriddenRotation() {
+ int savedRotation = mHalfFoldSavedRotation;
+ mHalfFoldSavedRotation = -1;
+ return savedRotation;
+ }
+
+ void foldStateChanged(DeviceStateController.FoldState newState) {
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "foldStateChanged: displayId %d, halfFoldStateChanged %s, "
+ + "saved rotation: %d, mUserRotation: %d, mLastSensorRotation: %d, "
+ + "mLastOrientation: %d, mRotation: %d",
+ mDisplayContent.getDisplayId(), newState.name(), mHalfFoldSavedRotation,
+ mUserRotation, mLastSensorRotation, mLastOrientation, mRotation);
+ if (mFoldState == DeviceStateController.FoldState.UNKNOWN) {
+ mFoldState = newState;
+ return;
+ }
+ if (newState == DeviceStateController.FoldState.HALF_FOLDED
+ && mFoldState != DeviceStateController.FoldState.HALF_FOLDED) {
+ // The device has transitioned to HALF_FOLDED state: save the current rotation and
+ // update the device rotation.
+ mHalfFoldSavedRotation = mRotation;
+ mFoldState = newState;
+ // Now mFoldState is set to HALF_FOLDED, the overrideFrozenRotation function will
+ // return true, so rotation is unlocked.
+ mService.updateRotation(false /* alwaysSendConfiguration */,
+ false /* forceRelayout */);
+ } else {
+ // Revert the rotation to our saved value if we transition from HALF_FOLDED.
+ mRotation = mHalfFoldSavedRotation;
+ // Tell the device to update its orientation (mFoldState is still HALF_FOLDED here
+ // so we will override USER_ROTATION_LOCKED and allow a rotation).
+ mService.updateRotation(false /* alwaysSendConfiguration */,
+ false /* forceRelayout */);
+ // Once we are rotated, set mFoldstate, effectively removing the lock override.
+ mFoldState = newState;
+ }
+ }
+ }
+
private class OrientationListener extends WindowOrientationListener implements Runnable {
transient boolean mEnabled;
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/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java
index 7bd2a4a..e74e5787 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsController.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsController.java
@@ -64,6 +64,10 @@
void registerDefaultModifiers(ActivityTaskSupervisor supervisor) {
// {@link TaskLaunchParamsModifier} handles window layout preferences.
registerModifier(new TaskLaunchParamsModifier(supervisor));
+ if (DesktopModeLaunchParamsModifier.DESKTOP_MODE_SUPPORTED) {
+ // {@link DesktopModeLaunchParamsModifier} handles default task size changes
+ registerModifier(new DesktopModeLaunchParamsModifier());
+ }
}
/**
diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
index a89894d..30bdc34 100644
--- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
+++ b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
@@ -24,10 +24,7 @@
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.content.Context;
import android.graphics.Rect;
-import android.hardware.devicestate.DeviceStateManager;
-import android.os.HandlerExecutor;
import android.window.DisplayAreaInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
@@ -36,11 +33,8 @@
private final DisplayContent mDisplayContent;
private final WindowManagerService mService;
- private final DeviceStateManager mDeviceStateManager;
private final TransitionController mTransitionController;
- private DeviceStateListener mDeviceStateListener;
-
/**
* If on a foldable device represents whether the device is folded or not
*/
@@ -52,21 +46,15 @@
mDisplayContent = displayContent;
mService = displayContent.mWmService;
mTransitionController = transitionController;
-
- mDeviceStateManager = mService.mContext.getSystemService(DeviceStateManager.class);
-
- if (mDeviceStateManager != null) {
- mDeviceStateListener = new DeviceStateListener(mService.mContext);
- mDeviceStateManager
- .registerCallback(new HandlerExecutor(mDisplayContent.mWmService.mH),
- mDeviceStateListener);
- }
}
- public void destroy() {
- if (mDeviceStateManager != null) {
- mDeviceStateManager.unregisterCallback(mDeviceStateListener);
- }
+ /**
+ * Called by the DeviceStateManager callback when the state changes.
+ */
+ void foldStateChanged(DeviceStateController.FoldState newFoldState) {
+ // Ignore transitions to/from half-folded.
+ if (newFoldState == DeviceStateController.FoldState.HALF_FOLDED) return;
+ mIsFolded = newFoldState == DeviceStateController.FoldState.FOLDED;
}
/**
@@ -143,10 +131,4 @@
mTransition = null;
}
- class DeviceStateListener extends DeviceStateManager.FoldStateListener {
-
- DeviceStateListener(Context context) {
- super(context, newIsFolded -> mIsFolded = newIsFolded);
- }
- }
}
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 7f22242..2866f42 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2021,7 +2021,12 @@
// non-fullscreen bounds. Then when this new PIP task exits PIP, it can restore
// to its previous freeform bounds.
rootTask.setLastNonFullscreenBounds(task.mLastNonFullscreenBounds);
- rootTask.setBounds(task.getBounds());
+ // When creating a new Task for PiP, set its initial bounds as the TaskFragment in
+ // case the activity is embedded, so that it can be animated to PiP window from the
+ // current bounds.
+ // Use Task#setBoundsUnchecked to skip checking windowing mode as the windowing mode
+ // will be updated later after this is collected in transition.
+ rootTask.setBoundsUnchecked(r.getTaskFragment().getBounds());
// Move the last recents animation transaction from original task to the new one.
if (task.mLastRecentsAnimationTransaction != null) {
diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java
index e69a732..6d1b5fa 100644
--- a/services/core/java/com/android/server/wm/SafeActivityOptions.java
+++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java
@@ -117,13 +117,13 @@
/**
* To ensure that two activities, one using this object, and the other using the
- * SafeActivityOptions returned from this function, are launched into the same display through
- * ActivityStartController#startActivities, all display-related information, i.e.
- * displayAreaToken, launchDisplayId and callerDisplayId, are cloned.
+ * SafeActivityOptions returned from this function, are launched into the same display/root task
+ * through ActivityStartController#startActivities, all display-related information, i.e.
+ * displayAreaToken, launchDisplayId, callerDisplayId and the launch root task are cloned.
*/
- @Nullable SafeActivityOptions selectiveCloneDisplayOptions() {
- final ActivityOptions options = cloneLaunchingDisplayOptions(mOriginalOptions);
- final ActivityOptions callerOptions = cloneLaunchingDisplayOptions(mCallerOptions);
+ @Nullable SafeActivityOptions selectiveCloneLaunchOptions() {
+ final ActivityOptions options = cloneLaunchingOptions(mOriginalOptions);
+ final ActivityOptions callerOptions = cloneLaunchingOptions(mCallerOptions);
if (options == null && callerOptions == null) {
return null;
}
@@ -136,11 +136,12 @@
return safeOptions;
}
- private ActivityOptions cloneLaunchingDisplayOptions(ActivityOptions options) {
+ private ActivityOptions cloneLaunchingOptions(ActivityOptions options) {
return options == null ? null : ActivityOptions.makeBasic()
.setLaunchTaskDisplayArea(options.getLaunchTaskDisplayArea())
.setLaunchDisplayId(options.getLaunchDisplayId())
- .setCallerDisplayId((options.getCallerDisplayId()));
+ .setCallerDisplayId(options.getCallerDisplayId())
+ .setLaunchRootTask(options.getLaunchRootTask());
}
/**
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 6ce96ec..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;
@@ -2618,6 +2617,13 @@
return boundsChange;
}
+ /** Sets the requested bounds regardless of the windowing mode. */
+ int setBoundsUnchecked(@NonNull Rect bounds) {
+ final int boundsChange = super.setBounds(bounds);
+ updateSurfaceBounds();
+ return boundsChange;
+ }
+
@Override
public boolean isCompatible(int windowingMode, int activityType) {
// TODO: Should we just move this to ConfigurationContainer?
@@ -5848,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) {
@@ -5920,10 +5924,7 @@
return BOUNDS_CHANGE_NONE;
}
- final int result = super.setBounds(!inMultiWindowMode() ? null : bounds);
-
- updateSurfaceBounds();
- return result;
+ return setBoundsUnchecked(!inMultiWindowMode() ? null : bounds);
}
@Override
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/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 526a366..46253c1 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1595,6 +1595,10 @@
if (info.mEndParent != null) {
change.setParent(info.mEndParent.mRemoteToken.toWindowContainerToken());
}
+ if (info.mStartParent != null && info.mStartParent.mRemoteToken != null
+ && target.getParent() != info.mStartParent) {
+ change.setLastParent(info.mStartParent.mRemoteToken.toWindowContainerToken());
+ }
change.setMode(info.getTransitMode(target));
change.setStartAbsBounds(info.mAbsoluteBounds);
change.setFlags(info.getChangeFlags(target));
@@ -1874,15 +1878,15 @@
flags |= FLAG_TRANSLUCENT;
}
final Task task = wc.asTask();
- if (task != null && task.voiceSession != null) {
- flags |= FLAG_IS_VOICE_INTERACTION;
- }
if (task != null) {
final ActivityRecord topActivity = task.getTopNonFinishingActivity();
if (topActivity != null && topActivity.mStartingData != null
&& topActivity.mStartingData.hasImeSurface()) {
flags |= FLAG_WILL_IME_SHOWN;
}
+ if (task.voiceSession != null) {
+ flags |= FLAG_IS_VOICE_INTERACTION;
+ }
}
Task parentTask = null;
final ActivityRecord record = wc.asActivityRecord();
@@ -1910,20 +1914,26 @@
// Whether the container fills its parent Task bounds.
flags |= FLAG_FILLS_TASK;
}
- }
- final DisplayContent dc = wc.asDisplayContent();
- if (dc != null) {
- flags |= FLAG_IS_DISPLAY;
- if (dc.hasAlertWindowSurfaces()) {
- flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+ } else {
+ final DisplayContent dc = wc.asDisplayContent();
+ if (dc != null) {
+ flags |= FLAG_IS_DISPLAY;
+ if (dc.hasAlertWindowSurfaces()) {
+ flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+ }
+ } else if (isWallpaper(wc)) {
+ flags |= FLAG_IS_WALLPAPER;
+ } else if (isInputMethod(wc)) {
+ flags |= FLAG_IS_INPUT_METHOD;
+ } else {
+ // In this condition, the wc can only be WindowToken or DisplayArea.
+ final int type = wc.getWindowType();
+ if (type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW
+ && type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
+ flags |= TransitionInfo.FLAG_IS_SYSTEM_WINDOW;
+ }
}
}
- if (isWallpaper(wc)) {
- flags |= FLAG_IS_WALLPAPER;
- }
- if (isInputMethod(wc)) {
- flags |= FLAG_IS_INPUT_METHOD;
- }
if (occludesKeyguard(wc)) {
flags |= FLAG_OCCLUDES_KEYGUARD;
}
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index e8682f7..26ce4ae 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -126,19 +126,27 @@
mTransitionTracer = transitionTracer;
mTransitionPlayerDeath = () -> {
synchronized (mAtm.mGlobalLock) {
- // Clean-up/finish any playing transitions.
- for (int i = 0; i < mPlayingTransitions.size(); ++i) {
- mPlayingTransitions.get(i).cleanUpOnFailure();
- }
- mPlayingTransitions.clear();
- mTransitionPlayer = null;
- mTransitionPlayerProc = null;
- mRemotePlayer.clear();
- mRunningLock.doNotifyLocked();
+ detachPlayer();
}
};
}
+ private void detachPlayer() {
+ if (mTransitionPlayer == null) return;
+ // Clean-up/finish any playing transitions.
+ for (int i = 0; i < mPlayingTransitions.size(); ++i) {
+ mPlayingTransitions.get(i).cleanUpOnFailure();
+ }
+ mPlayingTransitions.clear();
+ if (mCollectingTransition != null) {
+ mCollectingTransition.abort();
+ }
+ mTransitionPlayer = null;
+ mTransitionPlayerProc = null;
+ mRemotePlayer.clear();
+ mRunningLock.doNotifyLocked();
+ }
+
/** @see #createTransition(int, int) */
@NonNull
Transition createTransition(int type) {
@@ -193,7 +201,7 @@
if (mTransitionPlayer.asBinder() != null) {
mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0);
}
- mTransitionPlayer = null;
+ detachPlayer();
}
if (player.asBinder() != null) {
player.asBinder().linkToDeath(mTransitionPlayerDeath, 0);
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 949fa96..32a110e 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1136,10 +1136,13 @@
final LauncherAppsServiceInternal launcherApps = LocalServices.getService(
LauncherAppsServiceInternal.class);
- launcherApps.startShortcut(caller.mUid, caller.mPid, callingPackage,
- hop.getShortcutInfo().getPackage(), null /* default featureId */,
+ final boolean success = launcherApps.startShortcut(caller.mUid, caller.mPid,
+ callingPackage, hop.getShortcutInfo().getPackage(), null /* featureId */,
hop.getShortcutInfo().getId(), null /* sourceBounds */, launchOpts,
hop.getShortcutInfo().getUserId());
+ if (success) {
+ effects |= TRANSACT_EFFECTS_LIFECYCLE;
+ }
break;
}
case HIERARCHY_OP_TYPE_REPARENT_CHILDREN: {
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 4a51b83..f53a1cf 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -383,6 +383,7 @@
</xs:complexType>
<xs:complexType name="autoBrightness">
+ <xs:attribute name="enabled" type="xs:boolean" use="optional" default="true"/>
<xs:sequence>
<!-- Sets the debounce for autoBrightness brightening in millis-->
<xs:element name="brighteningLightDebounceMillis" type="xs:nonNegativeInteger"
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 748ef4b..d89bd7c 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -6,9 +6,11 @@
method public final java.math.BigInteger getBrighteningLightDebounceMillis();
method public final java.math.BigInteger getDarkeningLightDebounceMillis();
method public final com.android.server.display.config.DisplayBrightnessMapping getDisplayBrightnessMapping();
+ method public boolean getEnabled();
method public final void setBrighteningLightDebounceMillis(java.math.BigInteger);
method public final void setDarkeningLightDebounceMillis(java.math.BigInteger);
method public final void setDisplayBrightnessMapping(com.android.server.display.config.DisplayBrightnessMapping);
+ method public void setEnabled(boolean);
}
public class BrightnessThresholds {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 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/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemStubMultiUserDisableUninstallTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemStubMultiUserDisableUninstallTest.kt
index 45e0d09..4ef6875 100644
--- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemStubMultiUserDisableUninstallTest.kt
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemStubMultiUserDisableUninstallTest.kt
@@ -628,6 +628,7 @@
CodePath.SAME, CodePath.DIFFERENT ->
throw AssertionError("secondDataPath cannot be a data path")
CodePath.SYSTEM -> assertThat(codePaths[1]).isEqualTo(stubFile.parent.toString())
+ else -> {}
}
}
diff --git a/services/tests/mockingservicestests/OWNERS b/services/tests/mockingservicestests/OWNERS
index 2bb1649..4dda51f 100644
--- a/services/tests/mockingservicestests/OWNERS
+++ b/services/tests/mockingservicestests/OWNERS
@@ -1,5 +1,8 @@
include platform/frameworks/base:/services/core/java/com/android/server/am/OWNERS
+
+# Game Platform
per-file FakeGameClassifier.java = file:/GAME_MANAGER_OWNERS
per-file FakeGameServiceProviderInstance = file:/GAME_MANAGER_OWNERS
per-file FakeServiceConnector.java = file:/GAME_MANAGER_OWNERS
per-file Game* = file:/GAME_MANAGER_OWNERS
+per-file res/xml/game_manager* = file:/GAME_MANAGER_OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt
index b9d6b2c..994df22 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt
@@ -392,7 +392,7 @@
whenever(rule.mocks().appsFilter.getVisibilityAllowList(
any(PackageDataSnapshot::class.java),
argThat { it?.packageName == pkgSetting.packageName }, any(IntArray::class.java),
- any() as ArrayMap<String, out PackageStateInternal>
+ any<ArrayMap<String, out PackageStateInternal>>()
))
.thenReturn(list)
}
diff --git a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
index bccd8a0b..9ae8922 100644
--- a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -61,6 +62,7 @@
public class UserBackupManagerServiceTest {
private static final String TEST_PACKAGE = "package1";
private static final String[] TEST_PACKAGES = new String[] { TEST_PACKAGE };
+ private static final int WORKER_THREAD_TIMEOUT_MILLISECONDS = 1;
@Mock Context mContext;
@Mock IBackupManagerMonitor mBackupManagerMonitor;
@@ -179,6 +181,7 @@
mService.agentDisconnected("com.android.foo");
+ mService.waitForAsyncOperation();
verify(mOperationStorage).cancelOperation(eq(123), eq(true), any(IntConsumer.class));
verify(mOperationStorage).cancelOperation(eq(456), eq(true), any());
verify(mOperationStorage).cancelOperation(eq(789), eq(true), any());
@@ -207,6 +210,8 @@
boolean isEnabledStatePersisted = false;
boolean shouldUseNewBackupEligibilityRules = false;
+ private volatile Thread mWorkerThread = null;
+
TestBackupService(Context context, PackageManager packageManager,
LifecycleOperationStorage operationStorage) {
super(context, packageManager, operationStorage);
@@ -229,5 +234,23 @@
boolean shouldUseNewBackupEligibilityRules() {
return shouldUseNewBackupEligibilityRules;
}
+
+ @Override
+ Thread getThreadForAsyncOperation(String operationName, Runnable operation) {
+ mWorkerThread = super.getThreadForAsyncOperation(operationName, operation);
+ return mWorkerThread;
+ }
+
+ private void waitForAsyncOperation() {
+ if (mWorkerThread == null) {
+ return;
+ }
+
+ try {
+ mWorkerThread.join(/* millis */ WORKER_THREAD_TIMEOUT_MILLISECONDS);
+ } catch (InterruptedException e) {
+ fail("Failed waiting for worker thread to complete: " + e.getMessage());
+ }
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
index 10f0a5c..68c9ce4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
@@ -23,6 +23,7 @@
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -50,6 +51,9 @@
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
@Presubmit
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -93,7 +97,7 @@
mSensorEventListenerCaptor.getValue().onSensorChanged(
new SensorEvent(mLightSensor, 1, 2, new float[]{value}));
- assertThat(mProbe.getCurrentLux()).isEqualTo(value);
+ assertThat(mProbe.getMostRecentLux()).isEqualTo(value);
}
@Test
@@ -121,13 +125,17 @@
mProbe.destroy();
mProbe.enable();
+ AtomicInteger lux = new AtomicInteger(10);
+ mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
verify(mSensorManager, never()).registerListener(any(), any(), anyInt());
verifyNoMoreInteractions(mSensorManager);
+ assertThat(lux.get()).isLessThan(0);
}
@Test
public void testDisabledReportsNegativeValue() {
- assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+ assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
mProbe.enable();
verify(mSensorManager).registerListener(
@@ -136,7 +144,7 @@
new SensorEvent(mLightSensor, 1, 1, new float[]{4.0f}));
mProbe.disable();
- assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+ assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
}
@Test
@@ -150,7 +158,7 @@
verify(mSensorManager).unregisterListener(eq(mSensorEventListenerCaptor.getValue()));
verifyNoMoreInteractions(mSensorManager);
- assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+ assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
}
@Test
@@ -166,7 +174,148 @@
verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
verifyNoMoreInteractions(mSensorManager);
- assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+ assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
+ }
+
+ @Test
+ public void testNextLuxWhenAlreadyEnabledAndNotAvailable() {
+ testNextLuxWhenAlreadyEnabled(false /* dataIsAvailable */);
+ }
+
+ @Test
+ public void testNextLuxWhenAlreadyEnabledAndAvailable() {
+ testNextLuxWhenAlreadyEnabled(true /* dataIsAvailable */);
+ }
+
+ private void testNextLuxWhenAlreadyEnabled(boolean dataIsAvailable) {
+ final List<Integer> values = List.of(1, 2, 3, 4, 6);
+ mProbe.enable();
+
+ verify(mSensorManager).registerListener(
+ mSensorEventListenerCaptor.capture(), any(), anyInt());
+
+ if (dataIsAvailable) {
+ for (int v : values) {
+ mSensorEventListenerCaptor.getValue().onSensorChanged(
+ new SensorEvent(mLightSensor, 1, 1, new float[]{v}));
+ }
+ }
+ AtomicInteger lux = new AtomicInteger(-1);
+ mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+ if (!dataIsAvailable) {
+ for (int v : values) {
+ mSensorEventListenerCaptor.getValue().onSensorChanged(
+ new SensorEvent(mLightSensor, 1, 1, new float[]{v}));
+ }
+ }
+
+ mSensorEventListenerCaptor.getValue().onSensorChanged(
+ new SensorEvent(mLightSensor, 1, 1, new float[]{200f}));
+
+ // should remain enabled
+ assertThat(lux.get()).isEqualTo(values.get(dataIsAvailable ? values.size() - 1 : 0));
+ verify(mSensorManager, never()).unregisterListener(any(SensorEventListener.class));
+ verifyNoMoreInteractions(mSensorManager);
+
+ final int anotherValue = 12;
+ mSensorEventListenerCaptor.getValue().onSensorChanged(
+ new SensorEvent(mLightSensor, 1, 1, new float[]{12}));
+ assertThat(mProbe.getMostRecentLux()).isEqualTo(anotherValue);
+ }
+
+ @Test
+ public void testNextLuxWhenNotEnabled() {
+ testNextLuxWhenNotEnabled(false /* enableWhileWaiting */);
+ }
+
+ @Test
+ public void testNextLuxWhenNotEnabledButEnabledLater() {
+ testNextLuxWhenNotEnabled(true /* enableWhileWaiting */);
+ }
+
+ private void testNextLuxWhenNotEnabled(boolean enableWhileWaiting) {
+ final List<Integer> values = List.of(1, 2, 3, 4, 6);
+ mProbe.disable();
+
+ AtomicInteger lux = new AtomicInteger(-1);
+ mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+ if (enableWhileWaiting) {
+ mProbe.enable();
+ }
+
+ verify(mSensorManager).registerListener(
+ mSensorEventListenerCaptor.capture(), any(), anyInt());
+ for (int v : values) {
+ mSensorEventListenerCaptor.getValue().onSensorChanged(
+ new SensorEvent(mLightSensor, 1, 1, new float[]{v}));
+ }
+
+ // should restore the disabled state
+ assertThat(lux.get()).isEqualTo(values.get(0));
+ verify(mSensorManager, enableWhileWaiting ? never() : times(1)).unregisterListener(
+ any(SensorEventListener.class));
+ verifyNoMoreInteractions(mSensorManager);
+ }
+
+ @Test
+ public void testNextLuxIsNotCanceledByDisableOrDestroy() {
+ final int value = 7;
+ AtomicInteger lux = new AtomicInteger(-1);
+ mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+ verify(mSensorManager).registerListener(
+ mSensorEventListenerCaptor.capture(), any(), anyInt());
+
+ mProbe.destroy();
+ mProbe.disable();
+
+ assertThat(lux.get()).isEqualTo(-1);
+
+ mSensorEventListenerCaptor.getValue().onSensorChanged(
+ new SensorEvent(mLightSensor, 1, 1, new float[]{value}));
+
+ assertThat(lux.get()).isEqualTo(value);
+
+ mSensorEventListenerCaptor.getValue().onSensorChanged(
+ new SensorEvent(mLightSensor, 1, 1, new float[]{value + 1}));
+
+ // should remain destroyed
+ mProbe.enable();
+
+ assertThat(lux.get()).isEqualTo(value);
+ verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
+ verifyNoMoreInteractions(mSensorManager);
+ }
+
+ @Test
+ public void testMultipleNextConsumers() {
+ final int value = 7;
+ AtomicInteger lux = new AtomicInteger(-1);
+ AtomicInteger lux2 = new AtomicInteger(-1);
+ mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+ mProbe.awaitNextLux((v) -> lux2.set(Math.round(v)), null /* handler */);
+
+ verify(mSensorManager).registerListener(
+ mSensorEventListenerCaptor.capture(), any(), anyInt());
+ mSensorEventListenerCaptor.getValue().onSensorChanged(
+ new SensorEvent(mLightSensor, 1, 1, new float[]{value}));
+
+ assertThat(lux.get()).isEqualTo(value);
+ assertThat(lux2.get()).isEqualTo(value);
+ }
+
+ @Test
+ public void testNoNextLuxWhenDestroyed() {
+ mProbe.destroy();
+
+ AtomicInteger lux = new AtomicInteger(-20);
+ mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+ assertThat(lux.get()).isEqualTo(-1);
+ verify(mSensorManager, never()).registerListener(
+ mSensorEventListenerCaptor.capture(), any(), anyInt());
+ verifyNoMoreInteractions(mSensorManager);
}
private void moveTimeBy(long millis) {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
index 60dc2eb..88a9646 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
@@ -121,7 +121,7 @@
verify(mSink).authenticate(eq(mOpContext),
eq(DEFAULT_MODALITY), eq(DEFAULT_ACTION), eq(DEFAULT_CLIENT), anyBoolean(),
anyLong(), anyInt(), eq(requireConfirmation),
- eq(targetUserId), anyFloat());
+ eq(targetUserId), any());
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index dea4d4f..606f486 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -28,6 +28,7 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -40,6 +41,7 @@
import android.hardware.biometrics.fingerprint.ISession;
import android.hardware.biometrics.fingerprint.PointerContext;
import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.hardware.fingerprint.ISidefpsController;
import android.hardware.fingerprint.IUdfpsOverlayController;
@@ -72,6 +74,7 @@
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import java.time.Clock;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
@@ -127,6 +130,8 @@
private ICancellationSignal mCancellationSignal;
@Mock
private Probe mLuxProbe;
+ @Mock
+ private Clock mClock;
@Captor
private ArgumentCaptor<OperationContext> mOperationContextCaptor;
@Captor
@@ -215,7 +220,7 @@
@Test
public void luxProbeWhenAwake() throws RemoteException {
- when(mBiometricContext.isAwake()).thenReturn(false, true, false);
+ when(mBiometricContext.isAwake()).thenReturn(false);
when(mBiometricContext.isAod()).thenReturn(false);
final FingerprintAuthenticationClient client = createClient();
client.start(mCallback);
@@ -228,15 +233,38 @@
verify(mLuxProbe, never()).enable();
reset(mLuxProbe);
+ when(mBiometricContext.isAwake()).thenReturn(true);
+
mContextInjector.getValue().accept(opContext);
verify(mLuxProbe).enable();
verify(mLuxProbe, never()).disable();
+ when(mBiometricContext.isAwake()).thenReturn(false);
+
mContextInjector.getValue().accept(opContext);
verify(mLuxProbe).disable();
}
@Test
+ public void luxProbeEnabledOnStartWhenWake() throws RemoteException {
+ luxProbeEnabledOnStart(true /* isAwake */);
+ }
+
+ @Test
+ public void luxProbeNotEnabledOnStartWhenNotWake() throws RemoteException {
+ luxProbeEnabledOnStart(false /* isAwake */);
+ }
+
+ private void luxProbeEnabledOnStart(boolean isAwake) throws RemoteException {
+ when(mBiometricContext.isAwake()).thenReturn(isAwake);
+ when(mBiometricContext.isAod()).thenReturn(false);
+ final FingerprintAuthenticationClient client = createClient();
+ client.start(mCallback);
+
+ verify(mLuxProbe, isAwake ? times(1) : never()).enable();
+ }
+
+ @Test
public void luxProbeDisabledOnAod() throws RemoteException {
when(mBiometricContext.isAwake()).thenReturn(false);
when(mBiometricContext.isAod()).thenReturn(true);
@@ -423,6 +451,52 @@
}
@Test
+ public void sideFingerprintSkipsWindowIfVendorMessageMatch() throws Exception {
+ when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+ final int vendorAcquireMessage = 1234;
+
+ mContext.getOrCreateTestableResources().addOverride(
+ R.integer.config_sidefpsSkipWaitForPowerAcquireMessage,
+ FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR);
+ mContext.getOrCreateTestableResources().addOverride(
+ R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage,
+ vendorAcquireMessage);
+
+ final FingerprintAuthenticationClient client = createClient(1);
+ client.start(mCallback);
+ mLooper.dispatchAll();
+ client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+ true /* authenticated */, new ArrayList<>());
+ client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, vendorAcquireMessage);
+ mLooper.dispatchAll();
+
+ verify(mCallback).onClientFinished(any(), eq(true));
+ }
+
+ @Test
+ public void sideFingerprintDoesNotSkipWindowOnVendorErrorMismatch() throws Exception {
+ when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+ final int vendorAcquireMessage = 1234;
+
+ mContext.getOrCreateTestableResources().addOverride(
+ R.integer.config_sidefpsSkipWaitForPowerAcquireMessage,
+ FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR);
+ mContext.getOrCreateTestableResources().addOverride(
+ R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage,
+ vendorAcquireMessage);
+
+ final FingerprintAuthenticationClient client = createClient(1);
+ client.start(mCallback);
+ mLooper.dispatchAll();
+ client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+ true /* authenticated */, new ArrayList<>());
+ client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 1);
+ mLooper.dispatchAll();
+
+ verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+ }
+
+ @Test
public void sideFingerprintSendsAuthIfFingerUp() throws Exception {
when(mSensorProps.isAnySidefpsType()).thenReturn(true);
@@ -469,6 +543,79 @@
verify(mCallback).onClientFinished(any(), eq(true));
}
+ @Test
+ public void sideFingerprintPowerWindowStartsOnAcquireStart() throws Exception {
+ final int powerWindow = 500;
+ final long authStart = 300;
+
+ when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+ mContext.getOrCreateTestableResources().addOverride(
+ R.integer.config_sidefpsBpPowerPressWindow, powerWindow);
+
+ final FingerprintAuthenticationClient client = createClient(1);
+ client.start(mCallback);
+
+ // Acquire start occurs at time = 0ms
+ when(mClock.millis()).thenReturn(0L);
+ client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
+
+ // Auth occurs at time = 300
+ when(mClock.millis()).thenReturn(authStart);
+ // At this point the delay should be 500 - (300 - 0) == 200 milliseconds.
+ client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+ true /* authenticated */, new ArrayList<>());
+ mLooper.dispatchAll();
+ verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+
+ // After waiting 200 milliseconds, auth should succeed.
+ mLooper.moveTimeForward(powerWindow - authStart);
+ mLooper.dispatchAll();
+ verify(mCallback).onClientFinished(any(), eq(true));
+ }
+
+ @Test
+ public void sideFingerprintPowerWindowStartsOnLastAcquireStart() throws Exception {
+ final int powerWindow = 500;
+
+ when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+ mContext.getOrCreateTestableResources().addOverride(
+ R.integer.config_sidefpsBpPowerPressWindow, powerWindow);
+
+ final FingerprintAuthenticationClient client = createClient(1);
+ client.start(mCallback);
+ // Acquire start occurs at time = 0ms
+ when(mClock.millis()).thenReturn(0L);
+ client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
+
+ // Auth reject occurs at time = 300ms
+ when(mClock.millis()).thenReturn(300L);
+ client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+ false /* authenticated */, new ArrayList<>());
+ mLooper.dispatchAll();
+
+ mLooper.moveTimeForward(300);
+ mLooper.dispatchAll();
+ verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+
+ when(mClock.millis()).thenReturn(1300L);
+ client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
+
+ // If code is correct, the new acquired start timestamp should be used
+ // and the code should only have to wait 500 - (1500-1300)ms.
+ when(mClock.millis()).thenReturn(1500L);
+ client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+ true /* authenticated */, new ArrayList<>());
+ mLooper.dispatchAll();
+
+ mLooper.moveTimeForward(299);
+ mLooper.dispatchAll();
+ verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+
+ mLooper.moveTimeForward(1);
+ mLooper.dispatchAll();
+ verify(mCallback).onClientFinished(any(), eq(true));
+ }
+
private FingerprintAuthenticationClient createClient() throws RemoteException {
return createClient(100 /* version */, true /* allowBackgroundAuthentication */);
}
@@ -496,7 +643,7 @@
null /* taskStackListener */, mLockoutCache,
mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication,
mSensorProps,
- new Handler(mLooper.getLooper())) {
+ new Handler(mLooper.getLooper()), mClock) {
@Override
protected ActivityTaskManager getActivityTaskManager() {
return mActivityTaskManager;
diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
index 356600d..0642228 100644
--- a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -885,6 +886,29 @@
assertNull(mInjector.mLightSensor);
}
+ @Test
+ public void testOnlyOneReceiverRegistered() {
+ assertNull(mInjector.mLightSensor);
+ assertNull(mInjector.mSensorListener);
+ startTracker(mTracker, 0.3f, false);
+
+ assertNotNull(mInjector.mLightSensor);
+ assertNotNull(mInjector.mSensorListener);
+ Sensor registeredLightSensor = mInjector.mLightSensor;
+ SensorEventListener registeredSensorListener = mInjector.mSensorListener;
+
+ mTracker.start(0.3f);
+ assertSame(registeredLightSensor, mInjector.mLightSensor);
+ assertSame(registeredSensorListener, mInjector.mSensorListener);
+
+ mTracker.stop();
+ assertNull(mInjector.mLightSensor);
+ assertNull(mInjector.mSensorListener);
+
+ // mInjector asserts that we aren't removing a null receiver
+ mTracker.stop();
+ }
+
private InputStream getInputStream(String data) {
return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
}
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
index a719f52..30024fb 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -49,6 +49,13 @@
@Presubmit
@RunWith(AndroidJUnit4.class)
public final class DisplayDeviceConfigTest {
+ private static final int DEFAULT_PEAK_REFRESH_RATE = 75;
+ private static final int DEFAULT_REFRESH_RATE = 120;
+ private static final int[] LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{10, 30};
+ private static final int[] LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{1, 21};
+ private static final int[] HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{160};
+ private static final int[] HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{30000};
+
private DisplayDeviceConfig mDisplayDeviceConfig;
private static final float ZERO_DELTA = 0.0f;
private static final float SMALL_DELTA = 0.0001f;
@@ -204,6 +211,16 @@
assertArrayEquals(new float[]{29, 30, 31},
mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
+ assertEquals(mDisplayDeviceConfig.getDefaultRefreshRate(), DEFAULT_REFRESH_RATE);
+ assertEquals(mDisplayDeviceConfig.getDefaultPeakRefreshRate(), DEFAULT_PEAK_REFRESH_RATE);
+ assertArrayEquals(mDisplayDeviceConfig.getLowDisplayBrightnessThresholds(),
+ LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
+ assertArrayEquals(mDisplayDeviceConfig.getLowAmbientBrightnessThresholds(),
+ LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
+ assertArrayEquals(mDisplayDeviceConfig.getHighDisplayBrightnessThresholds(),
+ HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
+ assertArrayEquals(mDisplayDeviceConfig.getHighAmbientBrightnessThresholds(),
+ HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
// Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping,
// HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
}
@@ -465,6 +482,21 @@
when(mResources.getIntArray(R.array.config_screenDarkeningThresholds))
.thenReturn(new int[]{370, 380, 390});
+ // Configs related to refresh rates and blocking zones
+ when(mResources.getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate))
+ .thenReturn(DEFAULT_PEAK_REFRESH_RATE);
+ when(mResources.getInteger(com.android.internal.R.integer.config_defaultRefreshRate))
+ .thenReturn(DEFAULT_REFRESH_RATE);
+ when(mResources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate))
+ .thenReturn(LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
+ when(mResources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate))
+ .thenReturn(LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
+ when(mResources.getIntArray(
+ R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate))
+ .thenReturn(HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
+ when(mResources.getIntArray(
+ R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate))
+ .thenReturn(HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true);
}
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index 968e1d8..18dd264 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -1853,6 +1853,20 @@
assertNull(vote);
}
+ @Test
+ public void testNotifyDefaultDisplayDeviceUpdated() {
+ DisplayDeviceConfig displayDeviceConfig = mock(DisplayDeviceConfig.class);
+ when(displayDeviceConfig.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{});
+ when(displayDeviceConfig.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{});
+ when(displayDeviceConfig.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{});
+ when(displayDeviceConfig.getHighAmbientBrightnessThresholds()).thenReturn(new int[]{});
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ director.defaultDisplayDeviceUpdated(displayDeviceConfig);
+ verify(displayDeviceConfig).getDefaultRefreshRate();
+ verify(displayDeviceConfig).getDefaultPeakRefreshRate();
+ }
+
private Temperature getSkinTemp(@Temperature.ThrottlingStatus int status) {
return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status);
}
diff --git a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
index 9fe8609c..3b0a22f 100644
--- a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
@@ -275,6 +275,75 @@
assertNull(mDataStore.getBrightnessConfiguration(userSerial));
}
+ @Test
+ public void testStoreAndRestoreResolution() {
+ final String uniqueDisplayId = "test:123";
+ DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
+ @Override
+ public boolean hasStableUniqueId() {
+ return true;
+ }
+
+ @Override
+ public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+ return null;
+ }
+ };
+ int width = 35;
+ int height = 45;
+ mDataStore.loadIfNeeded();
+ mDataStore.setUserPreferredResolution(testDisplayDevice, width, height);
+
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ mInjector.setWriteStream(baos);
+ mDataStore.saveIfNeeded();
+ mTestLooper.dispatchAll();
+ assertTrue(mInjector.wasWriteSuccessful());
+ TestInjector newInjector = new TestInjector();
+ PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ newInjector.setReadStream(bais);
+ newDataStore.loadIfNeeded();
+ assertNotNull(newDataStore.getUserPreferredResolution(testDisplayDevice));
+ assertEquals(35, newDataStore.getUserPreferredResolution(testDisplayDevice).x);
+ assertEquals(35, mDataStore.getUserPreferredResolution(testDisplayDevice).x);
+ assertEquals(45, newDataStore.getUserPreferredResolution(testDisplayDevice).y);
+ assertEquals(45, mDataStore.getUserPreferredResolution(testDisplayDevice).y);
+ }
+
+ @Test
+ public void testStoreAndRestoreRefreshRate() {
+ final String uniqueDisplayId = "test:123";
+ DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
+ @Override
+ public boolean hasStableUniqueId() {
+ return true;
+ }
+
+ @Override
+ public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+ return null;
+ }
+ };
+ float refreshRate = 85.3f;
+ mDataStore.loadIfNeeded();
+ mDataStore.setUserPreferredRefreshRate(testDisplayDevice, refreshRate);
+
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ mInjector.setWriteStream(baos);
+ mDataStore.saveIfNeeded();
+ mTestLooper.dispatchAll();
+ assertTrue(mInjector.wasWriteSuccessful());
+ TestInjector newInjector = new TestInjector();
+ PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ newInjector.setReadStream(bais);
+ newDataStore.loadIfNeeded();
+ assertNotNull(newDataStore.getUserPreferredRefreshRate(testDisplayDevice));
+ assertEquals(85.3f, mDataStore.getUserPreferredRefreshRate(testDisplayDevice), 01.f);
+ assertEquals(85.3f, newDataStore.getUserPreferredRefreshRate(testDisplayDevice), 0.1f);
+ }
+
public class TestInjector extends PersistentDataStore.Injector {
private InputStream mReadStream;
private OutputStream mWriteStream;
diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
new file mode 100644
index 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/power/PowerGroupTest.java b/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java
index d8c9c34..e3ca170 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java
@@ -116,7 +116,7 @@
@Test
public void testDreamPowerGroup() {
assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
- mPowerGroup.dreamLocked(TIMESTAMP1, UID);
+ mPowerGroup.dreamLocked(TIMESTAMP1, UID, /* allowWake= */ false);
assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
assertThat(mPowerGroup.isSandmanSummonedLocked()).isTrue();
verify(mWakefulnessCallbackMock).onWakefulnessChangedLocked(eq(GROUP_ID),
@@ -172,7 +172,7 @@
eq(UID), /* opUid= */ anyInt(), /* opPackageName= */ isNull(),
/* details= */ isNull());
assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DOZING);
- assertThat(mPowerGroup.dreamLocked(TIMESTAMP2, UID)).isFalse();
+ assertThat(mPowerGroup.dreamLocked(TIMESTAMP2, UID, /* allowWake= */ false)).isFalse();
assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DOZING);
verify(mWakefulnessCallbackMock, never()).onWakefulnessChangedLocked(
eq(GROUP_ID), /* wakefulness= */ eq(WAKEFULNESS_DREAMING), eq(TIMESTAMP2),
@@ -181,6 +181,22 @@
}
@Test
+ public void testDreamPowerGroupWhenNotAwakeShouldWake() {
+ mPowerGroup.dozeLocked(TIMESTAMP1, UID, GO_TO_SLEEP_REASON_TIMEOUT);
+ verify(mWakefulnessCallbackMock).onWakefulnessChangedLocked(eq(GROUP_ID),
+ eq(WAKEFULNESS_DOZING), eq(TIMESTAMP1), eq(GO_TO_SLEEP_REASON_TIMEOUT),
+ eq(UID), /* opUid= */ anyInt(), /* opPackageName= */ isNull(),
+ /* details= */ isNull());
+ assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DOZING);
+ assertThat(mPowerGroup.dreamLocked(TIMESTAMP2, UID, /* allowWake= */ true)).isTrue();
+ assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
+ verify(mWakefulnessCallbackMock).onWakefulnessChangedLocked(
+ eq(GROUP_ID), /* wakefulness= */ eq(WAKEFULNESS_DREAMING), eq(TIMESTAMP2),
+ /* reason= */ anyInt(), eq(UID), /* opUid= */ anyInt(), /* opPackageName= */ any(),
+ /* details= */ any());
+ }
+
+ @Test
public void testLastWakeAndSleepTimeIsUpdated() {
assertThat(mPowerGroup.getLastWakeTimeLocked()).isEqualTo(TIMESTAMP_CREATE);
assertThat(mPowerGroup.getLastSleepTimeLocked()).isEqualTo(TIMESTAMP_CREATE);
@@ -514,7 +530,7 @@
.setBatterySaverEnabled(batterySaverEnabled)
.setBrightnessFactor(brightnessFactor)
.build();
- mPowerGroup.dreamLocked(TIMESTAMP1, UID);
+ mPowerGroup.dreamLocked(TIMESTAMP1, UID, /* allowWake= */ false);
assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
mPowerGroup.setWakeLockSummaryLocked(WAKE_LOCK_SCREEN_BRIGHT);
mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
index 2a6e6d8..f5ed41a 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -393,6 +393,12 @@
.thenReturn(minimumScreenOffTimeoutConfigMillis);
}
+ private void setDreamsDisabledByAmbientModeSuppressionConfig(boolean disable) {
+ when(mResourcesSpy.getBoolean(
+ com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig))
+ .thenReturn(disable);
+ }
+
private void advanceTime(long timeMs) {
mClock.fastForward(timeMs);
mTestLooper.dispatchAll();
@@ -612,6 +618,31 @@
assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
}
+ /**
+ * Tests that dreaming continues when undocking and configured to do so.
+ */
+ @Test
+ public void testWakefulnessDream_shouldKeepDreamingWhenUndocked() {
+ createService();
+ startSystem();
+
+ when(mResourcesSpy.getBoolean(
+ com.android.internal.R.bool.config_keepDreamingWhenUndocking))
+ .thenReturn(true);
+ mService.readConfigurationLocked();
+
+ when(mBatteryManagerInternalMock.getPlugType())
+ .thenReturn(BatteryManager.BATTERY_PLUGGED_DOCK);
+ setPluggedIn(true);
+
+ forceAwake(); // Needs to be awake first before it can dream.
+ forceDream();
+ when(mBatteryManagerInternalMock.getPlugType()).thenReturn(0);
+ setPluggedIn(false);
+
+ assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
+ }
+
@Test
public void testWakefulnessDoze_goToSleep() {
createService();
@@ -765,6 +796,91 @@
assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
}
+ @SuppressWarnings("GuardedBy")
+ @Test
+ public void testAmbientSuppression_disablesDreamingAndWakesDevice() {
+ Settings.Secure.putInt(mContextSpy.getContentResolver(),
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1);
+ Settings.Secure.putInt(mContextSpy.getContentResolver(),
+ Settings.Secure.SCREENSAVER_ENABLED, 1);
+
+ setDreamsDisabledByAmbientModeSuppressionConfig(true);
+ setMinimumScreenOffTimeoutConfig(10000);
+ createService();
+ startSystem();
+
+ doAnswer(inv -> {
+ when(mDreamManagerInternalMock.isDreaming()).thenReturn(true);
+ return null;
+ }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString());
+
+ setPluggedIn(true);
+ // Allow asynchronous sandman calls to execute.
+ advanceTime(10000);
+
+ forceDream();
+ assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
+ mService.getBinderServiceInstance().suppressAmbientDisplay("test", true);
+ advanceTime(50);
+ assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ public void testAmbientSuppressionDisabled_shouldNotWakeDevice() {
+ Settings.Secure.putInt(mContextSpy.getContentResolver(),
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1);
+ Settings.Secure.putInt(mContextSpy.getContentResolver(),
+ Settings.Secure.SCREENSAVER_ENABLED, 1);
+
+ setDreamsDisabledByAmbientModeSuppressionConfig(false);
+ setMinimumScreenOffTimeoutConfig(10000);
+ createService();
+ startSystem();
+
+ doAnswer(inv -> {
+ when(mDreamManagerInternalMock.isDreaming()).thenReturn(true);
+ return null;
+ }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString());
+
+ setPluggedIn(true);
+ // Allow asynchronous sandman calls to execute.
+ advanceTime(10000);
+
+ forceDream();
+ assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
+ mService.getBinderServiceInstance().suppressAmbientDisplay("test", true);
+ advanceTime(50);
+ assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
+ }
+
+ @Test
+ public void testAmbientSuppression_doesNotAffectDreamForcing() {
+ Settings.Secure.putInt(mContextSpy.getContentResolver(),
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1);
+ Settings.Secure.putInt(mContextSpy.getContentResolver(),
+ Settings.Secure.SCREENSAVER_ENABLED, 1);
+
+ setDreamsDisabledByAmbientModeSuppressionConfig(true);
+ setMinimumScreenOffTimeoutConfig(10000);
+ createService();
+ startSystem();
+
+ doAnswer(inv -> {
+ when(mDreamManagerInternalMock.isDreaming()).thenReturn(true);
+ return null;
+ }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString());
+
+ mService.getBinderServiceInstance().suppressAmbientDisplay("test", true);
+ setPluggedIn(true);
+ // Allow asynchronous sandman calls to execute.
+ advanceTime(10000);
+
+ // Verify that forcing dream still works even though ambient display is suppressed
+ forceDream();
+ assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
+ }
+
@Test
public void testSetDozeOverrideFromDreamManager_triggersSuspendBlocker() {
final String suspendBlockerName = "PowerManagerService.Display";
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/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
index 91c2fe0..8e81e2d 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
@@ -1371,6 +1371,39 @@
verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
}
+ @Test
+ public void dreamWhenDocked_ambientModeSuppressed_suppressionEnabled() {
+ mUiManagerService.setStartDreamImmediatelyOnDock(true);
+ mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true);
+
+ when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true);
+ triggerDockIntent();
+ verifyAndSendResultBroadcast();
+ verify(mInjector, never()).startDreamWhenDockedIfAppropriate(mContext);
+ }
+
+ @Test
+ public void dreamWhenDocked_ambientModeSuppressed_suppressionDisabled() {
+ mUiManagerService.setStartDreamImmediatelyOnDock(true);
+ mUiManagerService.setDreamsDisabledByAmbientModeSuppression(false);
+
+ when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true);
+ triggerDockIntent();
+ verifyAndSendResultBroadcast();
+ verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
+ }
+
+ @Test
+ public void dreamWhenDocked_ambientModeNotSuppressed_suppressionEnabled() {
+ mUiManagerService.setStartDreamImmediatelyOnDock(true);
+ mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true);
+
+ when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(false);
+ triggerDockIntent();
+ verifyAndSendResultBroadcast();
+ verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
+ }
+
private void triggerDockIntent() {
final Intent dockedIntent =
new Intent(Intent.ACTION_DOCK_EVENT)
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
index 57e403c..bf66dee 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/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index d544744..462957a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -2319,6 +2319,22 @@
assertTrue(activity1.getTask().getTaskInfo().launchCookies.contains(launchCookie));
}
+ @Test
+ public void testOrientationForScreenOrientationBehind() {
+ final Task task = createTask(mDisplayContent);
+ // Activity below
+ new ActivityBuilder(mAtm)
+ .setTask(task)
+ .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
+ .build();
+ final ActivityRecord activityTop = new ActivityBuilder(mAtm)
+ .setTask(task)
+ .setScreenOrientation(SCREEN_ORIENTATION_BEHIND)
+ .build();
+ final int topOrientation = activityTop.getRequestedConfigurationOrientation();
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, topOrientation);
+ }
+
private void verifyProcessInfoUpdate(ActivityRecord activity, State state,
boolean shouldUpdate, boolean activityChange) {
reset(activity.app);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index d5e336b..eed32d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -40,14 +40,18 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
+import android.app.ActivityOptions;
import android.app.WaitResult;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
+import android.os.Binder;
import android.os.ConditionVariable;
+import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.view.Display;
@@ -308,4 +312,40 @@
waitHandlerIdle(mAtm.mH);
verify(mRootWindowContainer, timeout(TIMEOUT_MS)).startHomeOnEmptyDisplays("userUnlocked");
}
+
+ /** Verifies that launch from recents sets the launch cookie on the activity. */
+ @Test
+ public void testStartActivityFromRecents_withLaunchCookie() {
+ final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+ IBinder launchCookie = new Binder("test_launch_cookie");
+ ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchCookie(launchCookie);
+ SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(options.toBundle());
+
+ doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(),
+ anyInt(), any());
+
+ mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions);
+
+ assertThat(activity.mLaunchCookie).isEqualTo(launchCookie);
+ verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions));
+ }
+
+ /** Verifies that launch from recents doesn't set the launch cookie on the activity. */
+ @Test
+ public void testStartActivityFromRecents_withoutLaunchCookie() {
+ final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+ SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(
+ ActivityOptions.makeBasic().toBundle());
+
+ doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(),
+ anyInt(), any());
+
+ mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions);
+
+ assertThat(activity.mLaunchCookie).isNull();
+ verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions));
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index 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/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
new file mode 100644
index 0000000..7830e90
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.util.DisplayMetrics.DENSITY_DEFAULT;
+
+import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS;
+import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY;
+import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_DONE;
+import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.LaunchParamsController.LaunchParamsModifier.Result;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for desktop mode task bounds.
+ *
+ * Build/Install/Run:
+ * atest WmTests:DesktopModeLaunchParamsModifierTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase {
+
+ private ActivityRecord mActivity;
+
+ private DesktopModeLaunchParamsModifier mTarget;
+
+ private LaunchParamsController.LaunchParams mCurrent;
+ private LaunchParamsController.LaunchParams mResult;
+
+ @Before
+ public void setUp() throws Exception {
+ mActivity = new ActivityBuilder(mAtm).build();
+ mTarget = new DesktopModeLaunchParamsModifier();
+ mCurrent = new LaunchParamsController.LaunchParams();
+ mCurrent.reset();
+ mResult = new LaunchParamsController.LaunchParams();
+ mResult.reset();
+ }
+
+ @Test
+ public void testReturnsSkipIfTaskIsNull() {
+ assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(null).calculate());
+ }
+
+ @Test
+ public void testReturnsSkipIfNotBoundsPhase() {
+ final Task task = new TaskBuilder(mSupervisor).build();
+ assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).setPhase(
+ PHASE_DISPLAY).calculate());
+ }
+
+ @Test
+ public void testReturnsSkipIfTaskNotInFreeform() {
+ final Task task = new TaskBuilder(mSupervisor).setWindowingMode(
+ WINDOWING_MODE_FULLSCREEN).build();
+ assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).calculate());
+ }
+
+ @Test
+ public void testReturnsSkipIfCurrentParamsHasBounds() {
+ final Task task = new TaskBuilder(mSupervisor).setWindowingMode(
+ WINDOWING_MODE_FREEFORM).build();
+ mCurrent.mBounds.set(/* left */ 0, /* top */ 0, /* right */ 100, /* bottom */ 100);
+ assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).calculate());
+ }
+
+ @Test
+ public void testUsesDefaultBounds() {
+ final Task task = new TaskBuilder(mSupervisor).setWindowingMode(
+ WINDOWING_MODE_FREEFORM).build();
+ assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(dpiToPx(task, 840), mResult.mBounds.width());
+ assertEquals(dpiToPx(task, 630), mResult.mBounds.height());
+ }
+
+ @Test
+ public void testUsesDisplayAreaAndWindowingModeFromSource() {
+ final Task task = new TaskBuilder(mSupervisor).setWindowingMode(
+ WINDOWING_MODE_FREEFORM).build();
+ TaskDisplayArea mockTaskDisplayArea = mock(TaskDisplayArea.class);
+ mCurrent.mPreferredTaskDisplayArea = mockTaskDisplayArea;
+ mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM;
+
+ assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(mockTaskDisplayArea, mResult.mPreferredTaskDisplayArea);
+ assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode);
+ }
+
+ private int dpiToPx(Task task, int dpi) {
+ float density = (float) task.getConfiguration().densityDpi / DENSITY_DEFAULT;
+ return (int) (dpi * density + 0.5f);
+ }
+
+ private class CalculateRequestBuilder {
+ private Task mTask;
+ private int mPhase = PHASE_BOUNDS;
+ private final ActivityRecord mActivity =
+ DesktopModeLaunchParamsModifierTests.this.mActivity;
+ private final LaunchParamsController.LaunchParams mCurrentParams = mCurrent;
+ private final LaunchParamsController.LaunchParams mOutParams = mResult;
+
+ private CalculateRequestBuilder setTask(Task task) {
+ mTask = task;
+ return this;
+ }
+
+ private CalculateRequestBuilder setPhase(int phase) {
+ mPhase = phase;
+ return this;
+ }
+
+ @Result
+ private int calculate() {
+ return mTarget.onCalculate(mTask, /* layout*/ null, mActivity, /* source */
+ null, /* options */ null, /* request */ null, mPhase, mCurrentParams,
+ mOutParams);
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java
new file mode 100644
index 0000000..86732c9
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.function.Consumer;
+
+/**
+ * Test class for {@link DeviceStateController}.
+ *
+ * Build/Install/Run:
+ * atest WmTests:DeviceStateControllerTests
+ */
+@SmallTest
+@Presubmit
+public class DeviceStateControllerTests {
+
+ private DeviceStateController.FoldStateListener mFoldStateListener;
+ private DeviceStateController mTarget;
+ private DeviceStateControllerBuilder mBuilder;
+
+ private Context mMockContext;
+ private Handler mMockHandler;
+ private Resources mMockRes;
+ private DeviceStateManager mMockDeviceStateManager;
+
+ private Consumer<DeviceStateController.FoldState> mDelegate;
+ private DeviceStateController.FoldState mCurrentState = DeviceStateController.FoldState.UNKNOWN;
+
+ @Before
+ public void setUp() {
+ mBuilder = new DeviceStateControllerBuilder();
+ mCurrentState = DeviceStateController.FoldState.UNKNOWN;
+ }
+
+ private void initialize(boolean supportFold, boolean supportHalfFold) throws Exception {
+ mBuilder.setSupportFold(supportFold, supportHalfFold);
+ mDelegate = (newFoldState) -> {
+ mCurrentState = newFoldState;
+ };
+ mBuilder.setDelegate(mDelegate);
+ mBuilder.build();
+ verifyFoldStateListenerRegistration(1);
+ }
+
+ @Test
+ public void testInitialization() throws Exception {
+ initialize(true /* supportFold */, true /* supportHalfFolded */);
+ mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+ }
+
+ @Test
+ public void testInitializationWithNoFoldSupport() throws Exception {
+ initialize(false /* supportFold */, false /* supportHalfFolded */);
+ mFoldStateListener.onStateChanged(mFoldedStates[0]);
+ // Note that the folded state is ignored.
+ assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+ }
+
+ @Test
+ public void testWithFoldSupported() throws Exception {
+ initialize(true /* supportFold */, false /* supportHalfFolded */);
+ mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+ mFoldStateListener.onStateChanged(mFoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED);
+ mFoldStateListener.onStateChanged(mHalfFoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); // Ignored
+ }
+
+ @Test
+ public void testWithHalfFoldSupported() throws Exception {
+ initialize(true /* supportFold */, true /* supportHalfFolded */);
+ mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+ mFoldStateListener.onStateChanged(mFoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED);
+ mFoldStateListener.onStateChanged(mHalfFoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.HALF_FOLDED);
+ }
+
+
+ private final int[] mFoldedStates = {0};
+ private final int[] mUnfoldedStates = {1};
+ private final int[] mHalfFoldedStates = {2};
+
+
+ private void verifyFoldStateListenerRegistration(int numOfInvocation) {
+ final ArgumentCaptor<DeviceStateController.FoldStateListener> listenerCaptor =
+ ArgumentCaptor.forClass(DeviceStateController.FoldStateListener.class);
+ verify(mMockDeviceStateManager, times(numOfInvocation)).registerCallback(
+ any(),
+ listenerCaptor.capture());
+ if (numOfInvocation > 0) {
+ mFoldStateListener = listenerCaptor.getValue();
+ }
+ }
+
+ private class DeviceStateControllerBuilder {
+ private boolean mSupportFold = false;
+ private boolean mSupportHalfFold = false;
+ private Consumer<DeviceStateController.FoldState> mDelegate;
+
+ DeviceStateControllerBuilder setSupportFold(
+ boolean supportFold, boolean supportHalfFold) {
+ mSupportFold = supportFold;
+ mSupportHalfFold = supportHalfFold;
+ return this;
+ }
+
+ DeviceStateControllerBuilder setDelegate(
+ Consumer<DeviceStateController.FoldState> delegate) {
+ mDelegate = delegate;
+ return this;
+ }
+
+ private void mockFold(boolean enableFold, boolean enableHalfFold) {
+ if (enableFold) {
+ when(mMockContext.getResources().getIntArray(
+ com.android.internal.R.array.config_foldedDeviceStates))
+ .thenReturn(mFoldedStates);
+ }
+ if (enableHalfFold) {
+ when(mMockContext.getResources().getIntArray(
+ com.android.internal.R.array.config_halfFoldedDeviceStates))
+ .thenReturn(mHalfFoldedStates);
+ }
+ }
+
+ private void build() throws Exception {
+ mMockContext = mock(Context.class);
+ mMockRes = mock(Resources.class);
+ when(mMockContext.getResources()).thenReturn((mMockRes));
+ mMockDeviceStateManager = mock(DeviceStateManager.class);
+ when(mMockContext.getSystemService(DeviceStateManager.class))
+ .thenReturn(mMockDeviceStateManager);
+ mockFold(mSupportFold, mSupportHalfFold);
+ mMockHandler = mock(Handler.class);
+ mTarget = new DeviceStateController(mMockContext, mMockHandler, mDelegate);
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index 89f7111..b45c37f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -28,6 +28,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.atLeast;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.atMost;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
@@ -103,7 +104,7 @@
private Context mMockContext;
private Resources mMockRes;
private SensorManager mMockSensorManager;
- private Sensor mFakeSensor;
+ private Sensor mFakeOrientationSensor;
private DisplayWindowSettings mMockDisplayWindowSettings;
private ContentResolver mMockResolver;
private FakeSettingsProvider mFakeSettingsProvider;
@@ -323,7 +324,7 @@
waitForUiHandler();
verify(mMockSensorManager, times(numOfInvocation)).registerListener(
listenerCaptor.capture(),
- same(mFakeSensor),
+ same(mFakeOrientationSensor),
anyInt(),
any());
if (numOfInvocation > 0) {
@@ -460,7 +461,7 @@
SensorEvent.class.getDeclaredConstructor(int.class);
constructor.setAccessible(true);
final SensorEvent event = constructor.newInstance(1);
- event.sensor = mFakeSensor;
+ event.sensor = mFakeOrientationSensor;
event.values[0] = rotation;
event.timestamp = SystemClock.elapsedRealtimeNanos();
return event;
@@ -691,6 +692,43 @@
SCREEN_ORIENTATION_SENSOR, Surface.ROTATION_0));
}
+ // ====================================================
+ // Tests for half-fold auto-rotate override of rotation
+ // ====================================================
+ @Test
+ public void testUpdatesRotationWhenSensorUpdates_RotationLocked_HalfFolded() throws Exception {
+ mBuilder.setSupportHalfFoldAutoRotateOverride(true);
+ mBuilder.build();
+ configureDisplayRotation(SCREEN_ORIENTATION_LANDSCAPE, false, false);
+
+ enableOrientationSensor();
+
+ mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN);
+ freezeRotation(Surface.ROTATION_270);
+
+ mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_0));
+ assertTrue(waitForUiHandler());
+ // No rotation...
+ assertEquals(Surface.ROTATION_270, mTarget.rotationForOrientation(
+ SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+
+ // ... until half-fold
+ mTarget.foldStateChanged(DeviceStateController.FoldState.HALF_FOLDED);
+ assertTrue(waitForUiHandler());
+ verify(sMockWm).updateRotation(false, false);
+ assertTrue(waitForUiHandler());
+ assertEquals(Surface.ROTATION_0, mTarget.rotationForOrientation(
+ SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+
+ // ... then transition back to flat
+ mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN);
+ assertTrue(waitForUiHandler());
+ verify(sMockWm, atLeast(1)).updateRotation(false, false);
+ assertTrue(waitForUiHandler());
+ assertEquals(Surface.ROTATION_270, mTarget.rotationForOrientation(
+ SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+ }
+
// =================================
// Tests for Policy based Rotation
// =================================
@@ -884,6 +922,7 @@
private class DisplayRotationBuilder {
private boolean mIsDefaultDisplay = true;
private boolean mSupportAutoRotation = true;
+ private boolean mSupportHalfFoldAutoRotateOverride = false;
private int mLidOpenRotation = WindowManagerPolicy.WindowManagerFuncs.LID_ABSENT;
private int mCarDockRotation;
@@ -920,6 +959,12 @@
return this;
}
+ private DisplayRotationBuilder setSupportHalfFoldAutoRotateOverride(
+ boolean supportHalfFoldAutoRotateOverride) {
+ mSupportHalfFoldAutoRotateOverride = supportHalfFoldAutoRotateOverride;
+ return this;
+ }
+
private void captureObservers() {
ArgumentCaptor<ContentObserver> captor = ArgumentCaptor.forClass(
ContentObserver.class);
@@ -1032,9 +1077,13 @@
mMockSensorManager = mock(SensorManager.class);
when(mMockContext.getSystemService(Context.SENSOR_SERVICE))
.thenReturn(mMockSensorManager);
- mFakeSensor = createSensor(Sensor.TYPE_DEVICE_ORIENTATION);
+ mFakeOrientationSensor = createSensor(Sensor.TYPE_DEVICE_ORIENTATION);
when(mMockSensorManager.getSensorList(Sensor.TYPE_DEVICE_ORIENTATION)).thenReturn(
- Collections.singletonList(mFakeSensor));
+ Collections.singletonList(mFakeOrientationSensor));
+
+ when(mMockContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_windowManagerHalfFoldAutoRotateOverride))
+ .thenReturn(mSupportHalfFoldAutoRotateOverride);
mMockResolver = mock(ContentResolver.class);
when(mMockContext.getContentResolver()).thenReturn(mMockResolver);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index 601cf15..64c1e05 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -72,6 +72,7 @@
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.graphics.Rect;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import android.util.MergedConfiguration;
@@ -377,6 +378,33 @@
assertEquals(WINDOWING_MODE_FULLSCREEN, fullscreenTask.getWindowingMode());
}
+ @Test
+ public void testMovingEmbeddedActivityToPip() {
+ final Rect taskBounds = new Rect(0, 0, 800, 1000);
+ final Rect taskFragmentBounds = new Rect(0, 0, 400, 1000);
+ final Task task = mRootWindowContainer.getDefaultTaskDisplayArea().createRootTask(
+ WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ task.setBounds(taskBounds);
+ assertEquals(taskBounds, task.getBounds());
+ final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
+ .setParentTask(task)
+ .createActivityCount(2)
+ .setBounds(taskFragmentBounds)
+ .build();
+ assertEquals(taskFragmentBounds, taskFragment.getBounds());
+ final ActivityRecord topActivity = taskFragment.getTopMostActivity();
+
+ // Move the top activity to pinned root task.
+ mRootWindowContainer.moveActivityToPinnedRootTask(topActivity,
+ null /* launchIntoPipHostActivity */, "test");
+
+ final Task pinnedRootTask = task.getDisplayArea().getRootPinnedTask();
+
+ // Ensure the initial bounds of the PiP Task is the same as the TaskFragment.
+ ensureTaskPlacement(pinnedRootTask, topActivity);
+ assertEquals(taskFragmentBounds, pinnedRootTask.getBounds());
+ }
+
private static void ensureTaskPlacement(Task task, ActivityRecord... activities) {
final ArrayList<ActivityRecord> taskActivities = new ArrayList<>();
diff --git a/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java
index e57ad5d..24e932f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java
@@ -57,10 +57,20 @@
.setLaunchTaskDisplayArea(token)
.setLaunchDisplayId(launchDisplayId)
.setCallerDisplayId(callerDisplayId))
- .selectiveCloneDisplayOptions();
+ .selectiveCloneLaunchOptions();
assertSame(clone.getOriginalOptions().getLaunchTaskDisplayArea(), token);
assertEquals(clone.getOriginalOptions().getLaunchDisplayId(), launchDisplayId);
assertEquals(clone.getOriginalOptions().getCallerDisplayId(), callerDisplayId);
}
+
+ @Test
+ public void test_selectiveCloneLunchRootTask() {
+ final WindowContainerToken token = mock(WindowContainerToken.class);
+ final SafeActivityOptions clone = new SafeActivityOptions(ActivityOptions.makeBasic()
+ .setLaunchRootTask(token))
+ .selectiveCloneLaunchOptions();
+
+ assertSame(clone.getOriginalOptions().getLaunchRootTask(), token);
+ }
}
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/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 7342c49..9fd0850 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -1322,6 +1322,35 @@
}
@Test
+ public void testReparentChangeLastParent() {
+ final Transition transition = createTestTransition(TRANSIT_CHANGE);
+ final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
+ final ArraySet<WindowContainer> participants = transition.mParticipants;
+
+ // Reparent activity in transition.
+ final Task lastParent = createTask(mDisplayContent);
+ final Task newParent = createTask(mDisplayContent);
+ final ActivityRecord activity = createActivityRecord(lastParent);
+ activity.mVisibleRequested = true;
+ // Skip manipulate the SurfaceControl.
+ doNothing().when(activity).setDropInputMode(anyInt());
+ changes.put(activity, new Transition.ChangeInfo(activity));
+ activity.reparent(newParent, POSITION_TOP);
+ activity.mVisibleRequested = false;
+
+ participants.add(activity);
+ final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+ participants, changes);
+ final TransitionInfo info = Transition.calculateTransitionInfo(
+ transition.mType, 0 /* flags */, targets, changes, mMockT);
+
+ // Change contains last parent info.
+ assertEquals(1, info.getChanges().size());
+ assertEquals(lastParent.mRemoteToken.toWindowContainerToken(),
+ info.getChanges().get(0).getLastParent());
+ }
+
+ @Test
public void testIncludeEmbeddedActivityReparent() {
final Transition transition = createTestTransition(TRANSIT_OPEN);
final Task task = createTask(mDisplayContent);
diff --git a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java
index eafcef2..1e74451 100644
--- a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java
+++ b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java
@@ -210,21 +210,15 @@
final int translatedAppUid =
getAppUidByComponentName(getContext(), componentName, getUserId());
final String packageName = componentName.getPackageName();
- if (activityDestroyed) {
- // In the Activity destroy case, we only calls onTranslationFinished() in
- // non-finisTranslation() state. If there is a finisTranslation() calls by apps, we
- // should remove the waiting callback to avoid callback twice.
+ // In the Activity destroyed case, we only call onTranslationFinished() in
+ // non-finishTranslation() state. If there is a finishTranslation() call by apps, we
+ // should remove the waiting callback to avoid invoking callbacks twice.
+ if (activityDestroyed || mWaitingFinishedCallbackActivities.contains(token)) {
invokeCallbacks(STATE_UI_TRANSLATION_FINISHED,
/* sourceSpec= */ null, /* targetSpec= */ null,
packageName, translatedAppUid);
mWaitingFinishedCallbackActivities.remove(token);
- } else {
- if (mWaitingFinishedCallbackActivities.contains(token)) {
- invokeCallbacks(STATE_UI_TRANSLATION_FINISHED,
- /* sourceSpec= */ null, /* targetSpec= */ null,
- packageName, translatedAppUid);
- mWaitingFinishedCallbackActivities.remove(token);
- }
+ mActiveTranslations.remove(token);
}
}
@@ -237,6 +231,9 @@
// Activity is the new Activity, the original Activity is paused in the same task.
// To make sure the operation still work, we use the token to find the target Activity in
// this task, not the top Activity only.
+ //
+ // Note: getAttachedNonFinishingActivityForTask() takes the shareable activity token. We
+ // call this method so that we can get the regular activity token below.
ActivityTokens candidateActivityTokens =
mActivityTaskManagerInternal.getAttachedNonFinishingActivityForTask(taskId, token);
if (candidateActivityTokens == null) {
@@ -263,27 +260,27 @@
getAppUidByComponentName(getContext(), componentName, getUserId());
String packageName = componentName.getPackageName();
- invokeCallbacksIfNecessaryLocked(state, sourceSpec, targetSpec, packageName, activityToken,
+ invokeCallbacksIfNecessaryLocked(state, sourceSpec, targetSpec, packageName, token,
translatedAppUid);
- updateActiveTranslationsLocked(state, sourceSpec, targetSpec, packageName, activityToken,
+ updateActiveTranslationsLocked(state, sourceSpec, targetSpec, packageName, token,
translatedAppUid);
}
@GuardedBy("mLock")
private void updateActiveTranslationsLocked(int state, TranslationSpec sourceSpec,
- TranslationSpec targetSpec, String packageName, IBinder activityToken,
+ TranslationSpec targetSpec, String packageName, IBinder shareableActivityToken,
int translatedAppUid) {
// We keep track of active translations and their state so that we can:
// 1. Trigger callbacks that are registered after translation has started.
// See registerUiTranslationStateCallbackLocked().
// 2. NOT trigger callbacks when the state didn't change.
// See invokeCallbacksIfNecessaryLocked().
- ActiveTranslation activeTranslation = mActiveTranslations.get(activityToken);
+ ActiveTranslation activeTranslation = mActiveTranslations.get(shareableActivityToken);
switch (state) {
case STATE_UI_TRANSLATION_STARTED: {
if (activeTranslation == null) {
try {
- activityToken.linkToDeath(this, /* flags= */ 0);
+ shareableActivityToken.linkToDeath(this, /* flags= */ 0);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to call linkToDeath for translated app with uid="
+ translatedAppUid + "; activity is already dead", e);
@@ -294,7 +291,7 @@
packageName, translatedAppUid);
return;
}
- mActiveTranslations.put(activityToken,
+ mActiveTranslations.put(shareableActivityToken,
new ActiveTranslation(sourceSpec, targetSpec, translatedAppUid,
packageName));
}
@@ -317,7 +314,7 @@
case STATE_UI_TRANSLATION_FINISHED: {
if (activeTranslation != null) {
- mActiveTranslations.remove(activityToken);
+ mActiveTranslations.remove(shareableActivityToken);
}
break;
}
@@ -332,12 +329,12 @@
@GuardedBy("mLock")
private void invokeCallbacksIfNecessaryLocked(int state, TranslationSpec sourceSpec,
- TranslationSpec targetSpec, String packageName, IBinder activityToken,
+ TranslationSpec targetSpec, String packageName, IBinder shareableActivityToken,
int translatedAppUid) {
boolean shouldInvokeCallbacks = true;
int stateForCallbackInvocation = state;
- ActiveTranslation activeTranslation = mActiveTranslations.get(activityToken);
+ ActiveTranslation activeTranslation = mActiveTranslations.get(shareableActivityToken);
if (activeTranslation == null) {
if (state != STATE_UI_TRANSLATION_STARTED) {
shouldInvokeCallbacks = false;
@@ -403,14 +400,6 @@
}
}
- if (DEBUG) {
- Slog.d(TAG,
- (shouldInvokeCallbacks ? "" : "NOT ")
- + "Invoking callbacks for translation state="
- + stateForCallbackInvocation + " for app with uid=" + translatedAppUid
- + " packageName=" + packageName);
- }
-
if (shouldInvokeCallbacks) {
invokeCallbacks(stateForCallbackInvocation, sourceSpec, targetSpec, packageName,
translatedAppUid);
@@ -448,7 +437,7 @@
pw.println(waitingFinishCallbackSize);
for (IBinder activityToken : mWaitingFinishedCallbackActivities) {
pw.print(prefix);
- pw.print("activityToken: ");
+ pw.print("shareableActivityToken: ");
pw.println(activityToken);
}
}
@@ -458,7 +447,14 @@
int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName,
int translatedAppUid) {
Bundle result = createResultForCallback(state, sourceSpec, targetSpec, packageName);
- if (mCallbacks.getRegisteredCallbackCount() == 0) {
+ int registeredCallbackCount = mCallbacks.getRegisteredCallbackCount();
+ if (DEBUG) {
+ Slog.d(TAG, "Invoking " + registeredCallbackCount + " callbacks for translation state="
+ + state + " for app with uid=" + translatedAppUid
+ + " packageName=" + packageName);
+ }
+
+ if (registeredCallbackCount == 0) {
return;
}
List<InputMethodInfo> enabledInputMethods = getEnabledInputMethods();
@@ -521,8 +517,10 @@
@GuardedBy("mLock")
public void registerUiTranslationStateCallbackLocked(IRemoteCallback callback, int sourceUid) {
mCallbacks.register(callback, sourceUid);
-
- if (mActiveTranslations.size() == 0) {
+ int numActiveTranslations = mActiveTranslations.size();
+ Slog.i(TAG, "New registered callback for sourceUid=" + sourceUid + " with currently "
+ + numActiveTranslations + " active translations");
+ if (numActiveTranslations == 0) {
return;
}
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;
}
}
diff --git a/tools/xmlpersistence/src/main/kotlin/Generator.kt b/tools/xmlpersistence/src/main/kotlin/Generator.kt
index b2c5f4a..8e62388 100644
--- a/tools/xmlpersistence/src/main/kotlin/Generator.kt
+++ b/tools/xmlpersistence/src/main/kotlin/Generator.kt
@@ -149,6 +149,7 @@
when (field) {
is ClassFieldInfo -> this += field.allClassFields
is ListFieldInfo -> this += field.element.allClassFields
+ else -> {}
}
}
}