Merge "Support starting a shortcut into multi-instances split" into tm-qpr-dev
diff --git a/core/java/android/app/WallpaperColors.java b/core/java/android/app/WallpaperColors.java
index 067a4c3..a34a50c 100644
--- a/core/java/android/app/WallpaperColors.java
+++ b/core/java/android/app/WallpaperColors.java
@@ -27,6 +27,7 @@
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.SystemProperties;
import android.util.Log;
import android.util.MathUtils;
import android.util.Size;
@@ -101,11 +102,13 @@
// Decides when dark theme is optimal for this wallpaper
private static final float DARK_THEME_MEAN_LUMINANCE = 0.3f;
// Minimum mean luminosity that an image needs to have to support dark text
- private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = 0.7f;
+ private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = SystemProperties.getInt(
+ "persist.wallpapercolors.threshold", 70) / 100f;
// We also check if the image has dark pixels in it,
// to avoid bright images with some dark spots.
private static final float DARK_PIXEL_CONTRAST = 5.5f;
- private static final float MAX_DARK_AREA = 0.05f;
+ private static final float MAX_DARK_AREA = SystemProperties.getInt(
+ "persist.wallpapercolors.max_dark_area", 5) / 100f;
private final List<Color> mMainColors;
private final Map<Integer, Integer> mAllColors;
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 80cea55..bbe99f5 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1058,6 +1058,17 @@
public static final long ALWAYS_SANDBOX_DISPLAY_APIS = 185004937L; // buganizer id
/**
+ * This change id excludes the packages it is applied to from ignoreOrientationRequest behaviour
+ * that can be enabled by the device manufacturers for the com.android.server.wm.DisplayArea
+ * or for the whole display.
+ * @hide
+ */
+ @ChangeId
+ @Overridable
+ @Disabled
+ public static final long OVERRIDE_RESPECT_REQUESTED_ORIENTATION = 236283604L; // buganizer id
+
+ /**
* This change id excludes the packages it is applied to from the camera compat force rotation
* treatment. See com.android.server.wm.DisplayRotationCompatPolicy for context.
* @hide
@@ -1238,6 +1249,18 @@
public static final long OVERRIDE_ANY_ORIENTATION = 265464455L;
/**
+ * When enabled, activates OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE,
+ * OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR and OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT
+ * only when an app is connected to the camera. See
+ * com.android.server.wm.DisplayRotationCompatPolicy for more context.
+ * @hide
+ */
+ @ChangeId
+ @Disabled
+ @Overridable
+ public static final long OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA = 265456536L;
+
+ /**
* This override fixes display orientation to landscape natural orientation when a task is
* fullscreen. While display rotation is fixed to landscape, the orientation requested by the
* activity will be still respected by bounds resolution logic. For instance, if an activity
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 4dc6e93..32cf0a7 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -2353,6 +2353,15 @@
final AvailabilityCallback callback = mCallbackMap.keyAt(i);
postSingleUpdate(callback, executor, id, null /*physicalId*/, status);
+
+ // Send the NOT_PRESENT state for unavailable physical cameras
+ if (isAvailable(status) && mUnavailablePhysicalDevices.containsKey(id)) {
+ ArrayList<String> unavailableIds = mUnavailablePhysicalDevices.get(id);
+ for (String unavailableId : unavailableIds) {
+ postSingleUpdate(callback, executor, id, unavailableId,
+ ICameraServiceListener.STATUS_NOT_PRESENT);
+ }
+ }
}
} // onStatusChangedLocked
@@ -2372,9 +2381,8 @@
}
//TODO: Do we need to treat this as error?
- if (!mDeviceStatus.containsKey(id) || !isAvailable(mDeviceStatus.get(id))
- || !mUnavailablePhysicalDevices.containsKey(id)) {
- Log.e(TAG, String.format("Camera %s is not available. Ignore physical camera "
+ if (!mDeviceStatus.containsKey(id) || !mUnavailablePhysicalDevices.containsKey(id)) {
+ Log.e(TAG, String.format("Camera %s is not present. Ignore physical camera "
+ "status change", id));
return;
}
@@ -2399,6 +2407,12 @@
return;
}
+ if (!isAvailable(mDeviceStatus.get(id))) {
+ Log.i(TAG, String.format("Camera %s is not available. Ignore physical camera "
+ + "status change callback(s)", id));
+ return;
+ }
+
final int callbackCount = mCallbackMap.size();
for (int i = 0; i < callbackCount; i++) {
Executor executor = mCallbackMap.valueAt(i);
diff --git a/core/java/android/hardware/fingerprint/IUdfpsHbmListener.aidl b/core/java/android/hardware/fingerprint/IUdfpsHbmListener.aidl
index 9c2aa66..a36ccf6 100644
--- a/core/java/android/hardware/fingerprint/IUdfpsHbmListener.aidl
+++ b/core/java/android/hardware/fingerprint/IUdfpsHbmListener.aidl
@@ -39,5 +39,15 @@
* {@link android.view.Display#getDisplayId()}.
*/
void onHbmDisabled(int displayId);
+
+ /**
+ * To avoid delay in switching refresh rate when activating LHBM, allow screens to request
+ * higher refersh rate if auth is possible on particular screen
+ *
+ * @param displayId The displayId for which the refresh rate should be unset. See
+ * {@link android.view.Display#getDisplayId()}.
+ * @param isPossible If authentication is possible on particualr screen
+ */
+ void onAuthenticationPossible(int displayId, boolean isPossible);
}
diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java
index ac2156e..ca34337 100644
--- a/core/java/android/os/Trace.java
+++ b/core/java/android/os/Trace.java
@@ -109,7 +109,8 @@
public static final long TRACE_TAG_THERMAL = 1L << 27;
private static final long TRACE_TAG_NOT_READY = 1L << 63;
- private static final int MAX_SECTION_NAME_LEN = 127;
+ /** @hide **/
+ public static final int MAX_SECTION_NAME_LEN = 127;
// Must be volatile to avoid word tearing.
// This is only kept in case any apps get this by reflection but do not
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ce4a735..05c1297 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9921,6 +9921,17 @@
"active_unlock_on_unlock_intent_when_biometric_enrolled";
/**
+ * If active unlock triggers on unlock intents, then also request active unlock on
+ * these wake-up reasons. See PowerManager.WakeReason for value mappings.
+ * WakeReasons should be separated by a pipe. For example: "0|3" or "0". If this
+ * setting should be disabled, then this should be set to an empty string. A null value
+ * will use the system default value (WAKE_REASON_UNFOLD_DEVICE).
+ * @hide
+ */
+ public static final String ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS =
+ "active_unlock_wakeups_considered_unlock_intents";
+
+ /**
* Whether the assist gesture should be enabled.
*
* @hide
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 333efad..d7480e5 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -17,6 +17,7 @@
package android.view;
import static android.content.res.Resources.ID_NULL;
+import static android.os.Trace.TRACE_TAG_APP;
import static android.view.ContentInfo.SOURCE_DRAG_AND_DROP;
import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS;
@@ -985,6 +986,22 @@
private static boolean sAcceptZeroSizeDragShadow;
/**
+ * When true, measure and layout passes of all the newly attached views will be logged with
+ * {@link Trace}, so we can better debug jank due to complex view hierarchies.
+ */
+ private static boolean sTraceLayoutSteps;
+
+ /**
+ * When not null, emits a {@link Trace} instant event and the stacktrace every time a relayout
+ * of a class having this name happens.
+ */
+ private static String sTraceRequestLayoutClass;
+
+ /** Used to avoid computing the full strings each time when layout tracing is enabled. */
+ @Nullable
+ private ViewTraversalTracingStrings mTracingStrings;
+
+ /**
* Prior to R, {@link #dispatchApplyWindowInsets} had an issue:
* <p>The modified insets changed by {@link #onApplyWindowInsets} were passed to the
* entire view hierarchy in prefix order, including siblings as well as siblings of parents
@@ -3532,6 +3549,8 @@
* 1 PFLAG4_HAS_TRANSLATION_TRANSIENT_STATE
* 1 PFLAG4_DRAG_A11Y_STARTED
* 1 PFLAG4_AUTO_HANDWRITING_INITIATION_ENABLED
+ * 1 PFLAG4_TRAVERSAL_TRACING_ENABLED
+ * 1 PFLAG4_RELAYOUT_TRACING_ENABLED
* |-------|-------|-------|-------|
*/
@@ -3612,6 +3631,19 @@
* Indicates that the view enables auto handwriting initiation.
*/
private static final int PFLAG4_AUTO_HANDWRITING_ENABLED = 0x000010000;
+
+ /**
+ * When set, measure and layout passes of this view will be logged with {@link Trace}, so we
+ * can better debug jank due to complex view hierarchies.
+ */
+ private static final int PFLAG4_TRAVERSAL_TRACING_ENABLED = 0x000040000;
+
+ /**
+ * When set, emits a {@link Trace} instant event and stacktrace every time a requestLayout of
+ * this class happens.
+ */
+ private static final int PFLAG4_RELAYOUT_TRACING_ENABLED = 0x000080000;
+
/* End of masks for mPrivateFlags4 */
/** @hide */
@@ -6537,6 +6569,15 @@
out.append(mRight);
out.append(',');
out.append(mBottom);
+ appendId(out);
+ if (mAutofillId != null) {
+ out.append(" aid="); out.append(mAutofillId);
+ }
+ out.append("}");
+ return out.toString();
+ }
+
+ void appendId(StringBuilder out) {
final int id = getId();
if (id != NO_ID) {
out.append(" #");
@@ -6568,11 +6609,6 @@
}
}
}
- if (mAutofillId != null) {
- out.append(" aid="); out.append(mAutofillId);
- }
- out.append("}");
- return out.toString();
}
/**
@@ -20767,6 +20803,14 @@
if (isFocused()) {
notifyFocusChangeToImeFocusController(true /* hasFocus */);
}
+
+ if (sTraceLayoutSteps) {
+ setTraversalTracingEnabled(true);
+ }
+ if (sTraceRequestLayoutClass != null
+ && sTraceRequestLayoutClass.equals(getClass().getSimpleName())) {
+ setRelayoutTracingEnabled(true);
+ }
}
/**
@@ -23666,6 +23710,30 @@
return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
}
+ /**
+ * Enable measure/layout debugging on traces.
+ *
+ * @see Trace
+ * @hide
+ */
+ public static void setTraceLayoutSteps(boolean traceLayoutSteps) {
+ sTraceLayoutSteps = traceLayoutSteps;
+ }
+
+ /**
+ * Enable request layout tracing classes with {@code s} simple name.
+ * <p>
+ * When set, a {@link Trace} instant event and a log with the stacktrace is emitted every
+ * time a requestLayout of a class matching {@code s} name happens.
+ * This applies only to views attached from this point onwards.
+ *
+ * @see Trace#instant(long, String)
+ * @hide
+ */
+ public static void setTracedRequestLayoutClassClass(String s) {
+ sTraceRequestLayoutClass = s;
+ }
+
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
@@ -23700,7 +23768,13 @@
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
+ if (isTraversalTracingEnabled()) {
+ Trace.beginSection(mTracingStrings.onMeasureBeforeLayout);
+ }
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
+ if (isTraversalTracingEnabled()) {
+ Trace.endSection();
+ }
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
@@ -23713,7 +23787,13 @@
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
+ if (isTraversalTracingEnabled()) {
+ Trace.beginSection(mTracingStrings.onLayout);
+ }
onLayout(changed, l, t, r, b);
+ if (isTraversalTracingEnabled()) {
+ Trace.endSection();
+ }
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
@@ -26270,6 +26350,25 @@
return (viewRoot != null && viewRoot.isInLayout());
}
+ /** To be used only for debugging purposes. */
+ private void printStackStrace(String name) {
+ Log.d(VIEW_LOG_TAG, "---- ST:" + name);
+
+ StringBuilder sb = new StringBuilder();
+ StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
+ int startIndex = 1;
+ int endIndex = Math.min(stackTraceElements.length, startIndex + 20); // max 20 entries.
+ for (int i = startIndex; i < endIndex; i++) {
+ StackTraceElement s = stackTraceElements[i];
+ sb.append(s.getMethodName())
+ .append("(")
+ .append(s.getFileName())
+ .append(":")
+ .append(s.getLineNumber())
+ .append(") <- ");
+ }
+ Log.d(VIEW_LOG_TAG, name + ": " + sb);
+ }
/**
* Call this when something has changed which has invalidated the
* layout of this view. This will schedule a layout pass of the view
@@ -26283,6 +26382,12 @@
*/
@CallSuper
public void requestLayout() {
+ if (isRelayoutTracingEnabled()) {
+ Trace.instantForTrack(TRACE_TAG_APP, "requestLayoutTracing",
+ mTracingStrings.classSimpleName);
+ printStackStrace(mTracingStrings.requestLayoutStacktracePrefix);
+ }
+
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
@@ -26376,8 +26481,14 @@
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
+ if (isTraversalTracingEnabled()) {
+ Trace.beginSection(mTracingStrings.onMeasure);
+ }
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (isTraversalTracingEnabled()) {
+ Trace.endSection();
+ }
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
@@ -31547,6 +31658,38 @@
== PFLAG4_AUTO_HANDWRITING_ENABLED;
}
+ private void setTraversalTracingEnabled(boolean enabled) {
+ if (enabled) {
+ if (mTracingStrings == null) {
+ mTracingStrings = new ViewTraversalTracingStrings(this);
+ }
+ mPrivateFlags4 |= PFLAG4_TRAVERSAL_TRACING_ENABLED;
+ } else {
+ mPrivateFlags4 &= ~PFLAG4_TRAVERSAL_TRACING_ENABLED;
+ }
+ }
+
+ private boolean isTraversalTracingEnabled() {
+ return (mPrivateFlags4 & PFLAG4_TRAVERSAL_TRACING_ENABLED)
+ == PFLAG4_TRAVERSAL_TRACING_ENABLED;
+ }
+
+ private void setRelayoutTracingEnabled(boolean enabled) {
+ if (enabled) {
+ if (mTracingStrings == null) {
+ mTracingStrings = new ViewTraversalTracingStrings(this);
+ }
+ mPrivateFlags4 |= PFLAG4_RELAYOUT_TRACING_ENABLED;
+ } else {
+ mPrivateFlags4 &= ~PFLAG4_RELAYOUT_TRACING_ENABLED;
+ }
+ }
+
+ private boolean isRelayoutTracingEnabled() {
+ return (mPrivateFlags4 & PFLAG4_RELAYOUT_TRACING_ENABLED)
+ == PFLAG4_RELAYOUT_TRACING_ENABLED;
+ }
+
/**
* Collects a {@link ViewTranslationRequest} which represents the content to be translated in
* the view.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 43bbcfb..c8e1131 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -78,7 +78,6 @@
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
-import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL;
import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
@@ -712,6 +711,7 @@
// These are accessed by multiple threads.
final Rect mWinFrame; // frame given by window manager.
+ private final Rect mLastLayoutFrame;
Rect mOverrideInsetsFrame;
final Rect mPendingBackDropFrame = new Rect();
@@ -932,6 +932,7 @@
mHeight = -1;
mDirty = new Rect();
mWinFrame = new Rect();
+ mLastLayoutFrame = new Rect();
mWindow = new W(this);
mLeashToken = new Binder();
mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
@@ -1113,6 +1114,8 @@
// Update the last resource config in case the resource configuration was changed while
// activity relaunched.
updateLastConfigurationFromResources(getConfiguration());
+ // Make sure to report the completion of draw for relaunch with preserved window.
+ reportNextDraw("rebuilt");
}
private Configuration getConfiguration() {
@@ -1303,7 +1306,7 @@
UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH,
mInsetsController.getRequestedVisibilities(), 1f /* compactScale */,
mTmpFrames);
- setFrame(mTmpFrames.frame);
+ setFrame(mTmpFrames.frame, true /* withinRelayout */);
registerBackCallbackOnWindow();
if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
// For apps requesting legacy back behavior, we add a compat callback that
@@ -1824,7 +1827,7 @@
onMovedToDisplay(displayId, mLastConfigurationFromResources);
}
- setFrame(frame);
+ setFrame(frame, false /* withinRelayout */);
mTmpFrames.displayFrame.set(displayFrame);
if (mTmpFrames.attachedFrame != null && attachedFrame != null) {
mTmpFrames.attachedFrame.set(attachedFrame);
@@ -5740,7 +5743,7 @@
mTmpFrames.frame.right = l + w;
mTmpFrames.frame.top = t;
mTmpFrames.frame.bottom = t + h;
- setFrame(mTmpFrames.frame);
+ setFrame(mTmpFrames.frame, false /* withinRelayout */);
maybeHandleWindowMove(mWinFrame);
}
break;
@@ -8210,7 +8213,7 @@
// If the position and the size of the frame are both changed, it will trigger a BLAST
// sync, and we still need to call relayout to obtain the syncSeqId. Otherwise, we just
// need to send attributes via relayoutAsync.
- final Rect oldFrame = mWinFrame;
+ final Rect oldFrame = mLastLayoutFrame;
final Rect newFrame = mTmpFrames.frame;
final boolean positionChanged =
newFrame.top != oldFrame.top || newFrame.left != oldFrame.left;
@@ -8340,7 +8343,7 @@
params.restore();
}
- setFrame(mTmpFrames.frame);
+ setFrame(mTmpFrames.frame, true /* withinRelayout */);
return relayoutResult;
}
@@ -8375,8 +8378,18 @@
mIsSurfaceOpaque = opaque;
}
- private void setFrame(Rect frame) {
+ /**
+ * Set the mWinFrame of this window.
+ * @param frame the new frame of this window.
+ * @param withinRelayout {@code true} if this setting is within the relayout, or is the initial
+ * setting. That will make sure in the relayout process, we always compare
+ * the window frame with the last processed window frame.
+ */
+ private void setFrame(Rect frame, boolean withinRelayout) {
mWinFrame.set(frame);
+ if (withinRelayout) {
+ mLastLayoutFrame.set(frame);
+ }
final WindowConfiguration winConfig = getCompatWindowConfiguration();
mPendingBackDropFrame.set(mPendingDragResizing && !winConfig.useWindowFrameForBackdrop()
diff --git a/core/java/android/view/ViewTraversalTracingStrings.java b/core/java/android/view/ViewTraversalTracingStrings.java
new file mode 100644
index 0000000..7dde87b
--- /dev/null
+++ b/core/java/android/view/ViewTraversalTracingStrings.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.os.Trace;
+
+/**
+ * Keeps and caches strings used to trace {@link View} traversals.
+ * <p>
+ * This is done to avoid expensive computations of them every time, which can improve performance.
+ */
+class ViewTraversalTracingStrings {
+
+ /** {@link Trace} tag used to mark {@link View#onMeasure(int, int)}. */
+ public final String onMeasure;
+
+ /** {@link Trace} tag used to mark {@link View#onLayout(boolean, int, int, int, int)}. */
+ public final String onLayout;
+
+ /** Caches the view simple name to avoid re-computations. */
+ public final String classSimpleName;
+
+ /** Prefix for request layout stacktraces output in logs. */
+ public final String requestLayoutStacktracePrefix;
+
+ /** {@link Trace} tag used to mark {@link View#onMeasure(int, int)} happening before layout. */
+ public final String onMeasureBeforeLayout;
+
+ /**
+ * @param v {@link View} from where to get the class name.
+ */
+ ViewTraversalTracingStrings(View v) {
+ String className = v.getClass().getSimpleName();
+ classSimpleName = className;
+ onMeasureBeforeLayout = getTraceName("onMeasureBeforeLayout", className, v);
+ onMeasure = getTraceName("onMeasure", className, v);
+ onLayout = getTraceName("onLayout", className, v);
+ requestLayoutStacktracePrefix = "requestLayout " + className;
+ }
+
+ private String getTraceName(String sectionName, String className, View v) {
+ StringBuilder out = new StringBuilder();
+ out.append(sectionName);
+ out.append(" ");
+ out.append(className);
+ v.appendId(out);
+ return out.substring(0, Math.min(out.length() - 1, Trace.MAX_SECTION_NAME_LEN - 1));
+ }
+}
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index e3bf2d4..17df585 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -814,8 +814,8 @@
}
/**
- * Activity level {@link android.content.pm.PackageManager.Property PackageManager
- * .Property} for an app to inform the system that the activity can be opted-in or opted-out
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the app can be opted-in or opted-out
* from the compatibility treatment that avoids {@link
* android.app.Activity#setRequestedOrientation} loops. The loop can be trigerred by
* ignoreRequestedOrientation display setting enabled on the device or by the landscape natural
@@ -833,17 +833,17 @@
* <li>Camera compatibility force rotation treatment is active for the package.
* </ul>
*
- * <p>Setting this property to {@code false} informs the system that the activity must be
+ * <p>Setting this property to {@code false} informs the system that the app must be
* opted-out from the compatibility treatment even if the device manufacturer has opted the app
* into the treatment.
*
* <p><b>Syntax:</b>
* <pre>
- * <activity>
+ * <application>
* <property
* android:name="android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"
* android:value="true|false"/>
- * </activity>
+ * </application>
* </pre>
*
* @hide
@@ -853,8 +853,45 @@
"android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION";
/**
- * Activity level {@link android.content.pm.PackageManager.Property PackageManager
- * .Property} for an app to inform the system that the activity should be excluded from the
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the application can be opted-in or opted-out
+ * from the compatibility treatment that enables sending a fake focus event for unfocused
+ * resumed split screen activities. This is needed because some game engines wait to get
+ * focus before drawing the content of the app which isn't guaranteed by default in multi-window
+ * modes.
+ *
+ * <p>Device manufacturers can enable this treatment using their discretion on a per-device
+ * basis to improve display compatibility. The treatment also needs to be specifically enabled
+ * on a per-app basis afterwards. This can either be done by device manufacturers or developers.
+ *
+ * <p>With this property set to {@code true}, the system will apply the treatment only if the
+ * device manufacturer had previously enabled it on the device. A fake focus event will be sent
+ * to the app after it is resumed only if the app is in split-screen.
+ *
+ * <p>Setting this property to {@code false} informs the system that the activity must be
+ * opted-out from the compatibility treatment even if the device manufacturer has opted the app
+ * into the treatment.
+ *
+ * <p>If the property remains unset the system will apply the treatment only if it had
+ * previously been enabled both at the device and app level by the device manufacturer.
+ *
+ * <p><b>Syntax:</b>
+ * <pre>
+ * <application>
+ * <property
+ * android:name="android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS"
+ * android:value="true|false"/>
+ * </application>
+ * </pre>
+ *
+ * @hide
+ */
+ // TODO(b/263984287): Make this public API.
+ String PROPERTY_COMPAT_ENABLE_FAKE_FOCUS = "android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS";
+
+ /**
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the app should be excluded from the
* camera compatibility force rotation treatment.
*
* <p>The camera compatibility treatment aligns orientations of portrait app window and natural
@@ -879,11 +916,11 @@
*
* <p><b>Syntax:</b>
* <pre>
- * <activity>
+ * <application>
* <property
* android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION"
* android:value="true|false"/>
- * </activity>
+ * </application>
* </pre>
*
* @hide
@@ -893,8 +930,8 @@
"android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION";
/**
- * Activity level {@link android.content.pm.PackageManager.Property PackageManager
- * .Property} for an app to inform the system that the activity should be excluded
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the app should be excluded
* from the activity "refresh" after the camera compatibility force rotation treatment.
*
* <p>The camera compatibility treatment aligns orientations of portrait app window and natural
@@ -926,11 +963,11 @@
*
* <p><b>Syntax:</b>
* <pre>
- * <activity>
+ * <application>
* <property
* android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH"
* android:value="true|false"/>
- * </activity>
+ * </application>
* </pre>
*
* @hide
@@ -940,7 +977,7 @@
"android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH";
/**
- * Activity level {@link android.content.pm.PackageManager.Property PackageManager
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
* .Property} for an app to inform the system that the activity should be or shouldn't be
* "refreshed" after the camera compatibility force rotation treatment using "paused ->
* resumed" cycle rather than "stopped -> resumed".
@@ -976,11 +1013,11 @@
*
* <p><b>Syntax:</b>
* <pre>
- * <activity>
+ * <application>
* <property
* android:name="android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE"
* android:value="true|false"/>
- * </activity>
+ * </application>
* </pre>
*
* @hide
@@ -990,23 +1027,23 @@
"android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE";
/**
- * Activity level {@link android.content.pm.PackageManager.Property PackageManager
- * .Property} for an app to inform the system that the activity should be excluded from the
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the app should be excluded from the
* compatibility override for orientation set by the device manufacturer.
*
* <p>With this property set to {@code true} or unset, device manufacturers can override
- * orientation for the activity using their discretion to improve display compatibility.
+ * orientation for the app using their discretion to improve display compatibility.
*
* <p>With this property set to {@code false}, device manufactured per-app override for
* orientation won't be applied.
*
* <p><b>Syntax:</b>
* <pre>
- * <activity>
+ * <application>
* <property
* android:name="android.window.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE"
* android:value="true|false"/>
- * </activity>
+ * </application>
* </pre>
*
* @hide
@@ -1016,8 +1053,8 @@
"android.window.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE";
/**
- * Activity level {@link android.content.pm.PackageManager.Property PackageManager
- * .Property} for an app to inform the system that the activity should be opted-out from the
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the app should be opted-out from the
* compatibility override that fixes display orientation to landscape natural orientation when
* an activity is fullscreen.
*
@@ -1047,11 +1084,11 @@
*
* <p><b>Syntax:</b>
* <pre>
- * <activity>
+ * <application>
* <property
* android:name="android.window.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE"
* android:value="true|false"/>
- * </activity>
+ * </application>
* </pre>
*
* @hide
diff --git a/core/java/android/window/BackEvent.java b/core/java/android/window/BackEvent.java
index 4a4f561..940b133 100644
--- a/core/java/android/window/BackEvent.java
+++ b/core/java/android/window/BackEvent.java
@@ -99,7 +99,21 @@
}
/**
- * Returns a value between 0 and 1 on how far along the back gesture is.
+ * Returns a value between 0 and 1 on how far along the back gesture is. This value is
+ * driven by the horizontal location of the touch point, and should be used as the fraction to
+ * seek the predictive back animation with. Specifically,
+ * <ol>
+ * <li>The progress is 0 when the touch is at the starting edge of the screen (left or right),
+ * and animation should seek to its start state.
+ * <li>The progress is approximately 1 when the touch is at the opposite side of the screen,
+ * and animation should seek to its end state. Exact end value may vary depending on
+ * screen size.
+ * </ol>
+ * <li> After the gesture finishes in cancel state, this method keeps getting invoked until the
+ * progress value animates back to 0.
+ * </ol>
+ * In-between locations are linearly interpolated based on horizontal distance from the starting
+ * edge and smooth clamped to 1 when the distance exceeds a system-wide threshold.
*/
public float getProgress() {
return mProgress;
diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java
index 38c52e7..b22f967 100644
--- a/core/java/android/window/BackProgressAnimator.java
+++ b/core/java/android/window/BackProgressAnimator.java
@@ -16,8 +16,10 @@
package android.window;
+import android.annotation.NonNull;
import android.util.FloatProperty;
+import com.android.internal.dynamicanimation.animation.DynamicAnimation;
import com.android.internal.dynamicanimation.animation.SpringAnimation;
import com.android.internal.dynamicanimation.animation.SpringForce;
@@ -123,6 +125,27 @@
mProgress = 0;
}
+ /**
+ * Animate the back progress animation from current progress to start position.
+ * This should be called when back is cancelled.
+ *
+ * @param finishCallback the callback to be invoked when the progress is reach to 0.
+ */
+ public void onBackCancelled(@NonNull Runnable finishCallback) {
+ final DynamicAnimation.OnAnimationEndListener listener =
+ new DynamicAnimation.OnAnimationEndListener() {
+ @Override
+ public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
+ float velocity) {
+ mSpring.removeEndListener(this);
+ finishCallback.run();
+ reset();
+ }
+ };
+ mSpring.addEndListener(listener);
+ mSpring.animateToFinalPosition(0);
+ }
+
private void updateProgressValue(float progress) {
if (mLastBackEvent == null || mCallback == null || !mStarted) {
return;
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index dd9483a..bfa3447 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -255,11 +255,12 @@
@Override
public void onBackCancelled() {
Handler.getMain().post(() -> {
- mProgressAnimator.reset();
- final OnBackAnimationCallback callback = getBackAnimationCallback();
- if (callback != null) {
- callback.onBackCancelled();
- }
+ mProgressAnimator.onBackCancelled(() -> {
+ final OnBackAnimationCallback callback = getBackAnimationCallback();
+ if (callback != null) {
+ callback.onBackCancelled();
+ }
+ });
});
}
diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
new file mode 100644
index 0000000..f724e55
--- /dev/null
+++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
@@ -0,0 +1,183 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.config.sysui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Provides a central definition of debug SystemUI's SystemProperties flags, and their defaults.
+ *
+ * The main feature of this class is that it encodes a system-wide default for each flag which can
+ * be updated by engineers with a single-line CL.
+ *
+ * NOTE: Because flag values returned by this class are not cached, it is important that developers
+ * understand the intricacies of changing values and how that applies to their own code.
+ * Generally, the best practice is to set the property, and then restart the device so that any
+ * processes with stale state can be updated. However, if your code has no state derived from the
+ * flag value and queries it any time behavior is relevant, then it may be safe to change the flag
+ * and not immediately reboot.
+ *
+ * To enable flags in debuggable builds, use the following commands:
+ *
+ * $ adb shell setprop persist.sysui.whatever_the_flag true
+ * $ adb reboot
+ *
+ * @hide
+ */
+public class SystemUiSystemPropertiesFlags {
+
+ /** The interface used for resolving SystemUI SystemProperties Flags to booleans. */
+ public interface FlagResolver {
+ /** Is the flag enabled? */
+ boolean isEnabled(Flag flag);
+ }
+
+ /** The primary, immutable resolver returned by getResolver() */
+ private static final FlagResolver
+ MAIN_RESOLVER =
+ Build.IS_DEBUGGABLE ? new DebugResolver() : new ProdResolver();
+
+ /**
+ * On debuggable builds, this can be set to override the resolver returned by getResolver().
+ * This can be useful to override flags when testing components that do not allow injecting the
+ * SystemUiPropertiesFlags resolver they use.
+ * Always set this to null when tests tear down.
+ */
+ @VisibleForTesting
+ public static FlagResolver TEST_RESOLVER = null;
+
+ /** Get the resolver for this device configuration. */
+ public static FlagResolver getResolver() {
+ if (Build.IS_DEBUGGABLE && TEST_RESOLVER != null) {
+ Log.i("SystemUiSystemPropertiesFlags", "Returning debug resolver " + TEST_RESOLVER);
+ return TEST_RESOLVER;
+ }
+ return MAIN_RESOLVER;
+ }
+
+ /** The teamfood flag allows multiple features to be opted into at once. */
+ public static final Flag TEAMFOOD = devFlag("persist.sysui.teamfood");
+
+ /**
+ * Flags related to notification features
+ */
+ public static final class NotificationFlags {
+
+ /**
+ * FOR DEVELOPMENT / TESTING ONLY!!!
+ * Forcibly demote *ALL* FSI notifications as if no apps have the app op permission.
+ */
+ public static final Flag FSI_FORCE_DEMOTE =
+ devFlag("persist.sysui.notification.fsi_force_demote");
+
+ /** Gating the ability for users to dismiss ongoing event notifications */
+ public static final Flag ALLOW_DISMISS_ONGOING =
+ devFlag("persist.sysui.notification.ongoing_dismissal");
+
+ /** Gating the redaction of OTP notifications on the lockscreen */
+ public static final Flag OTP_REDACTION =
+ devFlag("persist.sysui.notification.otp_redaction");
+
+ }
+
+ //// == Everything below this line is the implementation == ////
+
+ /**
+ * Creates a flag that is enabled by default in debuggable builds.
+ * It can be enabled by setting this flag's SystemProperty to 1.
+ *
+ * This flag is ALWAYS disabled in release builds.
+ */
+ @VisibleForTesting
+ public static Flag devFlag(String name) {
+ return new Flag(name, false, null);
+ }
+
+ /**
+ * Creates a flag that is disabled by default in debuggable builds.
+ * It can be enabled or force-disabled by setting this flag's SystemProperty to 1 or 0.
+ * If this flag's SystemProperty is not set, the flag can be enabled by setting the
+ * TEAMFOOD flag's SystemProperty to 1.
+ *
+ * This flag is ALWAYS disabled in release builds.
+ */
+ @VisibleForTesting
+ public static Flag teamfoodFlag(String name) {
+ return new Flag(name, false, TEAMFOOD);
+ }
+
+ /**
+ * Creates a flag that is enabled by default in debuggable builds.
+ * It can be enabled by setting this flag's SystemProperty to 0.
+ *
+ * This flag is ALWAYS enabled in release builds.
+ */
+ @VisibleForTesting
+ public static Flag releasedFlag(String name) {
+ return new Flag(name, true, null);
+ }
+
+ /** Represents a developer-switchable gate for a feature. */
+ public static final class Flag {
+ public final String mSysPropKey;
+ public final boolean mDefaultValue;
+ @Nullable
+ public final Flag mDebugDefault;
+
+ /** constructs a new flag. only visible for testing the class */
+ @VisibleForTesting
+ public Flag(@NonNull String sysPropKey, boolean defaultValue, @Nullable Flag debugDefault) {
+ mSysPropKey = sysPropKey;
+ mDefaultValue = defaultValue;
+ mDebugDefault = debugDefault;
+ }
+ }
+
+ /** Implementation of the interface used in release builds. */
+ @VisibleForTesting
+ public static final class ProdResolver implements
+ FlagResolver {
+ @Override
+ public boolean isEnabled(Flag flag) {
+ return flag.mDefaultValue;
+ }
+ }
+
+ /** Implementation of the interface used in debuggable builds. */
+ @VisibleForTesting
+ public static class DebugResolver implements FlagResolver {
+ @Override
+ public final boolean isEnabled(Flag flag) {
+ if (flag.mDebugDefault == null) {
+ return getBoolean(flag.mSysPropKey, flag.mDefaultValue);
+ }
+ return getBoolean(flag.mSysPropKey, isEnabled(flag.mDebugDefault));
+ }
+
+ /** Look up the value; overridable for tests to avoid needing to set SystemProperties */
+ @VisibleForTesting
+ public boolean getBoolean(String key, boolean defaultValue) {
+ return SystemProperties.getBoolean(key, defaultValue);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/util/ScreenshotRequest.java b/core/java/com/android/internal/util/ScreenshotRequest.java
index 1902f80..c8b7def 100644
--- a/core/java/com/android/internal/util/ScreenshotRequest.java
+++ b/core/java/com/android/internal/util/ScreenshotRequest.java
@@ -173,6 +173,9 @@
public Builder(
@WindowManager.ScreenshotType int type,
@WindowManager.ScreenshotSource int source) {
+ if (type != TAKE_SCREENSHOT_FULLSCREEN && type != TAKE_SCREENSHOT_PROVIDED_IMAGE) {
+ throw new IllegalArgumentException("Invalid screenshot type requested!");
+ }
mType = type;
mSource = source;
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b10df60..31903e2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -315,6 +315,7 @@
<protected-broadcast android:name="android.media.MASTER_BALANCE_CHANGED_ACTION" />
<protected-broadcast android:name="android.media.SCO_AUDIO_STATE_CHANGED" />
<protected-broadcast android:name="android.media.ACTION_SCO_AUDIO_STATE_UPDATED" />
+ <protected-broadcast android:name="com.android.server.audio.action.CHECK_MUSIC_ACTIVE" />
<protected-broadcast android:name="android.intent.action.MEDIA_REMOVED" />
<protected-broadcast android:name="android.intent.action.MEDIA_UNMOUNTED" />
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index a74c787..15c40c0 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2562,6 +2562,8 @@
<java-symbol type="string" name="zen_mode_default_weekends_name" />
<java-symbol type="string" name="zen_mode_default_events_name" />
<java-symbol type="string" name="zen_mode_default_every_night_name" />
+ <java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" />
+ <java-symbol type="string" name="display_rotation_camera_compat_toast_in_split_screen" />
<java-symbol type="array" name="config_system_condition_providers" />
<java-symbol type="string" name="muted_by" />
<java-symbol type="string" name="zen_mode_alarm" />
diff --git a/core/tests/coretests/src/com/android/internal/config/sysui/OWNERS b/core/tests/coretests/src/com/android/internal/config/sysui/OWNERS
new file mode 100644
index 0000000..2e96c97
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/config/sysui/OWNERS
@@ -0,0 +1 @@
+include /packages/SystemUI/OWNERS
diff --git a/core/tests/coretests/src/com/android/internal/config/sysui/SystemUiSystemPropertiesFlagsTest.java b/core/tests/coretests/src/com/android/internal/config/sysui/SystemUiSystemPropertiesFlagsTest.java
new file mode 100644
index 0000000..6b9d39c
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/config/sysui/SystemUiSystemPropertiesFlagsTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.config.sysui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.Flag;
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.FlagResolver;
+
+import junit.framework.TestCase;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@SmallTest
+public class SystemUiSystemPropertiesFlagsTest extends TestCase {
+
+ public class TestableDebugResolver extends SystemUiSystemPropertiesFlags.DebugResolver {
+ final Map<String, Boolean> mTestData = new HashMap<>();
+
+ @Override
+ public boolean getBoolean(String key, boolean defaultValue) {
+ Boolean testValue = mTestData.get(key);
+ return testValue == null ? defaultValue : testValue;
+ }
+
+ public void set(Flag flag, Boolean value) {
+ mTestData.put(flag.mSysPropKey, value);
+ }
+ }
+
+ private FlagResolver mProdResolver;
+ private TestableDebugResolver mDebugResolver;
+
+ private Flag mReleasedFlag;
+ private Flag mTeamfoodFlag;
+ private Flag mDevFlag;
+
+ public void setUp() {
+ mProdResolver = new SystemUiSystemPropertiesFlags.ProdResolver();
+ mDebugResolver = new TestableDebugResolver();
+ mReleasedFlag = SystemUiSystemPropertiesFlags.releasedFlag("mReleasedFlag");
+ mTeamfoodFlag = SystemUiSystemPropertiesFlags.teamfoodFlag("mTeamfoodFlag");
+ mDevFlag = SystemUiSystemPropertiesFlags.devFlag("mDevFlag");
+ }
+
+ public void tearDown() {
+ SystemUiSystemPropertiesFlags.TEST_RESOLVER = null;
+ }
+
+ public void testProdResolverReturnsDefault() {
+ assertThat(mProdResolver.isEnabled(mReleasedFlag)).isTrue();
+ assertThat(mProdResolver.isEnabled(mTeamfoodFlag)).isFalse();
+ assertThat(mProdResolver.isEnabled(mDevFlag)).isFalse();
+ }
+
+ public void testDebugResolverAndReleasedFlag() {
+ assertThat(mDebugResolver.isEnabled(mReleasedFlag)).isTrue();
+
+ mDebugResolver.set(mReleasedFlag, false);
+ assertThat(mDebugResolver.isEnabled(mReleasedFlag)).isFalse();
+
+ mDebugResolver.set(mReleasedFlag, true);
+ assertThat(mDebugResolver.isEnabled(mReleasedFlag)).isTrue();
+ }
+
+ private void assertTeamfoodFlag(Boolean flagValue, Boolean teamfood, boolean expected) {
+ mDebugResolver.set(mTeamfoodFlag, flagValue);
+ mDebugResolver.set(SystemUiSystemPropertiesFlags.TEAMFOOD, teamfood);
+ assertThat(mDebugResolver.isEnabled(mTeamfoodFlag)).isEqualTo(expected);
+ }
+
+ public void testDebugResolverAndTeamfoodFlag() {
+ assertTeamfoodFlag(null, null, false);
+ assertTeamfoodFlag(true, null, true);
+ assertTeamfoodFlag(false, null, false);
+ assertTeamfoodFlag(null, true, true);
+ assertTeamfoodFlag(true, true, true);
+ assertTeamfoodFlag(false, true, false);
+ assertTeamfoodFlag(null, false, false);
+ assertTeamfoodFlag(true, false, true);
+ assertTeamfoodFlag(false, false, false);
+ }
+
+ public void testDebugResolverAndDevFlag() {
+ assertThat(mDebugResolver.isEnabled(mDevFlag)).isFalse();
+
+ mDebugResolver.set(mDevFlag, true);
+ assertThat(mDebugResolver.isEnabled(mDevFlag)).isTrue();
+
+ mDebugResolver.set(mDevFlag, false);
+ assertThat(mDebugResolver.isEnabled(mDevFlag)).isFalse();
+ }
+}
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
index 30540a5..89acbc7 100644
--- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
@@ -131,6 +131,12 @@
assertEquals(Insets.NONE, out.getInsets());
}
+ @Test
+ public void testInvalidType() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new ScreenshotRequest.Builder(5, 2).build());
+ }
+
private Bitmap makeHardwareBitmap(int width, int height) {
HardwareBuffer buffer = HardwareBuffer.create(
width, height, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 58a2073..caa118a 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -306,6 +306,7 @@
<permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
<!-- Permission required for UiModeManager CTS test -->
<permission name="android.permission.READ_PROJECTION_STATE"/>
+ <permission name="android.permission.READ_WALLPAPER_INTERNAL"/>
<permission name="android.permission.READ_WIFI_CREDENTIAL"/>
<permission name="android.permission.REAL_GET_TASKS"/>
<permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
index 3adae70..87c2822 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -25,12 +25,12 @@
import android.util.ArraySet;
import androidx.annotation.NonNull;
+import androidx.window.extensions.core.util.function.Consumer;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import java.util.concurrent.Executor;
-import java.util.function.Consumer;
/**
* Reference implementation of androidx.window.extensions.area OEM interface for use with
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index cddbf469..7eee6da 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 774f6c6..76eb094 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -105,6 +105,10 @@
1.777778
</item>
+ <!-- The aspect ratio that by which optimizations to large screen sizes are made.
+ Needs to be less that or equal to 1. -->
+ <item name="config_pipLargeScreenOptimizedAspectRatio" format="float" type="dimen">0.5625</item>
+
<!-- The default gravity for the picture-in-picture window.
Currently, this maps to Gravity.BOTTOM | Gravity.RIGHT -->
<integer name="config_defaultPictureInPictureGravity">0x55</integer>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 2534498..e24c228 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -126,7 +126,7 @@
private Icon mIcon;
private boolean mIsBubble;
private boolean mIsTextChanged;
- private boolean mIsClearable;
+ private boolean mIsDismissable;
private boolean mShouldSuppressNotificationDot;
private boolean mShouldSuppressNotificationList;
private boolean mShouldSuppressPeek;
@@ -181,7 +181,7 @@
@VisibleForTesting(visibility = PRIVATE)
public Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo,
final int desiredHeight, final int desiredHeightResId, @Nullable final String title,
- int taskId, @Nullable final String locus, boolean isClearable, Executor mainExecutor,
+ int taskId, @Nullable final String locus, boolean isDismissable, Executor mainExecutor,
final Bubbles.BubbleMetadataFlagListener listener) {
Objects.requireNonNull(key);
Objects.requireNonNull(shortcutInfo);
@@ -190,7 +190,7 @@
mKey = key;
mGroupKey = null;
mLocusId = locus != null ? new LocusId(locus) : null;
- mIsClearable = isClearable;
+ mIsDismissable = isDismissable;
mFlags = 0;
mUser = shortcutInfo.getUserHandle();
mPackageName = shortcutInfo.getPackage();
@@ -248,8 +248,8 @@
}
@Hide
- public boolean isClearable() {
- return mIsClearable;
+ public boolean isDismissable() {
+ return mIsDismissable;
}
/**
@@ -533,7 +533,7 @@
mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent();
}
- mIsClearable = entry.isClearable();
+ mIsDismissable = entry.isDismissable();
mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot();
mShouldSuppressNotificationList = entry.shouldSuppressNotificationList();
mShouldSuppressPeek = entry.shouldSuppressPeek();
@@ -612,7 +612,7 @@
* Whether this notification should be shown in the shade.
*/
boolean showInShade() {
- return !shouldSuppressNotification() || !mIsClearable;
+ return !shouldSuppressNotification() || !mIsDismissable;
}
/**
@@ -877,7 +877,7 @@
pw.print(" desiredHeight: "); pw.println(getDesiredHeightString());
pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification());
pw.print(" autoExpand: "); pw.println(shouldAutoExpand());
- pw.print(" isClearable: "); pw.println(mIsClearable);
+ pw.print(" isDismissable: "); pw.println(mIsDismissable);
pw.println(" bubbleMetadataFlagListener null: " + (mBubbleMetadataFlagListener == null));
if (mExpandedView != null) {
mExpandedView.dump(pw);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
index e3aefa5..e37c785 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
@@ -110,7 +110,7 @@
b.title,
b.taskId,
b.locusId?.id,
- b.isClearable
+ b.isDismissable
)
}
}
@@ -206,7 +206,7 @@
entity.title,
entity.taskId,
entity.locus,
- entity.isClearable,
+ entity.isDismissable,
mainExecutor,
bubbleMetadataFlagListener
)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java
index 5f42826..afe19c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java
@@ -38,18 +38,18 @@
private StatusBarNotification mSbn;
private Ranking mRanking;
- private boolean mIsClearable;
+ private boolean mIsDismissable;
private boolean mShouldSuppressNotificationDot;
private boolean mShouldSuppressNotificationList;
private boolean mShouldSuppressPeek;
public BubbleEntry(@NonNull StatusBarNotification sbn,
- Ranking ranking, boolean isClearable, boolean shouldSuppressNotificationDot,
+ Ranking ranking, boolean isDismissable, boolean shouldSuppressNotificationDot,
boolean shouldSuppressNotificationList, boolean shouldSuppressPeek) {
mSbn = sbn;
mRanking = ranking;
- mIsClearable = isClearable;
+ mIsDismissable = isDismissable;
mShouldSuppressNotificationDot = shouldSuppressNotificationDot;
mShouldSuppressNotificationList = shouldSuppressNotificationList;
mShouldSuppressPeek = shouldSuppressPeek;
@@ -115,9 +115,9 @@
return mRanking.canBubble();
}
- /** @return true if this notification is clearable. */
- public boolean isClearable() {
- return mIsClearable;
+ /** @return true if this notification can be dismissed. */
+ public boolean isDismissable() {
+ return mIsDismissable;
}
/** @return true if {@link Policy#SUPPRESSED_EFFECT_BADGE} set for this notification. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt
index f3abc27..9b2e263 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt
@@ -28,5 +28,5 @@
val title: String? = null,
val taskId: Int,
val locus: String? = null,
- val isClearable: Boolean = false
+ val isDismissable: Boolean = false
)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt
index 14c053c..48d8ccf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt
@@ -43,9 +43,7 @@
private const val ATTR_TITLE = "t"
private const val ATTR_TASK_ID = "tid"
private const val ATTR_LOCUS = "l"
-
-// TODO rename it to dismissable to follow NotificationEntry namings
-private const val ATTR_CLEARABLE = "d"
+private const val ATTR_DISMISSABLE = "d"
/**
* Writes the bubbles in xml format into given output stream.
@@ -87,7 +85,7 @@
bubble.title?.let { serializer.attribute(null, ATTR_TITLE, it) }
serializer.attribute(null, ATTR_TASK_ID, bubble.taskId.toString())
bubble.locus?.let { serializer.attribute(null, ATTR_LOCUS, it) }
- serializer.attribute(null, ATTR_CLEARABLE, bubble.isClearable.toString())
+ serializer.attribute(null, ATTR_DISMISSABLE, bubble.isDismissable.toString())
serializer.endTag(null, TAG_BUBBLE)
} catch (e: IOException) {
throw RuntimeException(e)
@@ -147,7 +145,7 @@
parser.getAttributeWithName(ATTR_TITLE),
parser.getAttributeWithName(ATTR_TASK_ID)?.toInt() ?: INVALID_TASK_ID,
parser.getAttributeWithName(ATTR_LOCUS),
- parser.getAttributeWithName(ATTR_CLEARABLE)?.toBoolean() ?: false
+ parser.getAttributeWithName(ATTR_DISMISSABLE)?.toBoolean() ?: false
)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
index 8022e9b..94db878 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
@@ -39,6 +39,7 @@
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipTransitionState;
import com.android.wm.shell.pip.PipUiEventLogger;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm;
import com.android.wm.shell.pip.tv.TvPipBoundsController;
import com.android.wm.shell.pip.tv.TvPipBoundsState;
@@ -69,6 +70,7 @@
ShellInit shellInit,
ShellController shellController,
TvPipBoundsState tvPipBoundsState,
+ PipSizeSpecHandler pipSizeSpecHandler,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
PipAppOpsListener pipAppOpsListener,
@@ -88,6 +90,7 @@
shellInit,
shellController,
tvPipBoundsState,
+ pipSizeSpecHandler,
tvPipBoundsAlgorithm,
tvPipBoundsController,
pipAppOpsListener,
@@ -127,14 +130,23 @@
@WMSingleton
@Provides
static TvPipBoundsAlgorithm provideTvPipBoundsAlgorithm(Context context,
- TvPipBoundsState tvPipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) {
- return new TvPipBoundsAlgorithm(context, tvPipBoundsState, pipSnapAlgorithm);
+ TvPipBoundsState tvPipBoundsState, PipSnapAlgorithm pipSnapAlgorithm,
+ PipSizeSpecHandler pipSizeSpecHandler) {
+ return new TvPipBoundsAlgorithm(context, tvPipBoundsState, pipSnapAlgorithm,
+ pipSizeSpecHandler);
}
@WMSingleton
@Provides
- static TvPipBoundsState provideTvPipBoundsState(Context context) {
- return new TvPipBoundsState(context);
+ static TvPipBoundsState provideTvPipBoundsState(Context context,
+ PipSizeSpecHandler pipSizeSpecHandler) {
+ return new TvPipBoundsState(context, pipSizeSpecHandler);
+ }
+
+ @WMSingleton
+ @Provides
+ static PipSizeSpecHandler providePipSizeSpecHelper(Context context) {
+ return new PipSizeSpecHandler(context);
}
// Handler needed for loadDrawableAsync() in PipControlsViewController
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 512a4ef..1135aa3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -77,6 +77,7 @@
import com.android.wm.shell.pip.phone.PhonePipMenuController;
import com.android.wm.shell.pip.phone.PipController;
import com.android.wm.shell.pip.phone.PipMotionHelper;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import com.android.wm.shell.pip.phone.PipTouchHandler;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -338,6 +339,7 @@
PipBoundsAlgorithm pipBoundsAlgorithm,
PhonePipKeepClearAlgorithm pipKeepClearAlgorithm,
PipBoundsState pipBoundsState,
+ PipSizeSpecHandler pipSizeSpecHandler,
PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
PhonePipMenuController phonePipMenuController,
@@ -354,17 +356,18 @@
return Optional.ofNullable(PipController.create(
context, shellInit, shellCommandHandler, shellController,
displayController, pipAnimationController, pipAppOpsListener, pipBoundsAlgorithm,
- pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper, pipMediaController,
- phonePipMenuController, pipTaskOrganizer, pipTransitionState, pipTouchHandler,
- pipTransitionController, windowManagerShellWrapper, taskStackListener,
- pipParamsChangedForwarder, displayInsetsController, oneHandedController,
- mainExecutor));
+ pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler, pipMotionHelper,
+ pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState,
+ pipTouchHandler, pipTransitionController, windowManagerShellWrapper,
+ taskStackListener, pipParamsChangedForwarder, displayInsetsController,
+ oneHandedController, mainExecutor));
}
@WMSingleton
@Provides
- static PipBoundsState providePipBoundsState(Context context) {
- return new PipBoundsState(context);
+ static PipBoundsState providePipBoundsState(Context context,
+ PipSizeSpecHandler pipSizeSpecHandler) {
+ return new PipBoundsState(context, pipSizeSpecHandler);
}
@WMSingleton
@@ -381,11 +384,18 @@
@WMSingleton
@Provides
+ static PipSizeSpecHandler providePipSizeSpecHelper(Context context) {
+ return new PipSizeSpecHandler(context);
+ }
+
+ @WMSingleton
+ @Provides
static PipBoundsAlgorithm providesPipBoundsAlgorithm(Context context,
PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm,
- PhonePipKeepClearAlgorithm pipKeepClearAlgorithm) {
+ PhonePipKeepClearAlgorithm pipKeepClearAlgorithm,
+ PipSizeSpecHandler pipSizeSpecHandler) {
return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm,
- pipKeepClearAlgorithm);
+ pipKeepClearAlgorithm, pipSizeSpecHandler);
}
// Handler is used by Icon.loadDrawableAsync
@@ -409,13 +419,14 @@
PhonePipMenuController menuPhoneController,
PipBoundsAlgorithm pipBoundsAlgorithm,
PipBoundsState pipBoundsState,
+ PipSizeSpecHandler pipSizeSpecHandler,
PipTaskOrganizer pipTaskOrganizer,
PipMotionHelper pipMotionHelper,
FloatingContentCoordinator floatingContentCoordinator,
PipUiEventLogger pipUiEventLogger,
@ShellMainThread ShellExecutor mainExecutor) {
return new PipTouchHandler(context, shellInit, menuPhoneController, pipBoundsAlgorithm,
- pipBoundsState, pipTaskOrganizer, pipMotionHelper,
+ pipBoundsState, pipSizeSpecHandler, pipTaskOrganizer, pipMotionHelper,
floatingContentCoordinator, pipUiEventLogger, mainExecutor);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 6728c00..d9ac76e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -28,6 +28,7 @@
import android.annotation.NonNull;
import android.app.TaskInfo;
import android.content.Context;
+import android.content.pm.ActivityInfo;
import android.graphics.Rect;
import android.view.Surface;
import android.view.SurfaceControl;
@@ -361,22 +362,26 @@
}
void setColorContentOverlay(Context context) {
- final SurfaceControl.Transaction tx =
- mSurfaceControlTransactionFactory.getTransaction();
- if (mContentOverlay != null) {
- mContentOverlay.detach(tx);
- }
- mContentOverlay = new PipContentOverlay.PipColorOverlay(context);
- mContentOverlay.attach(tx, mLeash);
+ reattachContentOverlay(new PipContentOverlay.PipColorOverlay(context));
}
void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
+ reattachContentOverlay(
+ new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint));
+ }
+
+ void setAppIconContentOverlay(Context context, Rect bounds, ActivityInfo activityInfo) {
+ reattachContentOverlay(
+ new PipContentOverlay.PipAppIconOverlay(context, bounds, activityInfo));
+ }
+
+ private void reattachContentOverlay(PipContentOverlay overlay) {
final SurfaceControl.Transaction tx =
mSurfaceControlTransactionFactory.getTransaction();
if (mContentOverlay != null) {
mContentOverlay.detach(tx);
}
- mContentOverlay = new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint);
+ mContentOverlay = overlay;
mContentOverlay.attach(tx, mLeash);
}
@@ -570,8 +575,9 @@
final Rect base = getBaseValue();
final Rect start = getStartValue();
final Rect end = getEndValue();
+ Rect bounds = mRectEvaluator.evaluate(fraction, start, end);
if (mContentOverlay != null) {
- mContentOverlay.onAnimationUpdate(tx, fraction);
+ mContentOverlay.onAnimationUpdate(tx, bounds, fraction);
}
if (rotatedEndRect != null) {
// Animate the bounds in a different orientation. It only happens when
@@ -579,7 +585,6 @@
applyRotation(tx, leash, fraction, start, end);
return;
}
- Rect bounds = mRectEvaluator.evaluate(fraction, start, end);
float angle = (1.0f - fraction) * startingAngle;
setCurrentValue(bounds);
if (inScaleTransition() || sourceHintRect == null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
index f6d67d8..867162b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
@@ -16,24 +16,19 @@
package com.android.wm.shell.pip;
-import static android.util.TypedValue.COMPLEX_UNIT_DIP;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PictureInPictureParams;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
-import android.graphics.Point;
-import android.graphics.PointF;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.util.Size;
-import android.util.TypedValue;
import android.view.Gravity;
import com.android.wm.shell.R;
-import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import java.io.PrintWriter;
@@ -45,33 +40,29 @@
private static final String TAG = PipBoundsAlgorithm.class.getSimpleName();
private static final float INVALID_SNAP_FRACTION = -1f;
- private final @NonNull PipBoundsState mPipBoundsState;
+ @NonNull private final PipBoundsState mPipBoundsState;
+ @NonNull protected final PipSizeSpecHandler mPipSizeSpecHandler;
private final PipSnapAlgorithm mSnapAlgorithm;
private final PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
- private float mDefaultSizePercent;
- private float mMinAspectRatioForMinSize;
- private float mMaxAspectRatioForMinSize;
private float mDefaultAspectRatio;
private float mMinAspectRatio;
private float mMaxAspectRatio;
private int mDefaultStackGravity;
- private int mDefaultMinSize;
- private int mOverridableMinSize;
- protected Point mScreenEdgeInsets;
public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState,
@NonNull PipSnapAlgorithm pipSnapAlgorithm,
- @NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm) {
+ @NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
+ @NonNull PipSizeSpecHandler pipSizeSpecHandler) {
mPipBoundsState = pipBoundsState;
mSnapAlgorithm = pipSnapAlgorithm;
mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
+ mPipSizeSpecHandler = pipSizeSpecHandler;
reloadResources(context);
// Initialize the aspect ratio to the default aspect ratio. Don't do this in reload
// resources as it would clobber mAspectRatio when entering PiP from fullscreen which
// triggers a configuration change and the resources to be reloaded.
mPipBoundsState.setAspectRatio(mDefaultAspectRatio);
- mPipBoundsState.setMinEdgeSize(mDefaultMinSize);
}
/**
@@ -83,27 +74,15 @@
R.dimen.config_pictureInPictureDefaultAspectRatio);
mDefaultStackGravity = res.getInteger(
R.integer.config_defaultPictureInPictureGravity);
- mDefaultMinSize = res.getDimensionPixelSize(
- R.dimen.default_minimal_size_pip_resizable_task);
- mOverridableMinSize = res.getDimensionPixelSize(
- R.dimen.overridable_minimal_size_pip_resizable_task);
final String screenEdgeInsetsDpString = res.getString(
R.string.config_defaultPictureInPictureScreenEdgeInsets);
final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty()
? Size.parseSize(screenEdgeInsetsDpString)
: null;
- mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point()
- : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), res.getDisplayMetrics()),
- dpToPx(screenEdgeInsetsDp.getHeight(), res.getDisplayMetrics()));
mMinAspectRatio = res.getFloat(
com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
mMaxAspectRatio = res.getFloat(
com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
- mDefaultSizePercent = res.getFloat(
- R.dimen.config_pictureInPictureDefaultSizePercent);
- mMaxAspectRatioForMinSize = res.getFloat(
- R.dimen.config_pictureInPictureAspectRatioLimitForMinSize);
- mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize;
}
/**
@@ -180,8 +159,9 @@
if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) {
// If either dimension is smaller than the allowed minimum, adjust them
// according to mOverridableMinSize
- return new Size(Math.max(windowLayout.minWidth, mOverridableMinSize),
- Math.max(windowLayout.minHeight, mOverridableMinSize));
+ return new Size(
+ Math.max(windowLayout.minWidth, mPipSizeSpecHandler.getOverrideMinEdgeSize()),
+ Math.max(windowLayout.minHeight, mPipSizeSpecHandler.getOverrideMinEdgeSize()));
}
return null;
}
@@ -243,28 +223,13 @@
final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
getMovementBounds(stackBounds), mPipBoundsState.getStashedState());
- final Size overrideMinSize = mPipBoundsState.getOverrideMinSize();
final Size size;
if (useCurrentMinEdgeSize || useCurrentSize) {
- // The default minimum edge size, or the override min edge size if set.
- final int defaultMinEdgeSize = overrideMinSize == null ? mDefaultMinSize
- : mPipBoundsState.getOverrideMinEdgeSize();
- final int minEdgeSize = useCurrentMinEdgeSize ? mPipBoundsState.getMinEdgeSize()
- : defaultMinEdgeSize;
- // Use the existing size but adjusted to the aspect ratio and min edge size.
- size = getSizeForAspectRatio(
- new Size(stackBounds.width(), stackBounds.height()), aspectRatio, minEdgeSize);
+ // Use the existing size but adjusted to the new aspect ratio.
+ size = mPipSizeSpecHandler.getSizeForAspectRatio(
+ new Size(stackBounds.width(), stackBounds.height()), aspectRatio);
} else {
- if (overrideMinSize != null) {
- // The override minimal size is set, use that as the default size making sure it's
- // adjusted to the aspect ratio.
- size = adjustSizeToAspectRatio(overrideMinSize, aspectRatio);
- } else {
- // Calculate the default size using the display size and default min edge size.
- final DisplayLayout displayLayout = mPipBoundsState.getDisplayLayout();
- size = getSizeForAspectRatio(aspectRatio, mDefaultMinSize,
- displayLayout.width(), displayLayout.height());
- }
+ size = mPipSizeSpecHandler.getDefaultSize(aspectRatio);
}
final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f);
@@ -273,18 +238,6 @@
mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
}
- /** Adjusts the given size to conform to the given aspect ratio. */
- private Size adjustSizeToAspectRatio(@NonNull Size size, float aspectRatio) {
- final float sizeAspectRatio = size.getWidth() / (float) size.getHeight();
- if (sizeAspectRatio > aspectRatio) {
- // Size is wider, fix the width and increase the height
- return new Size(size.getWidth(), (int) (size.getWidth() / aspectRatio));
- } else {
- // Size is taller, fix the height and adjust the width.
- return new Size((int) (size.getHeight() * aspectRatio), size.getHeight());
- }
- }
-
/**
* @return the default bounds to show the PIP, if a {@param snapFraction} and {@param size} are
* provided, then it will apply the default bounds to the provided snap fraction and size.
@@ -303,17 +256,9 @@
final Size defaultSize;
final Rect insetBounds = new Rect();
getInsetBounds(insetBounds);
- final DisplayLayout displayLayout = mPipBoundsState.getDisplayLayout();
- final Size overrideMinSize = mPipBoundsState.getOverrideMinSize();
- if (overrideMinSize != null) {
- // The override minimal size is set, use that as the default size making sure it's
- // adjusted to the aspect ratio.
- defaultSize = adjustSizeToAspectRatio(overrideMinSize, mDefaultAspectRatio);
- } else {
- // Calculate the default size using the display size and default min edge size.
- defaultSize = getSizeForAspectRatio(mDefaultAspectRatio,
- mDefaultMinSize, displayLayout.width(), displayLayout.height());
- }
+
+ // Calculate the default size
+ defaultSize = mPipSizeSpecHandler.getDefaultSize(mDefaultAspectRatio);
// Now that we have the default size, apply the snap fraction if valid or position the
// bounds using the default gravity.
@@ -335,12 +280,7 @@
* Populates the bounds on the screen that the PIP can be visible in.
*/
public void getInsetBounds(Rect outRect) {
- final DisplayLayout displayLayout = mPipBoundsState.getDisplayLayout();
- Rect insets = mPipBoundsState.getDisplayLayout().stableInsets();
- outRect.set(insets.left + mScreenEdgeInsets.x,
- insets.top + mScreenEdgeInsets.y,
- displayLayout.width() - insets.right - mScreenEdgeInsets.x,
- displayLayout.height() - insets.bottom - mScreenEdgeInsets.y);
+ outRect.set(mPipSizeSpecHandler.getInsetBounds());
}
/**
@@ -405,71 +345,11 @@
mSnapAlgorithm.applySnapFraction(stackBounds, movementBounds, snapFraction);
}
- public int getDefaultMinSize() {
- return mDefaultMinSize;
- }
-
/**
* @return the pixels for a given dp value.
*/
private int dpToPx(float dpValue, DisplayMetrics dm) {
- return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
- }
-
- /**
- * @return the size of the PiP at the given aspectRatio, ensuring that the minimum edge
- * is at least minEdgeSize.
- */
- public Size getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth,
- int displayHeight) {
- final int smallestDisplaySize = Math.min(displayWidth, displayHeight);
- final int minSize = (int) Math.max(minEdgeSize, smallestDisplaySize * mDefaultSizePercent);
-
- final int width;
- final int height;
- if (aspectRatio <= mMinAspectRatioForMinSize || aspectRatio > mMaxAspectRatioForMinSize) {
- // Beyond these points, we can just use the min size as the shorter edge
- if (aspectRatio <= 1) {
- // Portrait, width is the minimum size
- width = minSize;
- height = Math.round(width / aspectRatio);
- } else {
- // Landscape, height is the minimum size
- height = minSize;
- width = Math.round(height * aspectRatio);
- }
- } else {
- // Within these points, we ensure that the bounds fit within the radius of the limits
- // at the points
- final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize;
- final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize);
- height = (int) Math.round(Math.sqrt((radius * radius)
- / (aspectRatio * aspectRatio + 1)));
- width = Math.round(height * aspectRatio);
- }
- return new Size(width, height);
- }
-
- /**
- * @return the adjusted size so that it conforms to the given aspectRatio, ensuring that the
- * minimum edge is at least minEdgeSize.
- */
- public Size getSizeForAspectRatio(Size size, float aspectRatio, float minEdgeSize) {
- final int smallestSize = Math.min(size.getWidth(), size.getHeight());
- final int minSize = (int) Math.max(minEdgeSize, smallestSize);
-
- final int width;
- final int height;
- if (aspectRatio <= 1) {
- // Portrait, width is the minimum size.
- width = minSize;
- height = Math.round(width / aspectRatio);
- } else {
- // Landscape, height is the minimum size
- height = minSize;
- width = Math.round(height * aspectRatio);
- }
- return new Size(width, height);
+ return PipUtils.dpToPx(dpValue, dm);
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
index 5376ae3..61da10b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
@@ -37,6 +37,7 @@
import com.android.internal.util.function.TriConsumer;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.io.PrintWriter;
@@ -83,13 +84,10 @@
private int mStashedState = STASH_TYPE_NONE;
private int mStashOffset;
private @Nullable PipReentryState mPipReentryState;
+ private final @Nullable PipSizeSpecHandler mPipSizeSpecHandler;
private @Nullable ComponentName mLastPipComponentName;
private int mDisplayId = Display.DEFAULT_DISPLAY;
private final @NonNull DisplayLayout mDisplayLayout = new DisplayLayout();
- /** The current minimum edge size of PIP. */
- private int mMinEdgeSize;
- /** The preferred minimum (and default) size specified by apps. */
- private @Nullable Size mOverrideMinSize;
private final @NonNull MotionBoundsState mMotionBoundsState = new MotionBoundsState();
private boolean mIsImeShowing;
private int mImeHeight;
@@ -122,9 +120,10 @@
private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback;
private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>();
- public PipBoundsState(@NonNull Context context) {
+ public PipBoundsState(@NonNull Context context, PipSizeSpecHandler pipSizeSpecHandler) {
mContext = context;
reloadResources();
+ mPipSizeSpecHandler = pipSizeSpecHandler;
}
/** Reloads the resources. */
@@ -323,20 +322,10 @@
mPipReentryState = null;
}
- /** Set the PIP minimum edge size. */
- public void setMinEdgeSize(int minEdgeSize) {
- mMinEdgeSize = minEdgeSize;
- }
-
- /** Returns the PIP's current minimum edge size. */
- public int getMinEdgeSize() {
- return mMinEdgeSize;
- }
-
/** Sets the preferred size of PIP as specified by the activity in PIP mode. */
public void setOverrideMinSize(@Nullable Size overrideMinSize) {
- final boolean changed = !Objects.equals(overrideMinSize, mOverrideMinSize);
- mOverrideMinSize = overrideMinSize;
+ final boolean changed = !Objects.equals(overrideMinSize, getOverrideMinSize());
+ mPipSizeSpecHandler.setOverrideMinSize(overrideMinSize);
if (changed && mOnMinimalSizeChangeCallback != null) {
mOnMinimalSizeChangeCallback.run();
}
@@ -345,13 +334,12 @@
/** Returns the preferred minimal size specified by the activity in PIP. */
@Nullable
public Size getOverrideMinSize() {
- return mOverrideMinSize;
+ return mPipSizeSpecHandler.getOverrideMinSize();
}
/** Returns the minimum edge size of the override minimum size, or 0 if not set. */
public int getOverrideMinEdgeSize() {
- if (mOverrideMinSize == null) return 0;
- return Math.min(mOverrideMinSize.getWidth(), mOverrideMinSize.getHeight());
+ return mPipSizeSpecHandler.getOverrideMinEdgeSize();
}
/** Get the state of the bounds in motion. */
@@ -581,11 +569,8 @@
pw.println(innerPrefix + "mLastPipComponentName=" + mLastPipComponentName);
pw.println(innerPrefix + "mAspectRatio=" + mAspectRatio);
pw.println(innerPrefix + "mDisplayId=" + mDisplayId);
- pw.println(innerPrefix + "mDisplayLayout=" + mDisplayLayout);
pw.println(innerPrefix + "mStashedState=" + mStashedState);
pw.println(innerPrefix + "mStashOffset=" + mStashOffset);
- pw.println(innerPrefix + "mMinEdgeSize=" + mMinEdgeSize);
- pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize);
pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing);
pw.println(innerPrefix + "mImeHeight=" + mImeHeight);
pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
index 283b1ec..480bf93 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
@@ -16,11 +16,21 @@
package com.android.wm.shell.pip;
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+
import android.annotation.Nullable;
import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.Matrix;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.TaskSnapshot;
@@ -51,9 +61,11 @@
* Animates the internal {@link #mLeash} by a given fraction.
* @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
* call apply on this transaction, it should be applied on the caller side.
+ * @param currentBounds {@link Rect} of the current animation bounds.
* @param fraction progress of the animation ranged from 0f to 1f.
*/
- public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction);
+ public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction);
/**
* Callback when reaches the end of animation on the internal {@link #mLeash}.
@@ -66,13 +78,15 @@
/** A {@link PipContentOverlay} uses solid color. */
public static final class PipColorOverlay extends PipContentOverlay {
+ private static final String TAG = PipColorOverlay.class.getSimpleName();
+
private final Context mContext;
public PipColorOverlay(Context context) {
mContext = context;
mLeash = new SurfaceControl.Builder(new SurfaceSession())
- .setCallsite("PipAnimation")
- .setName(PipColorOverlay.class.getSimpleName())
+ .setCallsite(TAG)
+ .setName(TAG)
.setColorLayer()
.build();
}
@@ -88,7 +102,8 @@
}
@Override
- public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) {
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction) {
atomicTx.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
}
@@ -114,6 +129,8 @@
/** A {@link PipContentOverlay} uses {@link TaskSnapshot}. */
public static final class PipSnapshotOverlay extends PipContentOverlay {
+ private static final String TAG = PipSnapshotOverlay.class.getSimpleName();
+
private final TaskSnapshot mSnapshot;
private final Rect mSourceRectHint;
@@ -121,8 +138,8 @@
mSnapshot = snapshot;
mSourceRectHint = new Rect(sourceRectHint);
mLeash = new SurfaceControl.Builder(new SurfaceSession())
- .setCallsite("PipAnimation")
- .setName(PipSnapshotOverlay.class.getSimpleName())
+ .setCallsite(TAG)
+ .setName(TAG)
.build();
}
@@ -143,7 +160,8 @@
}
@Override
- public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) {
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction) {
// Do nothing. Keep the snapshot till animation ends.
}
@@ -152,4 +170,113 @@
atomicTx.remove(mLeash);
}
}
+
+ /** A {@link PipContentOverlay} shows app icon on solid color background. */
+ public static final class PipAppIconOverlay extends PipContentOverlay {
+ private static final String TAG = PipAppIconOverlay.class.getSimpleName();
+ private static final int APP_ICON_SIZE_DP = 48;
+
+ private final Context mContext;
+ private final int mAppIconSizePx;
+ private final Rect mAppBounds;
+ private final Matrix mTmpTransform = new Matrix();
+ private final float[] mTmpFloat9 = new float[9];
+
+ private Bitmap mBitmap;
+
+ public PipAppIconOverlay(Context context, Rect appBounds, ActivityInfo activityInfo) {
+ mContext = context;
+ mAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, APP_ICON_SIZE_DP,
+ context.getResources().getDisplayMetrics());
+ mAppBounds = new Rect(appBounds);
+ mBitmap = Bitmap.createBitmap(appBounds.width(), appBounds.height(),
+ Bitmap.Config.ARGB_8888);
+ prepareAppIconOverlay(activityInfo);
+ mLeash = new SurfaceControl.Builder(new SurfaceSession())
+ .setCallsite(TAG)
+ .setName(TAG)
+ .build();
+ }
+
+ @Override
+ public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
+ tx.show(mLeash);
+ tx.setLayer(mLeash, Integer.MAX_VALUE);
+ tx.setBuffer(mLeash, mBitmap.getHardwareBuffer());
+ tx.reparent(mLeash, parentLeash);
+ tx.apply();
+ }
+
+ @Override
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction) {
+ mTmpTransform.reset();
+ // Scale back the bitmap with the pivot point at center.
+ mTmpTransform.postScale(
+ (float) mAppBounds.width() / currentBounds.width(),
+ (float) mAppBounds.height() / currentBounds.height(),
+ mAppBounds.centerX(),
+ mAppBounds.centerY());
+ atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9)
+ .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
+ }
+
+ @Override
+ public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) {
+ atomicTx.remove(mLeash);
+ }
+
+ @Override
+ public void detach(SurfaceControl.Transaction tx) {
+ super.detach(tx);
+ if (mBitmap != null && !mBitmap.isRecycled()) {
+ mBitmap.recycle();
+ }
+ }
+
+ private void prepareAppIconOverlay(ActivityInfo activityInfo) {
+ final Canvas canvas = new Canvas();
+ canvas.setBitmap(mBitmap);
+ final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+ android.R.attr.colorBackground });
+ try {
+ int colorAccent = ta.getColor(0, 0);
+ canvas.drawRGB(
+ Color.red(colorAccent),
+ Color.green(colorAccent),
+ Color.blue(colorAccent));
+ } finally {
+ ta.recycle();
+ }
+ final Drawable appIcon = loadActivityInfoIcon(activityInfo,
+ mContext.getResources().getConfiguration().densityDpi);
+ final Rect appIconBounds = new Rect(
+ mAppBounds.centerX() - mAppIconSizePx / 2,
+ mAppBounds.centerY() - mAppIconSizePx / 2,
+ mAppBounds.centerX() + mAppIconSizePx / 2,
+ mAppBounds.centerY() + mAppIconSizePx / 2);
+ appIcon.setBounds(appIconBounds);
+ appIcon.draw(canvas);
+ mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */);
+ }
+
+ // Copied from com.android.launcher3.icons.IconProvider#loadActivityInfoIcon
+ private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) {
+ final int iconRes = ai.getIconResource();
+ Drawable icon = null;
+ // Get the preferred density icon from the app's resources
+ if (density != 0 && iconRes != 0) {
+ try {
+ final Resources resources = mContext.getPackageManager()
+ .getResourcesForApplication(ai.applicationInfo);
+ icon = resources.getDrawableForDensity(iconRes, density);
+ } catch (PackageManager.NameNotFoundException | Resources.NotFoundException exc) { }
+ }
+ // Get the default density icon
+ if (icon == null) {
+ icon = ai.loadIcon(mContext.getPackageManager());
+ }
+ return icon;
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 8ba2583..aad27b9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -62,6 +62,7 @@
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.util.Log;
import android.view.Choreographer;
import android.view.Display;
@@ -1568,7 +1569,13 @@
// Similar to auto-enter-pip transition, we use content overlay when there is no
// source rect hint to enter PiP use bounds animation.
if (sourceHintRect == null) {
- animator.setColorContentOverlay(mContext);
+ if (SystemProperties.getBoolean(
+ "persist.wm.debug.enable_pip_app_icon_overlay", false)) {
+ animator.setAppIconContentOverlay(
+ mContext, currentBounds, mTaskInfo.topActivityInfo);
+ } else {
+ animator.setColorContentOverlay(mContext);
+ }
} else {
final TaskSnapshot snapshot = PipUtils.getTaskSnapshot(
mTaskInfo.launchIntoPipHostTaskId, false /* isLowResolution */);
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 83158ff..d9d1009 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
@@ -50,6 +50,7 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
+import android.os.SystemProperties;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
@@ -792,7 +793,13 @@
if (sourceHintRect == null) {
// We use content overlay when there is no source rect hint to enter PiP use bounds
// animation.
- animator.setColorContentOverlay(mContext);
+ if (SystemProperties.getBoolean(
+ "persist.wm.debug.enable_pip_app_icon_overlay", false)) {
+ animator.setAppIconContentOverlay(
+ mContext, currentBounds, taskInfo.topActivityInfo);
+ } else {
+ animator.setColorContentOverlay(mContext);
+ }
}
} else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java
index fa00619..8b98790 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java
@@ -18,6 +18,7 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
import android.annotation.Nullable;
import android.app.ActivityTaskManager;
@@ -26,8 +27,10 @@
import android.content.ComponentName;
import android.content.Context;
import android.os.RemoteException;
+import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
+import android.util.TypedValue;
import android.window.TaskSnapshot;
import com.android.internal.protolog.common.ProtoLog;
@@ -70,6 +73,13 @@
}
/**
+ * @return the pixels for a given dp value.
+ */
+ public static int dpToPx(float dpValue, DisplayMetrics dm) {
+ return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
+ }
+
+ /**
* @return true if the aspect ratios differ
*/
public static boolean aspectRatioChanged(float aspectRatio1, float aspectRatio2) {
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 525beb1..d86468a 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
@@ -137,6 +137,7 @@
private PipBoundsAlgorithm mPipBoundsAlgorithm;
private PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
private PipBoundsState mPipBoundsState;
+ private PipSizeSpecHandler mPipSizeSpecHandler;
private PipMotionHelper mPipMotionHelper;
private PipTouchHandler mTouchHandler;
private PipTransitionController mPipTransitionController;
@@ -380,6 +381,7 @@
PipBoundsAlgorithm pipBoundsAlgorithm,
PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
PipBoundsState pipBoundsState,
+ PipSizeSpecHandler pipSizeSpecHandler,
PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
PhonePipMenuController phonePipMenuController,
@@ -401,11 +403,11 @@
return new PipController(context, shellInit, shellCommandHandler, shellController,
displayController, pipAnimationController, pipAppOpsListener,
- pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper,
- pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState,
- pipTouchHandler, pipTransitionController, windowManagerShellWrapper,
- taskStackListener, pipParamsChangedForwarder, displayInsetsController,
- oneHandedController, mainExecutor)
+ pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler,
+ pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer,
+ pipTransitionState, pipTouchHandler, pipTransitionController,
+ windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
+ displayInsetsController, oneHandedController, mainExecutor)
.mImpl;
}
@@ -419,6 +421,7 @@
PipBoundsAlgorithm pipBoundsAlgorithm,
PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
@NonNull PipBoundsState pipBoundsState,
+ PipSizeSpecHandler pipSizeSpecHandler,
PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
PhonePipMenuController phonePipMenuController,
@@ -444,6 +447,7 @@
mPipBoundsAlgorithm = pipBoundsAlgorithm;
mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
mPipBoundsState = pipBoundsState;
+ mPipSizeSpecHandler = pipSizeSpecHandler;
mPipMotionHelper = pipMotionHelper;
mPipTaskOrganizer = pipTaskOrganizer;
mPipTransitionState = pipTransitionState;
@@ -512,7 +516,10 @@
// Ensure that we have the display info in case we get calls to update the bounds before the
// listener calls back
mPipBoundsState.setDisplayId(mContext.getDisplayId());
- mPipBoundsState.setDisplayLayout(new DisplayLayout(mContext, mContext.getDisplay()));
+
+ DisplayLayout layout = new DisplayLayout(mContext, mContext.getDisplay());
+ mPipSizeSpecHandler.setDisplayLayout(layout);
+ mPipBoundsState.setDisplayLayout(layout);
try {
mWindowManagerShellWrapper.addPinnedStackListener(mPinnedTaskListener);
@@ -686,6 +693,7 @@
mPipBoundsAlgorithm.onConfigurationChanged(mContext);
mTouchHandler.onConfigurationChanged();
mPipBoundsState.onConfigurationChanged();
+ mPipSizeSpecHandler.onConfigurationChanged();
}
@Override
@@ -711,7 +719,11 @@
Runnable updateDisplayLayout = () -> {
final boolean fromRotation = Transitions.ENABLE_SHELL_TRANSITIONS
&& mPipBoundsState.getDisplayLayout().rotation() != layout.rotation();
+
+ // update the internal state of objects subscribed to display changes
+ mPipSizeSpecHandler.setDisplayLayout(layout);
mPipBoundsState.setDisplayLayout(layout);
+
final WindowContainerTransaction wct =
fromRotation ? new WindowContainerTransaction() : null;
updateMovementBounds(null /* toBounds */,
@@ -1083,6 +1095,7 @@
mPipTaskOrganizer.dump(pw, innerPrefix);
mPipBoundsState.dump(pw, innerPrefix);
mPipInputConsumer.dump(pw, innerPrefix);
+ mPipSizeSpecHandler.dump(pw, innerPrefix);
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java
new file mode 100644
index 0000000..e787ed9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java
@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.pip.phone;
+
+import static com.android.wm.shell.pip.PipUtils.dpToPx;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.SystemProperties;
+import android.util.Size;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.DisplayLayout;
+
+import java.io.PrintWriter;
+
+/**
+ * Acts as a source of truth for appropriate size spec for PIP.
+ */
+public class PipSizeSpecHandler {
+ private static final String TAG = PipSizeSpecHandler.class.getSimpleName();
+
+ @NonNull private final DisplayLayout mDisplayLayout = new DisplayLayout();
+
+ @VisibleForTesting
+ final SizeSpecSource mSizeSpecSourceImpl;
+
+ /** The preferred minimum (and default minimum) size specified by apps. */
+ @Nullable private Size mOverrideMinSize;
+ private int mOverridableMinSize;
+
+ /** Used to store values obtained from resource files. */
+ private Point mScreenEdgeInsets;
+ private float mMinAspectRatioForMinSize;
+ private float mMaxAspectRatioForMinSize;
+ private int mDefaultMinSize;
+
+ @NonNull private final Context mContext;
+
+ private interface SizeSpecSource {
+ /** Returns max size allowed for the PIP window */
+ Size getMaxSize(float aspectRatio);
+
+ /** Returns default size for the PIP window */
+ Size getDefaultSize(float aspectRatio);
+
+ /** Returns min size allowed for the PIP window */
+ Size getMinSize(float aspectRatio);
+
+ /** Returns the adjusted size based on current size and target aspect ratio */
+ Size getSizeForAspectRatio(Size size, float aspectRatio);
+
+ /** Updates internal resources on configuration changes */
+ default void reloadResources() {}
+ }
+
+ /**
+ * Determines PIP window size optimized for large screens and aspect ratios close to 1:1
+ */
+ private class SizeSpecLargeScreenOptimizedImpl implements SizeSpecSource {
+ private static final float DEFAULT_OPTIMIZED_ASPECT_RATIO = 9f / 16;
+
+ /** Default and minimum percentages for the PIP size logic. */
+ private final float mDefaultSizePercent;
+ private final float mMinimumSizePercent;
+
+ /** Aspect ratio that the PIP size spec logic optimizes for. */
+ private float mOptimizedAspectRatio;
+
+ private SizeSpecLargeScreenOptimizedImpl() {
+ mDefaultSizePercent = Float.parseFloat(SystemProperties
+ .get("com.android.wm.shell.pip.phone.def_percentage", "0.6"));
+ mMinimumSizePercent = Float.parseFloat(SystemProperties
+ .get("com.android.wm.shell.pip.phone.min_percentage", "0.5"));
+ }
+
+ @Override
+ public void reloadResources() {
+ final Resources res = mContext.getResources();
+
+ mOptimizedAspectRatio = res.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio);
+ // make sure the optimized aspect ratio is valid with a default value to fall back to
+ if (mOptimizedAspectRatio > 1) {
+ mOptimizedAspectRatio = DEFAULT_OPTIMIZED_ASPECT_RATIO;
+ }
+ }
+
+ /**
+ * Calculates the max size of PIP.
+ *
+ * Optimizes for 16:9 aspect ratios, making them take full length of shortest display edge.
+ * As aspect ratio approaches values close to 1:1, the logic does not let PIP occupy the
+ * whole screen. A linear function is used to calculate these sizes.
+ *
+ * @param aspectRatio aspect ratio of the PIP window
+ * @return dimensions of the max size of the PIP
+ */
+ @Override
+ public Size getMaxSize(float aspectRatio) {
+ final int totalHorizontalPadding = getInsetBounds().left
+ + (getDisplayBounds().width() - getInsetBounds().right);
+ final int totalVerticalPadding = getInsetBounds().top
+ + (getDisplayBounds().height() - getInsetBounds().bottom);
+
+ final int shorterLength = (int) (1f * Math.min(
+ getDisplayBounds().width() - totalHorizontalPadding,
+ getDisplayBounds().height() - totalVerticalPadding));
+
+ int maxWidth, maxHeight;
+
+ // use the optimized max sizing logic only within a certain aspect ratio range
+ if (aspectRatio >= mOptimizedAspectRatio && aspectRatio <= 1 / mOptimizedAspectRatio) {
+ // this formula and its derivation is explained in b/198643358#comment16
+ maxWidth = (int) (mOptimizedAspectRatio * shorterLength
+ + shorterLength * (aspectRatio - mOptimizedAspectRatio) / (1
+ + aspectRatio));
+ maxHeight = (int) (maxWidth / aspectRatio);
+ } else {
+ if (aspectRatio > 1f) {
+ maxWidth = shorterLength;
+ maxHeight = (int) (maxWidth / aspectRatio);
+ } else {
+ maxHeight = shorterLength;
+ maxWidth = (int) (maxHeight * aspectRatio);
+ }
+ }
+
+ return new Size(maxWidth, maxHeight);
+ }
+
+ /**
+ * Decreases the dimensions by a percentage relative to max size to get default size.
+ *
+ * @param aspectRatio aspect ratio of the PIP window
+ * @return dimensions of the default size of the PIP
+ */
+ @Override
+ public Size getDefaultSize(float aspectRatio) {
+ Size minSize = this.getMinSize(aspectRatio);
+
+ if (mOverrideMinSize != null) {
+ return minSize;
+ }
+
+ Size maxSize = this.getMaxSize(aspectRatio);
+
+ int defaultWidth = Math.max((int) (maxSize.getWidth() * mDefaultSizePercent),
+ minSize.getWidth());
+ int defaultHeight = Math.max((int) (maxSize.getHeight() * mDefaultSizePercent),
+ minSize.getHeight());
+
+ return new Size(defaultWidth, defaultHeight);
+ }
+
+ /**
+ * Decreases the dimensions by a certain percentage relative to max size to get min size.
+ *
+ * @param aspectRatio aspect ratio of the PIP window
+ * @return dimensions of the min size of the PIP
+ */
+ @Override
+ public Size getMinSize(float aspectRatio) {
+ // if there is an overridden min size provided, return that
+ if (mOverrideMinSize != null) {
+ return adjustOverrideMinSizeToAspectRatio(aspectRatio);
+ }
+
+ Size maxSize = this.getMaxSize(aspectRatio);
+
+ int minWidth = (int) (maxSize.getWidth() * mMinimumSizePercent);
+ int minHeight = (int) (maxSize.getHeight() * mMinimumSizePercent);
+
+ // make sure the calculated min size is not smaller than the allowed default min size
+ if (aspectRatio > 1f) {
+ minHeight = (int) Math.max(minHeight, mDefaultMinSize);
+ minWidth = (int) (minHeight * aspectRatio);
+ } else {
+ minWidth = (int) Math.max(minWidth, mDefaultMinSize);
+ minHeight = (int) (minWidth / aspectRatio);
+ }
+ return new Size(minWidth, minHeight);
+ }
+
+ /**
+ * Returns the size for target aspect ratio making sure new size conforms with the rules.
+ *
+ * <p>Recalculates the dimensions such that the target aspect ratio is achieved, while
+ * maintaining the same maximum size to current size ratio.
+ *
+ * @param size current size
+ * @param aspectRatio target aspect ratio
+ */
+ @Override
+ public Size getSizeForAspectRatio(Size size, float aspectRatio) {
+ // getting the percentage of the max size that current size takes
+ float currAspectRatio = (float) size.getWidth() / size.getHeight();
+ Size currentMaxSize = getMaxSize(currAspectRatio);
+ float currentPercent = (float) size.getWidth() / currentMaxSize.getWidth();
+
+ // getting the max size for the target aspect ratio
+ Size updatedMaxSize = getMaxSize(aspectRatio);
+
+ int width = (int) (updatedMaxSize.getWidth() * currentPercent);
+ int height = (int) (updatedMaxSize.getHeight() * currentPercent);
+
+ // adjust the dimensions if below allowed min edge size
+ if (width < getMinEdgeSize() && aspectRatio <= 1) {
+ width = getMinEdgeSize();
+ height = (int) (width / aspectRatio);
+ } else if (height < getMinEdgeSize() && aspectRatio > 1) {
+ height = getMinEdgeSize();
+ width = (int) (height * aspectRatio);
+ }
+
+ // reduce the dimensions of the updated size to the calculated percentage
+ return new Size(width, height);
+ }
+ }
+
+ private class SizeSpecDefaultImpl implements SizeSpecSource {
+ private float mDefaultSizePercent;
+ private float mMinimumSizePercent;
+
+ @Override
+ public void reloadResources() {
+ final Resources res = mContext.getResources();
+
+ mMaxAspectRatioForMinSize = res.getFloat(
+ R.dimen.config_pictureInPictureAspectRatioLimitForMinSize);
+ mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize;
+
+ mDefaultSizePercent = res.getFloat(R.dimen.config_pictureInPictureDefaultSizePercent);
+ mMinimumSizePercent = res.getFraction(R.fraction.config_pipShortestEdgePercent, 1, 1);
+ }
+
+ @Override
+ public Size getMaxSize(float aspectRatio) {
+ final int shorterLength = Math.min(getDisplayBounds().width(),
+ getDisplayBounds().height());
+
+ final int totalHorizontalPadding = getInsetBounds().left
+ + (getDisplayBounds().width() - getInsetBounds().right);
+ final int totalVerticalPadding = getInsetBounds().top
+ + (getDisplayBounds().height() - getInsetBounds().bottom);
+
+ final int maxWidth, maxHeight;
+
+ if (aspectRatio > 1f) {
+ maxWidth = (int) Math.max(getDefaultSize(aspectRatio).getWidth(),
+ shorterLength - totalHorizontalPadding);
+ maxHeight = (int) (maxWidth / aspectRatio);
+ } else {
+ maxHeight = (int) Math.max(getDefaultSize(aspectRatio).getHeight(),
+ shorterLength - totalVerticalPadding);
+ maxWidth = (int) (maxHeight * aspectRatio);
+ }
+
+ return new Size(maxWidth, maxHeight);
+ }
+
+ @Override
+ public Size getDefaultSize(float aspectRatio) {
+ if (mOverrideMinSize != null) {
+ return this.getMinSize(aspectRatio);
+ }
+
+ final int smallestDisplaySize = Math.min(getDisplayBounds().width(),
+ getDisplayBounds().height());
+ final int minSize = (int) Math.max(getMinEdgeSize(),
+ smallestDisplaySize * mDefaultSizePercent);
+
+ final int width;
+ final int height;
+
+ if (aspectRatio <= mMinAspectRatioForMinSize
+ || aspectRatio > mMaxAspectRatioForMinSize) {
+ // Beyond these points, we can just use the min size as the shorter edge
+ if (aspectRatio <= 1) {
+ // Portrait, width is the minimum size
+ width = minSize;
+ height = Math.round(width / aspectRatio);
+ } else {
+ // Landscape, height is the minimum size
+ height = minSize;
+ width = Math.round(height * aspectRatio);
+ }
+ } else {
+ // Within these points, ensure that the bounds fit within the radius of the limits
+ // at the points
+ final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize;
+ final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize);
+ height = (int) Math.round(Math.sqrt((radius * radius)
+ / (aspectRatio * aspectRatio + 1)));
+ width = Math.round(height * aspectRatio);
+ }
+
+ return new Size(width, height);
+ }
+
+ @Override
+ public Size getMinSize(float aspectRatio) {
+ if (mOverrideMinSize != null) {
+ return adjustOverrideMinSizeToAspectRatio(aspectRatio);
+ }
+
+ final int shorterLength = Math.min(getDisplayBounds().width(),
+ getDisplayBounds().height());
+ final int minWidth, minHeight;
+
+ if (aspectRatio > 1f) {
+ minWidth = (int) Math.min(getDefaultSize(aspectRatio).getWidth(),
+ shorterLength * mMinimumSizePercent);
+ minHeight = (int) (minWidth / aspectRatio);
+ } else {
+ minHeight = (int) Math.min(getDefaultSize(aspectRatio).getHeight(),
+ shorterLength * mMinimumSizePercent);
+ minWidth = (int) (minHeight * aspectRatio);
+ }
+
+ return new Size(minWidth, minHeight);
+ }
+
+ @Override
+ public Size getSizeForAspectRatio(Size size, float aspectRatio) {
+ final int smallestSize = Math.min(size.getWidth(), size.getHeight());
+ final int minSize = Math.max(getMinEdgeSize(), smallestSize);
+
+ final int width;
+ final int height;
+ if (aspectRatio <= 1) {
+ // Portrait, width is the minimum size.
+ width = minSize;
+ height = Math.round(width / aspectRatio);
+ } else {
+ // Landscape, height is the minimum size
+ height = minSize;
+ width = Math.round(height * aspectRatio);
+ }
+
+ return new Size(width, height);
+ }
+ }
+
+ public PipSizeSpecHandler(Context context) {
+ mContext = context;
+
+ boolean enablePipSizeLargeScreen = SystemProperties
+ .getBoolean("persist.wm.debug.enable_pip_size_large_screen", false);
+
+ // choose between two implementations of size spec logic
+ if (enablePipSizeLargeScreen) {
+ mSizeSpecSourceImpl = new SizeSpecLargeScreenOptimizedImpl();
+ } else {
+ mSizeSpecSourceImpl = new SizeSpecDefaultImpl();
+ }
+
+ reloadResources();
+ }
+
+ /** Reloads the resources */
+ public void onConfigurationChanged() {
+ reloadResources();
+ }
+
+ private void reloadResources() {
+ final Resources res = mContext.getResources();
+
+ mDefaultMinSize = res.getDimensionPixelSize(
+ R.dimen.default_minimal_size_pip_resizable_task);
+ mOverridableMinSize = res.getDimensionPixelSize(
+ R.dimen.overridable_minimal_size_pip_resizable_task);
+
+ final String screenEdgeInsetsDpString = res.getString(
+ R.string.config_defaultPictureInPictureScreenEdgeInsets);
+ final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty()
+ ? Size.parseSize(screenEdgeInsetsDpString)
+ : null;
+ mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point()
+ : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), res.getDisplayMetrics()),
+ dpToPx(screenEdgeInsetsDp.getHeight(), res.getDisplayMetrics()));
+
+ // update the internal resources of the size spec source's stub
+ mSizeSpecSourceImpl.reloadResources();
+ }
+
+ /** Returns the display's bounds. */
+ @NonNull
+ public Rect getDisplayBounds() {
+ return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
+ }
+
+ /** Get the display layout. */
+ @NonNull
+ public DisplayLayout getDisplayLayout() {
+ return mDisplayLayout;
+ }
+
+ /** Update the display layout. */
+ public void setDisplayLayout(@NonNull DisplayLayout displayLayout) {
+ mDisplayLayout.set(displayLayout);
+ }
+
+ public Point getScreenEdgeInsets() {
+ return mScreenEdgeInsets;
+ }
+
+ /**
+ * Returns the inset bounds the PIP window can be visible in.
+ */
+ public Rect getInsetBounds() {
+ Rect insetBounds = new Rect();
+ final DisplayLayout displayLayout = getDisplayLayout();
+ Rect insets = getDisplayLayout().stableInsets();
+ insetBounds.set(insets.left + mScreenEdgeInsets.x,
+ insets.top + mScreenEdgeInsets.y,
+ displayLayout.width() - insets.right - mScreenEdgeInsets.x,
+ displayLayout.height() - insets.bottom - mScreenEdgeInsets.y);
+ return insetBounds;
+ }
+
+ /** Sets the preferred size of PIP as specified by the activity in PIP mode. */
+ public void setOverrideMinSize(@Nullable Size overrideMinSize) {
+ mOverrideMinSize = overrideMinSize;
+ }
+
+ /** Returns the preferred minimal size specified by the activity in PIP. */
+ @Nullable
+ public Size getOverrideMinSize() {
+ if (mOverrideMinSize != null
+ && (mOverrideMinSize.getWidth() < mOverridableMinSize
+ || mOverrideMinSize.getHeight() < mOverridableMinSize)) {
+ return new Size(mOverridableMinSize, mOverridableMinSize);
+ }
+
+ return mOverrideMinSize;
+ }
+
+ /** Returns the minimum edge size of the override minimum size, or 0 if not set. */
+ public int getOverrideMinEdgeSize() {
+ if (mOverrideMinSize == null) return 0;
+ return Math.min(getOverrideMinSize().getWidth(), getOverrideMinSize().getHeight());
+ }
+
+ public int getMinEdgeSize() {
+ return mOverrideMinSize == null ? mDefaultMinSize : getOverrideMinEdgeSize();
+ }
+
+ /**
+ * Returns the size for the max size spec.
+ */
+ public Size getMaxSize(float aspectRatio) {
+ return mSizeSpecSourceImpl.getMaxSize(aspectRatio);
+ }
+
+ /**
+ * Returns the size for the default size spec.
+ */
+ public Size getDefaultSize(float aspectRatio) {
+ return mSizeSpecSourceImpl.getDefaultSize(aspectRatio);
+ }
+
+ /**
+ * Returns the size for the min size spec.
+ */
+ public Size getMinSize(float aspectRatio) {
+ return mSizeSpecSourceImpl.getMinSize(aspectRatio);
+ }
+
+ /**
+ * Returns the adjusted size so that it conforms to the given aspectRatio.
+ *
+ * @param size current size
+ * @param aspectRatio target aspect ratio
+ */
+ public Size getSizeForAspectRatio(@NonNull Size size, float aspectRatio) {
+ if (size.equals(mOverrideMinSize)) {
+ return adjustOverrideMinSizeToAspectRatio(aspectRatio);
+ }
+
+ return mSizeSpecSourceImpl.getSizeForAspectRatio(size, aspectRatio);
+ }
+
+ /**
+ * Returns the adjusted overridden min size if it is set; otherwise, returns null.
+ *
+ * <p>Overridden min size needs to be adjusted in its own way while making sure that the target
+ * aspect ratio is maintained
+ *
+ * @param aspectRatio target aspect ratio
+ */
+ @Nullable
+ @VisibleForTesting
+ Size adjustOverrideMinSizeToAspectRatio(float aspectRatio) {
+ if (mOverrideMinSize == null) {
+ return null;
+ }
+ final Size size = getOverrideMinSize();
+ final float sizeAspectRatio = size.getWidth() / (float) size.getHeight();
+ if (sizeAspectRatio > aspectRatio) {
+ // Size is wider, fix the width and increase the height
+ return new Size(size.getWidth(), (int) (size.getWidth() / aspectRatio));
+ } else {
+ // Size is taller, fix the height and adjust the width.
+ return new Size((int) (size.getHeight() * aspectRatio), size.getHeight());
+ }
+ }
+
+ /** Dumps internal state. */
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mSizeSpecSourceImpl=" + mSizeSpecSourceImpl.toString());
+ pw.println(innerPrefix + "mDisplayLayout=" + mDisplayLayout);
+ pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize);
+ }
+}
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 850c561..0e8d13d 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
@@ -78,7 +78,8 @@
private boolean mEnableResize;
private final Context mContext;
private final PipBoundsAlgorithm mPipBoundsAlgorithm;
- private final @NonNull PipBoundsState mPipBoundsState;
+ @NonNull private final PipBoundsState mPipBoundsState;
+ @NonNull private final PipSizeSpecHandler mPipSizeSpecHandler;
private final PipUiEventLogger mPipUiEventLogger;
private final PipDismissTargetHandler mPipDismissTargetHandler;
private final PipTaskOrganizer mPipTaskOrganizer;
@@ -99,7 +100,6 @@
// The reference inset bounds, used to determine the dismiss fraction
private final Rect mInsetBounds = new Rect();
- private int mExpandedShortestEdgeSize;
// Used to workaround an issue where the WM rotation happens before we are notified, allowing
// us to send stale bounds
@@ -120,7 +120,6 @@
private float mSavedSnapFraction = -1f;
private boolean mSendingHoverAccessibilityEvents;
private boolean mMovementWithinDismiss;
- private float mMinimumSizePercent;
// Touch state
private final PipTouchState mTouchState;
@@ -174,6 +173,7 @@
PhonePipMenuController menuController,
PipBoundsAlgorithm pipBoundsAlgorithm,
@NonNull PipBoundsState pipBoundsState,
+ @NonNull PipSizeSpecHandler pipSizeSpecHandler,
PipTaskOrganizer pipTaskOrganizer,
PipMotionHelper pipMotionHelper,
FloatingContentCoordinator floatingContentCoordinator,
@@ -184,6 +184,7 @@
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
mPipBoundsAlgorithm = pipBoundsAlgorithm;
mPipBoundsState = pipBoundsState;
+ mPipSizeSpecHandler = pipSizeSpecHandler;
mPipTaskOrganizer = pipTaskOrganizer;
mMenuController = menuController;
mPipUiEventLogger = pipUiEventLogger;
@@ -271,10 +272,7 @@
private void reloadResources() {
final Resources res = mContext.getResources();
mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer);
- mExpandedShortestEdgeSize = res.getDimensionPixelSize(
- R.dimen.pip_expanded_shortest_edge_size);
mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
- mMinimumSizePercent = res.getFraction(R.fraction.config_pipShortestEdgePercent, 1, 1);
mPipDismissTargetHandler.updateMagneticTargetSize();
}
@@ -407,10 +405,7 @@
// Calculate the expanded size
float aspectRatio = (float) normalBounds.width() / normalBounds.height();
- Point displaySize = new Point();
- mContext.getDisplay().getRealSize(displaySize);
- Size expandedSize = mPipBoundsAlgorithm.getSizeForAspectRatio(
- aspectRatio, mExpandedShortestEdgeSize, displaySize.x, displaySize.y);
+ Size expandedSize = mPipSizeSpecHandler.getDefaultSize(aspectRatio);
mPipBoundsState.setExpandedBounds(
new Rect(0, 0, expandedSize.getWidth(), expandedSize.getHeight()));
Rect expandedMovementBounds = new Rect();
@@ -418,7 +413,7 @@
mPipBoundsState.getExpandedBounds(), insetBounds, expandedMovementBounds,
bottomOffset);
- updatePipSizeConstraints(insetBounds, normalBounds, aspectRatio);
+ updatePipSizeConstraints(normalBounds, aspectRatio);
// The extra offset does not really affect the movement bounds, but are applied based on the
// current state (ime showing, or shelf offset) when we need to actually shift
@@ -496,14 +491,14 @@
* @param aspectRatio aspect ratio to use for the calculation of min/max size
*/
public void updateMinMaxSize(float aspectRatio) {
- updatePipSizeConstraints(mInsetBounds, mPipBoundsState.getNormalBounds(),
+ updatePipSizeConstraints(mPipBoundsState.getNormalBounds(),
aspectRatio);
}
- private void updatePipSizeConstraints(Rect insetBounds, Rect normalBounds,
+ private void updatePipSizeConstraints(Rect normalBounds,
float aspectRatio) {
if (mPipResizeGestureHandler.isUsingPinchToZoom()) {
- updatePinchResizeSizeConstraints(insetBounds, normalBounds, aspectRatio);
+ updatePinchResizeSizeConstraints(aspectRatio);
} else {
mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height());
mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(),
@@ -511,26 +506,13 @@
}
}
- private void updatePinchResizeSizeConstraints(Rect insetBounds, Rect normalBounds,
- float aspectRatio) {
- final int shorterLength = Math.min(mPipBoundsState.getDisplayBounds().width(),
- mPipBoundsState.getDisplayBounds().height());
- final int totalHorizontalPadding = insetBounds.left
- + (mPipBoundsState.getDisplayBounds().width() - insetBounds.right);
- final int totalVerticalPadding = insetBounds.top
- + (mPipBoundsState.getDisplayBounds().height() - insetBounds.bottom);
+ private void updatePinchResizeSizeConstraints(float aspectRatio) {
final int minWidth, minHeight, maxWidth, maxHeight;
- if (aspectRatio > 1f) {
- minWidth = (int) Math.min(normalBounds.width(), shorterLength * mMinimumSizePercent);
- minHeight = (int) (minWidth / aspectRatio);
- maxWidth = (int) Math.max(normalBounds.width(), shorterLength - totalHorizontalPadding);
- maxHeight = (int) (maxWidth / aspectRatio);
- } else {
- minHeight = (int) Math.min(normalBounds.height(), shorterLength * mMinimumSizePercent);
- minWidth = (int) (minHeight * aspectRatio);
- maxHeight = (int) Math.max(normalBounds.height(), shorterLength - totalVerticalPadding);
- maxWidth = (int) (maxHeight * aspectRatio);
- }
+
+ minWidth = mPipSizeSpecHandler.getMinSize(aspectRatio).getWidth();
+ minHeight = mPipSizeSpecHandler.getMinSize(aspectRatio).getHeight();
+ maxWidth = mPipSizeSpecHandler.getMaxSize(aspectRatio).getWidth();
+ maxHeight = mPipSizeSpecHandler.getMaxSize(aspectRatio).getHeight();
mPipResizeGestureHandler.updateMinSize(minWidth, minHeight);
mPipResizeGestureHandler.updateMaxSize(maxWidth, maxHeight);
@@ -1064,11 +1046,6 @@
mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(),
mInsetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0);
mMotionHelper.onMovementBoundsChanged();
-
- boolean isMenuExpanded = mMenuState == MENU_STATE_FULL;
- mPipBoundsState.setMinEdgeSize(
- isMenuExpanded && willResizeMenu() ? mExpandedShortestEdgeSize
- : mPipBoundsAlgorithm.getDefaultMinSize());
}
private Rect getMovementBounds(Rect curBounds) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
index 1ff77f7..22feb43 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
@@ -41,6 +41,7 @@
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import com.android.wm.shell.pip.PipSnapAlgorithm;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -63,9 +64,10 @@
public TvPipBoundsAlgorithm(Context context,
@NonNull TvPipBoundsState tvPipBoundsState,
- @NonNull PipSnapAlgorithm pipSnapAlgorithm) {
+ @NonNull PipSnapAlgorithm pipSnapAlgorithm,
+ @NonNull PipSizeSpecHandler pipSizeSpecHandler) {
super(context, tvPipBoundsState, pipSnapAlgorithm,
- new PipKeepClearAlgorithmInterface() {});
+ new PipKeepClearAlgorithmInterface() {}, pipSizeSpecHandler);
this.mTvPipBoundsState = tvPipBoundsState;
this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm();
reloadResources(context);
@@ -370,7 +372,8 @@
if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
expandedSize = mTvPipBoundsState.getTvExpandedSize();
} else {
- int maxHeight = displayLayout.height() - (2 * mScreenEdgeInsets.y)
+ int maxHeight = displayLayout.height()
+ - (2 * mPipSizeSpecHandler.getScreenEdgeInsets().y)
- pipDecorations.top - pipDecorations.bottom;
float aspectRatioHeight = mFixedExpandedWidthInPx / expandedRatio;
@@ -393,7 +396,8 @@
if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_VERTICAL) {
expandedSize = mTvPipBoundsState.getTvExpandedSize();
} else {
- int maxWidth = displayLayout.width() - (2 * mScreenEdgeInsets.x)
+ int maxWidth = displayLayout.width()
+ - (2 * mPipSizeSpecHandler.getScreenEdgeInsets().x)
- pipDecorations.left - pipDecorations.right;
float aspectRatioWidth = mFixedExpandedHeightInPx * expandedRatio;
if (maxWidth > aspectRatioWidth) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
index ca22882..4e3ee51 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
@@ -30,6 +30,7 @@
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -64,8 +65,9 @@
private @NonNull Insets mPipMenuPermanentDecorInsets = Insets.NONE;
private @NonNull Insets mPipMenuTemporaryDecorInsets = Insets.NONE;
- public TvPipBoundsState(@NonNull Context context) {
- super(context);
+ public TvPipBoundsState(@NonNull Context context,
+ @NonNull PipSizeSpecHandler pipSizeSpecHandler) {
+ super(context, pipSizeSpecHandler);
mIsTvExpandedPipSupported = context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_EXPANDED_PICTURE_IN_PICTURE);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 4e1b046..6bc666f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -50,6 +50,7 @@
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellController;
@@ -102,6 +103,7 @@
private final ShellController mShellController;
private final TvPipBoundsState mTvPipBoundsState;
+ private final PipSizeSpecHandler mPipSizeSpecHandler;
private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
private final TvPipBoundsController mTvPipBoundsController;
private final PipAppOpsListener mAppOpsListener;
@@ -133,6 +135,7 @@
ShellInit shellInit,
ShellController shellController,
TvPipBoundsState tvPipBoundsState,
+ PipSizeSpecHandler pipSizeSpecHandler,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
PipAppOpsListener pipAppOpsListener,
@@ -151,6 +154,7 @@
shellInit,
shellController,
tvPipBoundsState,
+ pipSizeSpecHandler,
tvPipBoundsAlgorithm,
tvPipBoundsController,
pipAppOpsListener,
@@ -171,6 +175,7 @@
ShellInit shellInit,
ShellController shellController,
TvPipBoundsState tvPipBoundsState,
+ PipSizeSpecHandler pipSizeSpecHandler,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
PipAppOpsListener pipAppOpsListener,
@@ -189,9 +194,13 @@
mShellController = shellController;
mDisplayController = displayController;
+ DisplayLayout layout = new DisplayLayout(context, context.getDisplay());
+
mTvPipBoundsState = tvPipBoundsState;
+ mTvPipBoundsState.setDisplayLayout(layout);
mTvPipBoundsState.setDisplayId(context.getDisplayId());
- mTvPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay()));
+ mPipSizeSpecHandler = pipSizeSpecHandler;
+ mPipSizeSpecHandler.setDisplayLayout(layout);
mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
mTvPipBoundsController = tvPipBoundsController;
mTvPipBoundsController.setListener(this);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
index 053491e..22e8045 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
@@ -430,7 +430,8 @@
}
@Override
- public @Nullable SplashScreenView get() {
+ @Nullable
+ public SplashScreenView get() {
synchronized (this) {
while (!mIsViewSet) {
try {
@@ -691,7 +692,7 @@
private final TaskSnapshotWindow mTaskSnapshotWindow;
private SplashScreenView mContentView;
private boolean mSetSplashScreen;
- private @StartingWindowType int mSuggestType;
+ @StartingWindowType private int mSuggestType;
private int mBGColor;
private final long mCreateTime;
private int mSystemBarAppearance;
@@ -732,7 +733,7 @@
// Reset the system bar color which set by splash screen, make it align to the app.
private void clearSystemBarColor() {
- if (mDecorView == null) {
+ if (mDecorView == null || !mDecorView.isAttachedToWindow()) {
return;
}
if (mDecorView.getLayoutParams() instanceof WindowManager.LayoutParams) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 476a7ec..2981f5e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -36,6 +36,7 @@
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.transition.Transitions;
/**
* View model for the window decoration with a caption and shadows. Works with
@@ -65,6 +66,9 @@
mTaskOrganizer = taskOrganizer;
mDisplayController = displayController;
mSyncQueue = syncQueue;
+ if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTaskOperations = new TaskOperations(null, mContext, mSyncQueue);
+ }
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
index 907977c..3734487 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
@@ -29,7 +29,8 @@
*/
public interface WindowDecorViewModel {
/**
- * Sets the transition starter that starts freeform task transitions.
+ * Sets the transition starter that starts freeform task transitions. Only called when
+ * {@link com.android.wm.shell.transition.Transitions#ENABLE_SHELL_TRANSITIONS} is {@code true}.
*
* @param transitionStarter the transition starter that starts freeform task transitions
*/
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt
index c02e2d1..3bfbcd2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt
@@ -35,7 +35,7 @@
private val user0Bubbles = listOf(
BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1,
- isClearable = true),
+ isDismissable = true),
BubbleEntity(10, "com.example.chat", "alice and bob", "0k2", 0, 16537428, "title", 2,
null),
BubbleEntity(0, "com.example.messenger", "shortcut-2", "0k3", 120, 0, null,
@@ -44,7 +44,7 @@
private val user1Bubbles = listOf(
BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3,
- isClearable = true),
+ isDismissable = true),
BubbleEntity(12, "com.example.chat", "alice and bob", "1k2", 0, 16537428, "title", 4,
null),
BubbleEntity(1, "com.example.messenger", "shortcut-2", "1k3", 120, 0, null,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
index 298d0a6..ec264a6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
@@ -32,6 +32,7 @@
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import org.junit.Before;
import org.junit.Test;
@@ -57,17 +58,22 @@
private PipBoundsAlgorithm mPipBoundsAlgorithm;
private DisplayInfo mDefaultDisplayInfo;
private PipBoundsState mPipBoundsState;
+ private PipSizeSpecHandler mPipSizeSpecHandler;
@Before
public void setUp() throws Exception {
initializeMockResources();
- mPipBoundsState = new PipBoundsState(mContext);
+ mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
+ mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler);
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
- new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {});
+ new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {},
+ mPipSizeSpecHandler);
- mPipBoundsState.setDisplayLayout(
- new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true));
+ DisplayLayout layout =
+ new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true);
+ mPipBoundsState.setDisplayLayout(layout);
+ mPipSizeSpecHandler.setDisplayLayout(layout);
}
private void initializeMockResources() {
@@ -120,9 +126,7 @@
@Test
public void getDefaultBounds_noOverrideMinSize_matchesDefaultSizeAndAspectRatio() {
- final Size defaultSize = mPipBoundsAlgorithm.getSizeForAspectRatio(DEFAULT_ASPECT_RATIO,
- DEFAULT_MIN_EDGE_SIZE, mDefaultDisplayInfo.logicalWidth,
- mDefaultDisplayInfo.logicalHeight);
+ final Size defaultSize = mPipSizeSpecHandler.getDefaultSize(DEFAULT_ASPECT_RATIO);
mPipBoundsState.setOverrideMinSize(null);
final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
@@ -296,9 +300,9 @@
(MAX_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2
};
final Size[] minimalSizes = new Size[] {
- new Size((int) (100 * aspectRatios[0]), 100),
- new Size((int) (100 * aspectRatios[1]), 100),
- new Size((int) (100 * aspectRatios[2]), 100)
+ new Size((int) (200 * aspectRatios[0]), 200),
+ new Size((int) (200 * aspectRatios[1]), 200),
+ new Size((int) (200 * aspectRatios[2]), 200)
};
for (int i = 0; i < aspectRatios.length; i++) {
final float aspectRatio = aspectRatios[i];
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
index 8e30f65..341a451 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
@@ -33,6 +33,7 @@
import com.android.internal.util.function.TriConsumer;
import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import org.junit.Before;
import org.junit.Test;
@@ -57,7 +58,7 @@
@Before
public void setUp() {
- mPipBoundsState = new PipBoundsState(mContext);
+ mPipBoundsState = new PipBoundsState(mContext, new PipSizeSpecHandler(mContext));
mTestComponentName1 = new ComponentName(mContext, "component1");
mTestComponentName2 = new ComponentName(mContext, "component2");
}
@@ -161,10 +162,10 @@
@Test
public void testSetOverrideMinSize_notChanged_callbackNotInvoked() {
final Runnable callback = mock(Runnable.class);
- mPipBoundsState.setOverrideMinSize(new Size(5, 5));
+ mPipBoundsState.setOverrideMinSize(new Size(100, 150));
mPipBoundsState.setOnMinimalSizeChangeCallback(callback);
- mPipBoundsState.setOverrideMinSize(new Size(5, 5));
+ mPipBoundsState.setOverrideMinSize(new Size(100, 150));
verify(callback, never()).run();
}
@@ -174,11 +175,11 @@
mPipBoundsState.setOverrideMinSize(null);
assertEquals(0, mPipBoundsState.getOverrideMinEdgeSize());
- mPipBoundsState.setOverrideMinSize(new Size(5, 10));
- assertEquals(5, mPipBoundsState.getOverrideMinEdgeSize());
+ mPipBoundsState.setOverrideMinSize(new Size(100, 110));
+ assertEquals(100, mPipBoundsState.getOverrideMinEdgeSize());
- mPipBoundsState.setOverrideMinSize(new Size(15, 10));
- assertEquals(10, mPipBoundsState.getOverrideMinEdgeSize());
+ mPipBoundsState.setOverrideMinSize(new Size(150, 200));
+ assertEquals(150, mPipBoundsState.getOverrideMinEdgeSize());
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
index 17e7d74..2da4af8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -53,6 +53,7 @@
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.pip.phone.PhonePipMenuController;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import com.android.wm.shell.splitscreen.SplitScreenController;
import org.junit.Before;
@@ -86,6 +87,7 @@
private PipBoundsState mPipBoundsState;
private PipTransitionState mPipTransitionState;
private PipBoundsAlgorithm mPipBoundsAlgorithm;
+ private PipSizeSpecHandler mPipSizeSpecHandler;
private ComponentName mComponent1;
private ComponentName mComponent2;
@@ -95,10 +97,12 @@
MockitoAnnotations.initMocks(this);
mComponent1 = new ComponentName(mContext, "component1");
mComponent2 = new ComponentName(mContext, "component2");
- mPipBoundsState = new PipBoundsState(mContext);
+ mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
+ mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler);
mPipTransitionState = new PipTransitionState();
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
- new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {});
+ new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {},
+ mPipSizeSpecHandler);
mMainExecutor = new TestShellExecutor();
mPipTaskOrganizer = new PipTaskOrganizer(mContext,
mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState,
@@ -253,8 +257,10 @@
private void preparePipTaskOrg() {
final DisplayInfo info = new DisplayInfo();
- mPipBoundsState.setDisplayLayout(new DisplayLayout(info,
- mContext.getResources(), true, true));
+ DisplayLayout layout = new DisplayLayout(info,
+ mContext.getResources(), true, true);
+ mPipBoundsState.setDisplayLayout(layout);
+ mPipSizeSpecHandler.setDisplayLayout(layout);
mPipTaskOrganizer.setOneShotAnimationType(PipAnimationController.ANIM_TYPE_ALPHA);
mPipTaskOrganizer.setSurfaceControlTransactionFactory(
MockSurfaceControlHelper::createMockSurfaceControlTransaction);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 35c09a1..4a68287 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -65,7 +65,6 @@
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipTransitionState;
-import com.android.wm.shell.recents.IRecentTasksListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -108,6 +107,7 @@
@Mock private PipMotionHelper mMockPipMotionHelper;
@Mock private WindowManagerShellWrapper mMockWindowManagerShellWrapper;
@Mock private PipBoundsState mMockPipBoundsState;
+ @Mock private PipSizeSpecHandler mMockPipSizeSpecHandler;
@Mock private TaskStackListenerImpl mMockTaskStackListener;
@Mock private ShellExecutor mMockExecutor;
@Mock private Optional<OneHandedController> mMockOneHandedController;
@@ -130,11 +130,12 @@
mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler,
mShellController, mMockDisplayController, mMockPipAnimationController,
mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
- mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
- mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
- mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper,
- mMockTaskStackListener, mMockPipParamsChangedForwarder,
- mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor);
+ mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipMotionHelper,
+ mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer,
+ mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController,
+ mMockWindowManagerShellWrapper, mMockTaskStackListener,
+ mMockPipParamsChangedForwarder, mMockDisplayInsetsController,
+ mMockOneHandedController, mMockExecutor);
mShellInit.init();
when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm);
when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper);
@@ -220,11 +221,12 @@
assertNull(PipController.create(spyContext, shellInit, mMockShellCommandHandler,
mShellController, mMockDisplayController, mMockPipAnimationController,
mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
- mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
- mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
- mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper,
- mMockTaskStackListener, mMockPipParamsChangedForwarder,
- mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor));
+ mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipMotionHelper,
+ mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer,
+ mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController,
+ mMockWindowManagerShellWrapper, mMockTaskStackListener,
+ mMockPipParamsChangedForwarder, mMockDisplayInsetsController,
+ mMockOneHandedController, mMockExecutor));
}
@Test
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 c1993b2..c7b9eb3 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
@@ -85,15 +85,18 @@
private PipBoundsState mPipBoundsState;
+ private PipSizeSpecHandler mPipSizeSpecHandler;
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mPipBoundsState = new PipBoundsState(mContext);
+ mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
+ mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler);
final PipSnapAlgorithm pipSnapAlgorithm = new PipSnapAlgorithm();
final PipKeepClearAlgorithmInterface pipKeepClearAlgorithm =
new PipKeepClearAlgorithmInterface() {};
final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext,
- mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm);
+ mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm, mPipSizeSpecHandler);
final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState,
mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm,
mMockPipTransitionController, mFloatingContentCoordinator);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java
new file mode 100644
index 0000000..d9ff7d1
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.phone;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.SystemProperties;
+import android.testing.AndroidTestingRunner;
+import android.util.Size;
+import android.view.DisplayInfo;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayLayout;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.exceptions.misusing.InvalidUseOfMatchersException;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Unit test against {@link PipSizeSpecHandler} with feature flag on.
+ */
+@RunWith(AndroidTestingRunner.class)
+public class PipSizeSpecHandlerTest extends ShellTestCase {
+ /** A sample overridden min edge size. */
+ private static final int OVERRIDE_MIN_EDGE_SIZE = 40;
+ /** A sample default min edge size */
+ private static final int DEFAULT_MIN_EDGE_SIZE = 40;
+ /** Display edge size */
+ private static final int DISPLAY_EDGE_SIZE = 1000;
+ /** Default sizing percentage */
+ private static final float DEFAULT_PERCENT = 0.6f;
+ /** Minimum sizing percentage */
+ private static final float MIN_PERCENT = 0.5f;
+ /** Aspect ratio that the new PIP size spec logic optimizes for. */
+ private static final float OPTIMIZED_ASPECT_RATIO = 9f / 16;
+
+ /** A map of aspect ratios to be tested to expected sizes */
+ private static Map<Float, Size> sExpectedMaxSizes;
+ private static Map<Float, Size> sExpectedDefaultSizes;
+ private static Map<Float, Size> sExpectedMinSizes;
+ /** A static mockito session object to mock {@link SystemProperties} */
+ private static StaticMockitoSession sStaticMockitoSession;
+
+ @Mock private Context mContext;
+ @Mock private Resources mResources;
+
+ private PipSizeSpecHandler mPipSizeSpecHandler;
+
+ /**
+ * Sets up static Mockito session for SystemProperties and mocks necessary static methods.
+ */
+ private static void setUpStaticSystemPropertiesSession() {
+ sStaticMockitoSession = mockitoSession()
+ .mockStatic(SystemProperties.class).startMocking();
+ // make sure the feature flag is on
+ when(SystemProperties.getBoolean(anyString(), anyBoolean())).thenReturn(true);
+ when(SystemProperties.get(anyString(), anyString())).thenAnswer(invocation -> {
+ String property = invocation.getArgument(0);
+ if (property.equals("com.android.wm.shell.pip.phone.def_percentage")) {
+ return Float.toString(DEFAULT_PERCENT);
+ } else if (property.equals("com.android.wm.shell.pip.phone.min_percentage")) {
+ return Float.toString(MIN_PERCENT);
+ }
+
+ // throw an exception if illegal arguments are used for these tests
+ throw new InvalidUseOfMatchersException(
+ String.format("Argument %s does not match", property)
+ );
+ });
+ }
+
+ /**
+ * Initializes the map with the aspect ratios to be tested and corresponding expected max sizes.
+ */
+ private static void initExpectedSizes() {
+ sExpectedMaxSizes = new HashMap<>();
+ sExpectedDefaultSizes = new HashMap<>();
+ sExpectedMinSizes = new HashMap<>();
+
+ sExpectedMaxSizes.put(16f / 9, new Size(1000, 562));
+ sExpectedDefaultSizes.put(16f / 9, new Size(600, 337));
+ sExpectedMinSizes.put(16f / 9, new Size(499, 281));
+
+ sExpectedMaxSizes.put(4f / 3, new Size(892, 669));
+ sExpectedDefaultSizes.put(4f / 3, new Size(535, 401));
+ sExpectedMinSizes.put(4f / 3, new Size(445, 334));
+
+ sExpectedMaxSizes.put(3f / 4, new Size(669, 892));
+ sExpectedDefaultSizes.put(3f / 4, new Size(401, 535));
+ sExpectedMinSizes.put(3f / 4, new Size(334, 445));
+
+ sExpectedMaxSizes.put(9f / 16, new Size(562, 999));
+ sExpectedDefaultSizes.put(9f / 16, new Size(337, 599));
+ sExpectedMinSizes.put(9f / 16, new Size(281, 499));
+ }
+
+ private void forEveryTestCaseCheck(Map<Float, Size> expectedSizes,
+ Function<Float, Size> callback) {
+ for (Map.Entry<Float, Size> expectedSizesEntry : expectedSizes.entrySet()) {
+ float aspectRatio = expectedSizesEntry.getKey();
+ Size expectedSize = expectedSizesEntry.getValue();
+
+ Assert.assertEquals(expectedSize, callback.apply(aspectRatio));
+ }
+ }
+
+ @Before
+ public void setUp() {
+ initExpectedSizes();
+ setUpStaticSystemPropertiesSession();
+
+ when(mResources.getDimensionPixelSize(anyInt())).thenReturn(DEFAULT_MIN_EDGE_SIZE);
+ when(mResources.getFloat(anyInt())).thenReturn(OPTIMIZED_ASPECT_RATIO);
+ when(mResources.getString(anyInt())).thenReturn("0x0");
+ when(mResources.getDisplayMetrics())
+ .thenReturn(getContext().getResources().getDisplayMetrics());
+
+ // set up the mock context for spec handler specifically
+ when(mContext.getResources()).thenReturn(mResources);
+
+ mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
+
+ // no overridden min edge size by default
+ mPipSizeSpecHandler.setOverrideMinSize(null);
+
+ DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.logicalWidth = DISPLAY_EDGE_SIZE;
+ displayInfo.logicalHeight = DISPLAY_EDGE_SIZE;
+
+ // use the parent context (not the mocked one) to obtain the display layout
+ // this is done to avoid unnecessary mocking while allowing for custom display dimensions
+ DisplayLayout displayLayout = new DisplayLayout(displayInfo, getContext().getResources(),
+ false, false);
+ mPipSizeSpecHandler.setDisplayLayout(displayLayout);
+ }
+
+ @After
+ public void cleanUp() {
+ sStaticMockitoSession.finishMocking();
+ }
+
+ @Test
+ public void testGetMaxSize() {
+ forEveryTestCaseCheck(sExpectedMaxSizes,
+ (aspectRatio) -> mPipSizeSpecHandler.getMaxSize(aspectRatio));
+ }
+
+ @Test
+ public void testGetDefaultSize() {
+ forEveryTestCaseCheck(sExpectedDefaultSizes,
+ (aspectRatio) -> mPipSizeSpecHandler.getDefaultSize(aspectRatio));
+ }
+
+ @Test
+ public void testGetMinSize() {
+ forEveryTestCaseCheck(sExpectedMinSizes,
+ (aspectRatio) -> mPipSizeSpecHandler.getMinSize(aspectRatio));
+ }
+
+ @Test
+ public void testGetSizeForAspectRatio_noOverrideMinSize() {
+ // an initial size with 16:9 aspect ratio
+ Size initSize = new Size(600, 337);
+
+ Size expectedSize = new Size(337, 599);
+ Size actualSize = mPipSizeSpecHandler.getSizeForAspectRatio(initSize, 9f / 16);
+
+ Assert.assertEquals(expectedSize, actualSize);
+ }
+
+ @Test
+ public void testGetSizeForAspectRatio_withOverrideMinSize() {
+ // an initial size with a 1:1 aspect ratio
+ mPipSizeSpecHandler.setOverrideMinSize(new Size(OVERRIDE_MIN_EDGE_SIZE,
+ OVERRIDE_MIN_EDGE_SIZE));
+ // make sure initial size is same as override min size
+ Size initSize = mPipSizeSpecHandler.getOverrideMinSize();
+
+ Size expectedSize = new Size(40, 71);
+ Size actualSize = mPipSizeSpecHandler.getSizeForAspectRatio(initSize, 9f / 16);
+
+ Assert.assertEquals(expectedSize, actualSize);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
index 8ad2932..5c4863f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
@@ -25,6 +25,7 @@
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.util.Size;
import androidx.test.filters.SmallTest;
@@ -90,6 +91,7 @@
private PipSnapAlgorithm mPipSnapAlgorithm;
private PipMotionHelper mMotionHelper;
private PipResizeGestureHandler mPipResizeGestureHandler;
+ private PipSizeSpecHandler mPipSizeSpecHandler;
private DisplayLayout mDisplayLayout;
private Rect mInsetBounds;
@@ -103,16 +105,17 @@
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mPipBoundsState = new PipBoundsState(mContext);
+ mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
+ mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler);
mPipSnapAlgorithm = new PipSnapAlgorithm();
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm,
- new PipKeepClearAlgorithmInterface() {});
+ new PipKeepClearAlgorithmInterface() {}, mPipSizeSpecHandler);
PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState,
mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm,
mMockPipTransitionController, mFloatingContentCoordinator);
mPipTouchHandler = new PipTouchHandler(mContext, mShellInit, mPhonePipMenuController,
- mPipBoundsAlgorithm, mPipBoundsState, mPipTaskOrganizer, pipMotionHelper,
- mFloatingContentCoordinator, mPipUiEventLogger, mMainExecutor);
+ mPipBoundsAlgorithm, mPipBoundsState, mPipSizeSpecHandler, mPipTaskOrganizer,
+ pipMotionHelper, mFloatingContentCoordinator, mPipUiEventLogger, mMainExecutor);
// We aren't actually using ShellInit, so just call init directly
mPipTouchHandler.onInit();
mMotionHelper = Mockito.spy(mPipTouchHandler.getMotionHelper());
@@ -122,6 +125,7 @@
mDisplayLayout = new DisplayLayout(mContext, mContext.getDisplay());
mPipBoundsState.setDisplayLayout(mDisplayLayout);
+ mPipSizeSpecHandler.setDisplayLayout(mDisplayLayout);
mInsetBounds = new Rect(mPipBoundsState.getDisplayBounds().left + INSET,
mPipBoundsState.getDisplayBounds().top + INSET,
mPipBoundsState.getDisplayBounds().right - INSET,
@@ -154,13 +158,17 @@
mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mPipBounds, mCurBounds,
mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation);
+ // getting the expected min and max size
+ float aspectRatio = (float) mPipBounds.width() / mPipBounds.height();
+ Size expectedMinSize = mPipSizeSpecHandler.getMinSize(aspectRatio);
+ Size expectedMaxSize = mPipSizeSpecHandler.getMaxSize(aspectRatio);
+
assertEquals(expectedMovementBounds, mPipBoundsState.getNormalMovementBounds());
verify(mPipResizeGestureHandler, times(1))
- .updateMinSize(mPipBounds.width(), mPipBounds.height());
+ .updateMinSize(expectedMinSize.getWidth(), expectedMinSize.getHeight());
verify(mPipResizeGestureHandler, times(1))
- .updateMaxSize(shorterLength - 2 * mInsetBounds.left,
- shorterLength - 2 * mInsetBounds.left);
+ .updateMaxSize(expectedMaxSize.getWidth(), expectedMaxSize.getHeight());
}
@Test
diff --git a/packages/SettingsLib/IllustrationPreference/res/values/attrs.xml b/packages/SettingsLib/IllustrationPreference/res/values/attrs.xml
new file mode 100644
index 0000000..141886c
--- /dev/null
+++ b/packages/SettingsLib/IllustrationPreference/res/values/attrs.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <declare-styleable name="IllustrationPreference">
+ <attr name="dynamicColor" format="boolean" />
+ </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index 1592094..3b90275 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -61,6 +61,8 @@
private View mMiddleGroundView;
private OnBindListener mOnBindListener;
+ private boolean mLottieDynamicColor;
+
/**
* Interface to listen in on when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
*/
@@ -146,6 +148,10 @@
ColorUtils.applyDynamicColors(getContext(), illustrationView);
}
+ if (mLottieDynamicColor) {
+ LottieColorUtils.applyDynamicColors(getContext(), illustrationView);
+ }
+
if (mOnBindListener != null) {
mOnBindListener.onBind(illustrationView);
}
@@ -262,6 +268,21 @@
}
}
+ /**
+ * Sets the lottie illustration apply dynamic color.
+ */
+ public void applyDynamicColor() {
+ mLottieDynamicColor = true;
+ notifyChanged();
+ }
+
+ /**
+ * Return if the lottie illustration apply dynamic color or not.
+ */
+ public boolean isApplyDynamicColor() {
+ return mLottieDynamicColor;
+ }
+
private void resetImageResourceCache() {
mImageDrawable = null;
mImageUri = null;
@@ -403,9 +424,15 @@
mIsAutoScale = false;
if (attrs != null) {
- final TypedArray a = context.obtainStyledAttributes(attrs,
+ TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.LottieAnimationView, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
mImageResId = a.getResourceId(R.styleable.LottieAnimationView_lottie_rawRes, 0);
+
+ a = context.obtainStyledAttributes(attrs,
+ R.styleable.IllustrationPreference, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
+ mLottieDynamicColor = a.getBoolean(R.styleable.IllustrationPreference_dynamicColor,
+ false);
+
a.recycle();
}
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
index 103512d..21e119a 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
@@ -215,4 +215,20 @@
assertThat(mOnBindListenerAnimationView).isNull();
}
+
+ @Test
+ public void onBindViewHolder_default_shouldNotApplyDynamicColor() {
+ mPreference.onBindViewHolder(mViewHolder);
+
+ assertThat(mPreference.isApplyDynamicColor()).isFalse();
+ }
+
+ @Test
+ public void onBindViewHolder_applyDynamicColor_shouldReturnTrue() {
+ mPreference.applyDynamicColor();
+
+ mPreference.onBindViewHolder(mViewHolder);
+
+ assertThat(mPreference.isApplyDynamicColor()).isTrue();
+ }
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java
new file mode 100644
index 0000000..bdd4869
--- /dev/null
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider.settings.backup;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+/** Settings that should not be restored when target device is a large screen
+ * i.e. tablets and foldables in unfolded state
+ */
+public class LargeScreenSettings {
+ private static final float LARGE_SCREEN_MIN_DPS = 600;
+ private static final String LARGE_SCREEN_DO_NOT_RESTORE = "accelerometer_rotation";
+
+ /**
+ * Autorotation setting should not be restored when the target device is a large screen.
+ * (b/243489549)
+ */
+ public static boolean doNotRestoreIfLargeScreenSetting(String key, Context context) {
+ return isLargeScreen(context) && LARGE_SCREEN_DO_NOT_RESTORE.equals(key);
+ }
+
+ // copied from systemui/shared/...Utilities.java
+ // since we don't want to add compile time dependency on sys ui package
+ private static boolean isLargeScreen(Context context) {
+ final WindowManager windowManager = context.getSystemService(WindowManager.class);
+ final Rect bounds = windowManager.getCurrentWindowMetrics().getBounds();
+ float smallestWidth = dpiFromPx(Math.min(bounds.width(), bounds.height()),
+ context.getResources().getConfiguration().densityDpi);
+ return smallestWidth >= LARGE_SCREEN_MIN_DPS;
+ }
+
+ private static float dpiFromPx(float size, int densityDpi) {
+ float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
+ return (size / densityRatio);
+ }
+}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 211030a..c537d96 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -130,6 +130,7 @@
Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ERRORS,
Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO,
Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
+ Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
Settings.Secure.VR_DISPLAY_MODE,
Settings.Secure.NOTIFICATION_BADGING,
Settings.Secure.NOTIFICATION_DISMISS_RTL,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 0539f09..8a67a0d 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -189,6 +189,8 @@
VALIDATORS.put(Secure.ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO, ANY_STRING_VALIDATOR);
VALIDATORS.put(Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
ANY_STRING_VALIDATOR);
+ VALIDATORS.put(Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+ ANY_STRING_VALIDATOR);
VALIDATORS.put(Secure.ASSIST_GESTURE_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ASSIST_GESTURE_SILENCE_ALERTS_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ASSIST_GESTURE_WAKE_ENABLED, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index d3afccc..d0055d7 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -37,6 +37,7 @@
import android.provider.Settings;
import android.provider.settings.backup.DeviceSpecificSettings;
import android.provider.settings.backup.GlobalSettings;
+import android.provider.settings.backup.LargeScreenSettings;
import android.provider.settings.backup.SecureSettings;
import android.provider.settings.backup.SystemSettings;
import android.provider.settings.validators.GlobalSettingsValidators;
@@ -812,6 +813,12 @@
continue;
}
+ if (LargeScreenSettings.doNotRestoreIfLargeScreenSetting(key, getBaseContext())) {
+ Log.i(TAG, "Skipping restore for setting " + key + " as the target device "
+ + "is a large screen (i.e tablet or foldable in unfolded state)");
+ continue;
+ }
+
String value = null;
boolean hasValueToRestore = false;
if (cachedEntries.indexOfKey(key) >= 0) {
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 3b862ff..3915012 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -356,6 +356,9 @@
<!-- Permission needed to test wallpaper dimming -->
<uses-permission android:name="android.permission.SET_WALLPAPER_DIM_AMOUNT" />
+ <!-- Permission needed to test wallpaper read methods -->
+ <uses-permission android:name="android.permission.READ_WALLPAPER_INTERNAL" />
+
<!-- Permission required to test ContentResolver caching. -->
<uses-permission android:name="android.permission.CACHE_CONTENT" />
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index b6e006f..e96aead5 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -674,6 +674,7 @@
<intent-filter>
<action android:name="android.telecom.action.SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG" />
<category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="tel" />
</intent-filter>
</activity>
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
index 4322d53..74bc910 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
@@ -44,6 +44,20 @@
}
// language=AGSL
companion object {
+ // Default fade in/ out values. The value range is [0,1].
+ const val DEFAULT_FADE_IN_START = 0f
+ const val DEFAULT_FADE_OUT_END = 1f
+
+ const val DEFAULT_BASE_RING_FADE_IN_END = 0.1f
+ const val DEFAULT_BASE_RING_FADE_OUT_START = 0.3f
+
+ const val DEFAULT_SPARKLE_RING_FADE_IN_END = 0.1f
+ const val DEFAULT_SPARKLE_RING_FADE_OUT_START = 0.4f
+
+ const val DEFAULT_CENTER_FILL_FADE_IN_END = 0f
+ const val DEFAULT_CENTER_FILL_FADE_OUT_START = 0f
+ const val DEFAULT_CENTER_FILL_FADE_OUT_END = 0.6f
+
private const val SHADER_UNIFORMS =
"""
uniform vec2 in_center;
@@ -143,11 +157,26 @@
}
private fun subProgress(start: Float, end: Float, progress: Float): Float {
+ // Avoid division by 0.
+ if (start == end) {
+ // If start and end are the same and progress has exceeded the start/ end point,
+ // treat it as 1, otherwise 0.
+ return if (progress > start) 1f else 0f
+ }
+
val min = Math.min(start, end)
val max = Math.max(start, end)
val sub = Math.min(Math.max(progress, min), max)
return (sub - start) / (end - start)
}
+
+ private fun getFade(fadeParams: FadeParams, rawProgress: Float): Float {
+ val fadeIn = subProgress(fadeParams.fadeInStart, fadeParams.fadeInEnd, rawProgress)
+ val fadeOut =
+ 1f - subProgress(fadeParams.fadeOutStart, fadeParams.fadeOutEnd, rawProgress)
+
+ return Math.min(fadeIn, fadeOut)
+ }
}
/** Sets the center position of the ripple. */
@@ -172,17 +201,9 @@
field = value
progress = Interpolators.STANDARD.getInterpolation(value)
- val fadeIn = subProgress(0f, 0.1f, value)
- val fadeOutNoise = subProgress(0.4f, 1f, value)
- var fadeOutRipple = 0f
- var fadeFill = 0f
- if (!rippleFill) {
- fadeFill = subProgress(0f, 0.6f, value)
- fadeOutRipple = subProgress(0.3f, 1f, value)
- }
- setFloatUniform("in_fadeSparkle", Math.min(fadeIn, 1 - fadeOutNoise))
- setFloatUniform("in_fadeFill", 1 - fadeFill)
- setFloatUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
+ setFloatUniform("in_fadeSparkle", getFade(sparkleRingFadeParams, value))
+ setFloatUniform("in_fadeRing", getFade(baseRingFadeParams, value))
+ setFloatUniform("in_fadeFill", getFade(centerFillFadeParams, value))
}
/** Progress with Standard easing curve applied. */
@@ -232,21 +253,130 @@
setFloatUniform("in_distort_xy", 75 * value)
}
+ /**
+ * Pixel density of the screen that the effects are rendered to.
+ *
+ * <p>This value should come from [resources.displayMetrics.density].
+ */
var pixelDensity: Float = 1.0f
set(value) {
field = value
setFloatUniform("in_pixelDensity", value)
}
- /**
- * True if the ripple should stayed filled in as it expands to give a filled-in circle effect.
- * False for a ring effect.
- */
- var rippleFill: Boolean = false
-
var currentWidth: Float = 0f
private set
var currentHeight: Float = 0f
private set
+
+ /**
+ * True if the ripple should stayed filled in as it expands to give a filled-in circle effect.
+ * False for a ring effect.
+ *
+ * <p>You must reset fade params after changing this.
+ *
+ * TODO(b/265326983): Remove this and only expose fade params.
+ */
+ var rippleFill: Boolean = false
+ set(value) {
+ if (value) {
+ baseRingFadeParams.fadeOutStart = 1f
+ baseRingFadeParams.fadeOutEnd = 1f
+
+ centerFillFadeParams.fadeInStart = 0f
+ centerFillFadeParams.fadeInEnd = 0f
+ centerFillFadeParams.fadeOutStart = 1f
+ centerFillFadeParams.fadeOutEnd = 1f
+ } else {
+ // Set back to the original fade parameters.
+ // Ideally this should be set by the client as they know the initial value.
+ baseRingFadeParams.fadeOutStart = DEFAULT_BASE_RING_FADE_OUT_START
+ baseRingFadeParams.fadeOutEnd = DEFAULT_FADE_OUT_END
+
+ centerFillFadeParams.fadeInStart = DEFAULT_FADE_IN_START
+ centerFillFadeParams.fadeInEnd = DEFAULT_CENTER_FILL_FADE_IN_END
+ centerFillFadeParams.fadeOutStart = DEFAULT_CENTER_FILL_FADE_OUT_START
+ centerFillFadeParams.fadeOutEnd = DEFAULT_CENTER_FILL_FADE_OUT_END
+ }
+ field = value
+ }
+
+ /** Parameters that are used to fade in/ out of the sparkle ring. */
+ val sparkleRingFadeParams =
+ FadeParams(
+ DEFAULT_FADE_IN_START,
+ DEFAULT_SPARKLE_RING_FADE_IN_END,
+ DEFAULT_SPARKLE_RING_FADE_OUT_START,
+ DEFAULT_FADE_OUT_END
+ )
+
+ /**
+ * Parameters that are used to fade in/ out of the base ring.
+ *
+ * <p>Note that the shader draws the sparkle ring on top of the base ring.
+ */
+ val baseRingFadeParams =
+ FadeParams(
+ DEFAULT_FADE_IN_START,
+ DEFAULT_BASE_RING_FADE_IN_END,
+ DEFAULT_BASE_RING_FADE_OUT_START,
+ DEFAULT_FADE_OUT_END
+ )
+
+ /**
+ * Parameters that are used to fade in/ out of the center fill.
+ *
+ * <p>Note that if [rippleFill] is set to true, those will be ignored and the center fill will
+ * be always full alpha.
+ */
+ val centerFillFadeParams =
+ FadeParams(
+ DEFAULT_FADE_IN_START,
+ DEFAULT_CENTER_FILL_FADE_IN_END,
+ DEFAULT_CENTER_FILL_FADE_OUT_START,
+ DEFAULT_CENTER_FILL_FADE_OUT_END
+ )
+
+ /**
+ * Parameters used for fade in and outs of the ripple.
+ *
+ * <p>Note that all the fade in/ outs are "linear" progression.
+ * ```
+ * (opacity)
+ * 1
+ * │
+ * maxAlpha ← ――――――――――――
+ * │ / \
+ * │ / \
+ * minAlpha ←――――/ \―――― (alpha change)
+ * │
+ * │
+ * 0 ―――↑―――↑―――――――――↑―――↑――――1 (progress)
+ * fadeIn fadeOut
+ * Start & End Start & End
+ * ```
+ * <p>If no fade in/ out is needed, set [fadeInStart] and [fadeInEnd] to 0; [fadeOutStart] and
+ * [fadeOutEnd] to 1.
+ */
+ data class FadeParams(
+ /**
+ * The starting point of the fade out which ends at [fadeInEnd], given that the animation
+ * goes from 0 to 1.
+ */
+ var fadeInStart: Float = DEFAULT_FADE_IN_START,
+ /**
+ * The endpoint of the fade in when the fade in starts at [fadeInStart], given that the
+ * animation goes from 0 to 1.
+ */
+ var fadeInEnd: Float,
+ /**
+ * The starting point of the fade out which ends at 1, given that the animation goes from 0
+ * to 1.
+ */
+ var fadeOutStart: Float,
+
+ /** The endpoint of the fade out, given that the animation goes from 0 to 1. */
+ var fadeOutEnd: Float = DEFAULT_FADE_OUT_END,
+ )
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
index bc4796a..3c9328c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -117,15 +117,6 @@
rippleShader.color = ColorUtils.setAlphaComponent(color, alpha)
}
- /**
- * Set whether the ripple should remain filled as the ripple expands.
- *
- * See [RippleShader.rippleFill].
- */
- fun setRippleFill(rippleFill: Boolean) {
- rippleShader.rippleFill = rippleFill
- }
-
/** Set the intensity of the sparkles. */
fun setSparkleStrength(strength: Float) {
rippleShader.sparkleStrength = strength
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index 59b4848..a523cf1 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -21,15 +21,14 @@
import android.provider.Settings
import android.util.Log
import androidx.annotation.OpenForTesting
-import com.android.internal.annotations.Keep
import com.android.systemui.plugins.ClockController
import com.android.systemui.plugins.ClockId
import com.android.systemui.plugins.ClockMetadata
import com.android.systemui.plugins.ClockProvider
import com.android.systemui.plugins.ClockProviderPlugin
+import com.android.systemui.plugins.ClockSettings
import com.android.systemui.plugins.PluginListener
import com.android.systemui.plugins.PluginManager
-import org.json.JSONObject
private val TAG = ClockRegistry::class.simpleName
private const val DEBUG = true
@@ -64,34 +63,54 @@
disconnectClocks(plugin)
}
- open var currentClockId: ClockId
+ open var settings: ClockSettings?
get() {
- return try {
+ try {
val json = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
)
if (json == null || json.isEmpty()) {
- return fallbackClockId
+ return null
}
- ClockSetting.deserialize(json).clockId
+ return ClockSettings.deserialize(json)
} catch (ex: Exception) {
- Log.e(TAG, "Failed to parse clock setting", ex)
- fallbackClockId
+ Log.e(TAG, "Failed to parse clock settings", ex)
+ return null
}
}
- set(value) {
+ protected set(value) {
try {
- val json = ClockSetting.serialize(ClockSetting(value, System.currentTimeMillis()))
+ val json = if (value != null) {
+ value._applied_timestamp = System.currentTimeMillis()
+ ClockSettings.serialize(value)
+ } else {
+ ""
+ }
+
Settings.Secure.putString(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json
)
} catch (ex: Exception) {
- Log.e(TAG, "Failed to set clock setting", ex)
+ Log.e(TAG, "Failed to set clock settings", ex)
}
}
+ private fun mutateSetting(mutator: (ClockSettings) -> Unit) {
+ val settings = this.settings ?: ClockSettings()
+ mutator(settings)
+ this.settings = settings
+ }
+
+ var currentClockId: ClockId
+ get() = settings?.clockId ?: fallbackClockId
+ set(value) { mutateSetting { it.clockId = value } }
+
+ var seedColor: Int?
+ get() = settings?.seedColor
+ set(value) { mutateSetting { it.seedColor = value } }
+
init {
connectClocks(defaultClockProvider)
if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) {
@@ -194,36 +213,16 @@
return createClock(DEFAULT_CLOCK_ID)!!
}
- private fun createClock(clockId: ClockId): ClockController? =
- availableClocks[clockId]?.provider?.createClock(clockId)
+ private fun createClock(clockId: ClockId): ClockController? {
+ val settings = this.settings ?: ClockSettings()
+ if (clockId != settings.clockId) {
+ settings.clockId = clockId
+ }
+ return availableClocks[clockId]?.provider?.createClock(settings)
+ }
private data class ClockInfo(
val metadata: ClockMetadata,
val provider: ClockProvider
)
-
- @Keep
- data class ClockSetting(
- val clockId: ClockId,
- val _applied_timestamp: Long?
- ) {
- companion object {
- private val KEY_CLOCK_ID = "clockId"
- private val KEY_TIMESTAMP = "_applied_timestamp"
-
- fun serialize(setting: ClockSetting): String {
- return JSONObject()
- .put(KEY_CLOCK_ID, setting.clockId)
- .put(KEY_TIMESTAMP, setting._applied_timestamp)
- .toString()
- }
-
- fun deserialize(jsonStr: String): ClockSetting {
- val json = JSONObject(jsonStr)
- return ClockSetting(
- json.getString(KEY_CLOCK_ID),
- if (!json.isNull(KEY_TIMESTAMP)) json.getLong(KEY_TIMESTAMP) else null)
- }
- }
- }
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index 7645dec..2a40f5c 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -29,6 +29,7 @@
import com.android.systemui.plugins.ClockEvents
import com.android.systemui.plugins.ClockFaceController
import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.ClockSettings
import com.android.systemui.plugins.log.LogBuffer
import java.io.PrintWriter
import java.util.Locale
@@ -46,6 +47,7 @@
ctx: Context,
private val layoutInflater: LayoutInflater,
private val resources: Resources,
+ private val settings: ClockSettings?,
) : ClockController {
override val smallClock: DefaultClockFaceController
override val largeClock: LargeClockFaceController
@@ -66,12 +68,14 @@
smallClock =
DefaultClockFaceController(
layoutInflater.inflate(R.layout.clock_default_small, parent, false)
- as AnimatableClockView
+ as AnimatableClockView,
+ settings?.seedColor
)
largeClock =
LargeClockFaceController(
layoutInflater.inflate(R.layout.clock_default_large, parent, false)
- as AnimatableClockView
+ as AnimatableClockView,
+ settings?.seedColor
)
clocks = listOf(smallClock.view, largeClock.view)
@@ -85,11 +89,13 @@
animations = DefaultClockAnimations(dozeFraction, foldFraction)
events.onColorPaletteChanged(resources)
events.onTimeZoneChanged(TimeZone.getDefault())
- events.onTimeTick()
+ smallClock.events.onTimeTick()
+ largeClock.events.onTimeTick()
}
open inner class DefaultClockFaceController(
override val view: AnimatableClockView,
+ val seedColor: Int?,
) : ClockFaceController {
// MAGENTA is a placeholder, and will be assigned correctly in initialize
@@ -104,11 +110,16 @@
}
init {
+ if (seedColor != null) {
+ currentColor = seedColor
+ }
view.setColors(currentColor, currentColor)
}
override val events =
object : ClockFaceEvents {
+ override fun onTimeTick() = view.refreshTime()
+
override fun onRegionDarknessChanged(isRegionDark: Boolean) {
this@DefaultClockFaceController.isRegionDark = isRegionDark
updateColor()
@@ -129,7 +140,9 @@
fun updateColor() {
val color =
- if (isRegionDark) {
+ if (seedColor != null) {
+ seedColor
+ } else if (isRegionDark) {
resources.getColor(android.R.color.system_accent1_100)
} else {
resources.getColor(android.R.color.system_accent2_600)
@@ -149,7 +162,8 @@
inner class LargeClockFaceController(
view: AnimatableClockView,
- ) : DefaultClockFaceController(view) {
+ seedColor: Int?,
+ ) : DefaultClockFaceController(view, seedColor) {
override fun recomputePadding(targetRegion: Rect?) {
// We center the view within the targetRegion instead of within the parent
// view by computing the difference and adding that to the padding.
@@ -169,8 +183,6 @@
}
inner class DefaultClockEvents : ClockEvents {
- override fun onTimeTick() = clocks.forEach { it.refreshTime() }
-
override fun onTimeFormatChanged(is24Hr: Boolean) =
clocks.forEach { it.refreshFormat(is24Hr) }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 4c0504b..0fd1b49 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -22,6 +22,7 @@
import com.android.systemui.plugins.ClockId
import com.android.systemui.plugins.ClockMetadata
import com.android.systemui.plugins.ClockProvider
+import com.android.systemui.plugins.ClockSettings
private val TAG = DefaultClockProvider::class.simpleName
const val DEFAULT_CLOCK_NAME = "Default Clock"
@@ -36,12 +37,12 @@
override fun getClocks(): List<ClockMetadata> =
listOf(ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME))
- override fun createClock(id: ClockId): ClockController {
- if (id != DEFAULT_CLOCK_ID) {
- throw IllegalArgumentException("$id is unsupported by $TAG")
+ override fun createClock(settings: ClockSettings): ClockController {
+ if (settings.clockId != DEFAULT_CLOCK_ID) {
+ throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
}
- return DefaultClockController(ctx, layoutInflater, resources)
+ return DefaultClockController(ctx, layoutInflater, resources, settings)
}
override fun getClockThumbnail(id: ClockId): Drawable? {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
index e4e9c46..c120876 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
@@ -181,6 +181,9 @@
/** Flag denoting whether the Wallpaper preview should use the full screen UI. */
const val FLAG_NAME_WALLPAPER_FULLSCREEN_PREVIEW = "wallpaper_fullscreen_preview"
+ /** Flag denoting whether the Monochromatic Theme is enabled. */
+ const val FLAG_NAME_MONOCHROMATIC_THEME = "is_monochromatic_theme_enabled"
+
object Columns {
/** String. Unique ID for the flag. */
const val NAME = "name"
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 a2a0709..4ef525a 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -17,11 +17,14 @@
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
+import com.android.internal.annotations.Keep
import com.android.systemui.plugins.annotations.ProvidesInterface
import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.statusbar.Weather
import java.io.PrintWriter
import java.util.Locale
import java.util.TimeZone
+import org.json.JSONObject
/** Identifies a clock design */
typealias ClockId = String
@@ -41,7 +44,13 @@
fun getClocks(): List<ClockMetadata>
/** Initializes and returns the target clock design */
- fun createClock(id: ClockId): ClockController
+ @Deprecated("Use overload with ClockSettings")
+ fun createClock(id: ClockId): ClockController {
+ return createClock(ClockSettings(id, null, null))
+ }
+
+ /** Initializes and returns the target clock design */
+ fun createClock(settings: ClockSettings): ClockController
/** A static thumbnail for rendering in some examples */
fun getClockThumbnail(id: ClockId): Drawable?
@@ -62,11 +71,16 @@
val animations: ClockAnimations
/** Initializes various rendering parameters. If never called, provides reasonable defaults. */
- fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
+ fun initialize(
+ resources: Resources,
+ dozeFraction: Float,
+ foldFraction: Float,
+ ) {
events.onColorPaletteChanged(resources)
animations.doze(dozeFraction)
animations.fold(foldFraction)
- events.onTimeTick()
+ smallClock.events.onTimeTick()
+ largeClock.events.onTimeTick()
}
/** Optional method for dumping debug information */
@@ -87,9 +101,6 @@
/** Events that should call when various rendering parameters change */
interface ClockEvents {
- /** Call every time tick */
- fun onTimeTick() {}
-
/** Call whenever timezone changes */
fun onTimeZoneChanged(timeZone: TimeZone) {}
@@ -101,6 +112,9 @@
/** Call whenever the color palette should update */
fun onColorPaletteChanged(resources: Resources) {}
+
+ /** Call whenever the weather data should update */
+ fun onWeatherDataChanged(data: Weather) {}
}
/** Methods which trigger various clock animations */
@@ -131,6 +145,13 @@
/** Events that have specific data about the related face */
interface ClockFaceEvents {
+ /** Call every time tick */
+ fun onTimeTick() {}
+
+ /** Expected interval between calls to onTimeTick. Can always reduce to PER_MINUTE in AOD. */
+ val tickRate: ClockTickRate
+ get() = ClockTickRate.PER_MINUTE
+
/** Region Darkness specific to the clock face */
fun onRegionDarknessChanged(isDark: Boolean) {}
@@ -150,8 +171,46 @@
fun onTargetRegionChanged(targetRegion: Rect?) {}
}
+/** Tick rates for clocks */
+enum class ClockTickRate(val value: Int) {
+ PER_MINUTE(2), // Update the clock once per minute.
+ PER_SECOND(1), // Update the clock once per second.
+ PER_FRAME(0), // Update the clock every second.
+}
+
/** Some data about a clock design */
data class ClockMetadata(
val clockId: ClockId,
val name: String,
)
+
+/** Structure for keeping clock-specific settings */
+@Keep
+data class ClockSettings(
+ var clockId: ClockId? = null,
+ var seedColor: Int? = null,
+ var _applied_timestamp: Long? = null,
+) {
+ companion object {
+ private val KEY_CLOCK_ID = "clockId"
+ private val KEY_SEED_COLOR = "seedColor"
+ private val KEY_TIMESTAMP = "_applied_timestamp"
+
+ fun serialize(setting: ClockSettings): String {
+ return JSONObject()
+ .put(KEY_CLOCK_ID, setting.clockId)
+ .put(KEY_SEED_COLOR, setting.seedColor)
+ .put(KEY_TIMESTAMP, setting._applied_timestamp)
+ .toString()
+ }
+
+ fun deserialize(jsonStr: String): ClockSettings {
+ val json = JSONObject(jsonStr)
+ return ClockSettings(
+ json.getString(KEY_CLOCK_ID),
+ if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null,
+ if (!json.isNull(KEY_TIMESTAMP)) json.getLong(KEY_TIMESTAMP) else null
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/Weather.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Weather.kt
new file mode 100644
index 0000000..85ec42d
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Weather.kt
@@ -0,0 +1,85 @@
+package com.android.systemui.statusbar
+
+import android.os.Bundle
+
+class Weather(val conditions: WeatherStateIcon, val temperature: Int, val isCelsius: Boolean) {
+ companion object {
+ private const val TAG = "Weather"
+ private const val WEATHER_STATE_ICON_KEY = "weather_state_icon_extra_key"
+ private const val TEMPERATURE_VALUE_KEY = "temperature_value_extra_key"
+ private const val TEMPERATURE_UNIT_KEY = "temperature_unit_extra_key"
+ private const val INVALID_TEMPERATURE = Int.MIN_VALUE
+
+ fun fromBundle(extras: Bundle): Weather? {
+ val icon =
+ WeatherStateIcon.fromInt(
+ extras.getInt(WEATHER_STATE_ICON_KEY, WeatherStateIcon.UNKNOWN_ICON.id)
+ )
+ if (icon == null || icon == WeatherStateIcon.UNKNOWN_ICON) {
+ return null
+ }
+ val temperature = extras.getInt(TEMPERATURE_VALUE_KEY, INVALID_TEMPERATURE)
+ if (temperature == INVALID_TEMPERATURE) {
+ return null
+ }
+ return Weather(icon, temperature, extras.getBoolean(TEMPERATURE_UNIT_KEY))
+ }
+ }
+
+ enum class WeatherStateIcon(val id: Int) {
+ UNKNOWN_ICON(0),
+
+ // Clear, day & night.
+ SUNNY(1),
+ CLEAR_NIGHT(2),
+
+ // Mostly clear, day & night.
+ MOSTLY_SUNNY(3),
+ MOSTLY_CLEAR_NIGHT(4),
+
+ // Partly cloudy, day & night.
+ PARTLY_CLOUDY(5),
+ PARTLY_CLOUDY_NIGHT(6),
+
+ // Mostly cloudy, day & night.
+ MOSTLY_CLOUDY_DAY(7),
+ MOSTLY_CLOUDY_NIGHT(8),
+ CLOUDY(9),
+ HAZE_FOG_DUST_SMOKE(10),
+ DRIZZLE(11),
+ HEAVY_RAIN(12),
+ SHOWERS_RAIN(13),
+
+ // Scattered showers, day & night.
+ SCATTERED_SHOWERS_DAY(14),
+ SCATTERED_SHOWERS_NIGHT(15),
+
+ // Isolated scattered thunderstorms, day & night.
+ ISOLATED_SCATTERED_TSTORMS_DAY(16),
+ ISOLATED_SCATTERED_TSTORMS_NIGHT(17),
+ STRONG_TSTORMS(18),
+ BLIZZARD(19),
+ BLOWING_SNOW(20),
+ FLURRIES(21),
+ HEAVY_SNOW(22),
+
+ // Scattered snow showers, day & night.
+ SCATTERED_SNOW_SHOWERS_DAY(23),
+ SCATTERED_SNOW_SHOWERS_NIGHT(24),
+ SNOW_SHOWERS_SNOW(25),
+ MIXED_RAIN_HAIL_RAIN_SLEET(26),
+ SLEET_HAIL(27),
+ TORNADO(28),
+ TROPICAL_STORM_HURRICANE(29),
+ WINDY_BREEZY(30),
+ WINTRY_MIX_RAIN_SNOW(31);
+
+ companion object {
+ fun fromInt(value: Int) = values().firstOrNull { it.id == value }
+ }
+ }
+
+ override fun toString(): String {
+ return "$conditions $temperature${if (isCelsius) "C" else "F"}"
+ }
+}
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
index 64ece47..ca4028a 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
@@ -105,6 +105,7 @@
android:id="@+id/key1"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key2"
androidprv:digit="1"
androidprv:textView="@+id/pinEntry" />
@@ -112,6 +113,7 @@
android:id="@+id/key2"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key3"
androidprv:digit="2"
androidprv:textView="@+id/pinEntry" />
@@ -119,6 +121,7 @@
android:id="@+id/key3"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key4"
androidprv:digit="3"
androidprv:textView="@+id/pinEntry" />
@@ -126,6 +129,7 @@
android:id="@+id/key4"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key5"
androidprv:digit="4"
androidprv:textView="@+id/pinEntry" />
@@ -133,6 +137,7 @@
android:id="@+id/key5"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key6"
androidprv:digit="5"
androidprv:textView="@+id/pinEntry" />
@@ -140,6 +145,7 @@
android:id="@+id/key6"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key7"
androidprv:digit="6"
androidprv:textView="@+id/pinEntry" />
@@ -147,13 +153,16 @@
android:id="@+id/key7"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key8"
androidprv:digit="7"
androidprv:textView="@+id/pinEntry" />
+
<com.android.keyguard.NumPadKey
android:id="@+id/key8"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key9"
androidprv:digit="8"
androidprv:textView="@+id/pinEntry" />
@@ -161,34 +170,33 @@
android:id="@+id/key9"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/delete_button"
androidprv:digit="9"
androidprv:textView="@+id/pinEntry" />
-
<com.android.keyguard.NumPadButton
android:id="@+id/delete_button"
+ style="@style/NumPadKey.Delete"
android:layout_width="0dp"
android:layout_height="0dp"
- style="@style/NumPadKey.Delete"
- android:contentDescription="@string/keyboardview_keycode_delete"
- />
+ android:accessibilityTraversalBefore="@id/key0"
+ android:contentDescription="@string/keyboardview_keycode_delete" />
<com.android.keyguard.NumPadKey
android:id="@+id/key0"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key_enter"
androidprv:digit="0"
androidprv:textView="@+id/pinEntry" />
<com.android.keyguard.NumPadButton
android:id="@+id/key_enter"
+ style="@style/NumPadKey.Enter"
android:layout_width="0dp"
android:layout_height="0dp"
- style="@style/NumPadKey.Enter"
- android:contentDescription="@string/keyboardview_keycode_enter"
- />
-
- </androidx.constraintlayout.widget.ConstraintLayout>
+ android:contentDescription="@string/keyboardview_keycode_enter" />
+</androidx.constraintlayout.widget.ConstraintLayout>
<include layout="@layout/keyguard_eca"
android:id="@+id/keyguard_selector_fade_container"
diff --git a/packages/SystemUI/res/drawable/clipboard_minimized_background.xml b/packages/SystemUI/res/drawable/clipboard_minimized_background.xml
new file mode 100644
index 0000000..a179c14
--- /dev/null
+++ b/packages/SystemUI/res/drawable/clipboard_minimized_background.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <solid android:color="?androidprv:attr/colorAccentSecondary"/>
+ <corners android:radius="10dp"/>
+</shape>
diff --git a/packages/SystemUI/res/drawable/ic_content_paste.xml b/packages/SystemUI/res/drawable/ic_content_paste.xml
new file mode 100644
index 0000000..8c8b81e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_content_paste.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M9,42Q7.7,42 6.85,41.15Q6,40.3 6,39V9Q6,7.7 6.85,6.85Q7.7,6 9,6H19.1Q19.45,4.25 20.825,3.125Q22.2,2 24,2Q25.8,2 27.175,3.125Q28.55,4.25 28.9,6H39Q40.3,6 41.15,6.85Q42,7.7 42,9V39Q42,40.3 41.15,41.15Q40.3,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V9Q39,9 39,9Q39,9 39,9H36V13.5H12V9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM24,9Q24.85,9 25.425,8.425Q26,7.85 26,7Q26,6.15 25.425,5.575Q24.85,5 24,5Q23.15,5 22.575,5.575Q22,6.15 22,7Q22,7.85 22.575,8.425Q23.15,9 24,9Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index eec3b11..9b01bd8 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -125,6 +125,45 @@
android:layout_width="@dimen/clipboard_preview_size"
android:layout_height="@dimen/clipboard_preview_size"/>
</FrameLayout>
+ <LinearLayout
+ android:id="@+id/minimized_preview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:elevation="7dp"
+ android:padding="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+ android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+ android:background="@drawable/clipboard_minimized_background">
+ <ImageView
+ android:src="@drawable/ic_content_paste"
+ android:tint="?attr/overlayButtonTextColor"
+ android:layout_width="24dp"
+ android:layout_height="24dp"/>
+ <ImageView
+ android:src="@*android:drawable/ic_chevron_end"
+ android:tint="?attr/overlayButtonTextColor"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:paddingEnd="-8dp"
+ android:paddingStart="-4dp"/>
+ </LinearLayout>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/clipboard_content_top"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:barrierDirection="top"
+ app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/clipboard_content_end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:barrierDirection="end"
+ app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
<FrameLayout
android:id="@+id/dismiss_button"
android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
@@ -132,10 +171,10 @@
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"
+ app:layout_constraintStart_toEndOf="@id/clipboard_content_end"
+ app:layout_constraintEnd_toEndOf="@id/clipboard_content_end"
+ app:layout_constraintTop_toTopOf="@id/clipboard_content_top"
+ app:layout_constraintBottom_toTopOf="@id/clipboard_content_top"
android:contentDescription="@string/clipboard_dismiss_description">
<ImageView
android:id="@+id/dismiss_image"
diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml
index ee3adba..aa211bf 100644
--- a/packages/SystemUI/res/layout/controls_with_favorites.xml
+++ b/packages/SystemUI/res/layout/controls_with_favorites.xml
@@ -20,7 +20,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
- android:layout_marginTop="@dimen/controls_top_margin"
android:layout_marginBottom="@dimen/controls_header_bottom_margin">
<!-- make sure the header stays centered in the layout by adding a spacer -->
@@ -78,6 +77,7 @@
android:layout_weight="1"
android:orientation="vertical"
android:clipChildren="true"
+ android:paddingHorizontal="16dp"
android:scrollbars="none">
<include layout="@layout/global_actions_controls_list_view" />
@@ -88,10 +88,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
- android:layout_marginLeft="@dimen/global_actions_side_margin"
- android:layout_marginRight="@dimen/global_actions_side_margin"
android:background="@drawable/controls_panel_background"
- android:padding="@dimen/global_actions_side_margin"
android:visibility="gone"
/>
</merge>
diff --git a/packages/SystemUI/res/layout/keyguard_user_switcher_item.xml b/packages/SystemUI/res/layout/keyguard_user_switcher_item.xml
index aaa372a..e39f1a9 100644
--- a/packages/SystemUI/res/layout/keyguard_user_switcher_item.xml
+++ b/packages/SystemUI/res/layout/keyguard_user_switcher_item.xml
@@ -18,6 +18,7 @@
<!-- LinearLayout -->
<com.android.systemui.statusbar.policy.KeyguardUserDetailItemView
+ android:id="@+id/user_item"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/layout/media_recommendation_view.xml b/packages/SystemUI/res/layout/media_recommendation_view.xml
index 101fad9..c54c4e4 100644
--- a/packages/SystemUI/res/layout/media_recommendation_view.xml
+++ b/packages/SystemUI/res/layout/media_recommendation_view.xml
@@ -47,7 +47,8 @@
android:singleLine="true"
android:textSize="12sp"
android:gravity="top"
- android:layout_gravity="bottom"/>
+ android:layout_gravity="bottom"
+ android:importantForAccessibility="no"/>
<!-- Album name -->
<TextView
@@ -61,5 +62,26 @@
android:singleLine="true"
android:textSize="11sp"
android:gravity="center_vertical"
- android:layout_gravity="bottom"/>
+ android:layout_gravity="bottom"
+ android:importantForAccessibility="no"/>
+
+ <!-- Seek Bar -->
+ <SeekBar
+ android:id="@+id/media_progress_bar"
+ android:layout_width="match_parent"
+ android:layout_height="12dp"
+ android:layout_gravity="bottom"
+ android:maxHeight="@dimen/qs_media_enabled_seekbar_height"
+ android:thumb="@android:color/transparent"
+ android:splitTrack="false"
+ android:clickable="false"
+ android:progressTint="?android:attr/textColorPrimary"
+ android:progressBackgroundTint="?android:attr/textColorTertiary"
+ android:paddingTop="5dp"
+ android:paddingBottom="5dp"
+ android:paddingStart="0dp"
+ android:paddingEnd="0dp"
+ android:layout_marginEnd="@dimen/qs_media_info_spacing"
+ android:layout_marginStart="@dimen/qs_media_info_spacing"
+ android:layout_marginBottom="@dimen/qs_media_info_spacing"/>
</merge>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/qs_user_detail_item.xml b/packages/SystemUI/res/layout/qs_user_detail_item.xml
index 7c86bc7..ad129e8 100644
--- a/packages/SystemUI/res/layout/qs_user_detail_item.xml
+++ b/packages/SystemUI/res/layout/qs_user_detail_item.xml
@@ -18,6 +18,7 @@
<!-- LinearLayout -->
<com.android.systemui.qs.tiles.UserDetailItemView
+ android:id="@+id/user_item"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index a748e29..7e9202c 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -149,6 +149,8 @@
app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintWidth_max="450dp"
+ app:layout_constraintHorizontal_bias="0"
>
<include layout="@layout/screenshot_work_profile_first_run" />
<include layout="@layout/screenshot_detection_notice" />
diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
index c181724..392d845 100644
--- a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
+++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
@@ -4,12 +4,16 @@
android:id="@+id/work_profile_first_run"
android:layout_height="wrap_content"
android:layout_width="match_parent"
+ android:paddingStart="16dp"
+ android:paddingEnd="4dp"
+ android:paddingVertical="16dp"
android:visibility="gone">
<ImageView
android:id="@+id/screenshot_message_icon"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:paddingEnd="4dp"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_marginEnd="12dp"
+ android:layout_gravity="center_vertical"
android:src="@drawable/ic_work_app_badge"/>
<TextView
@@ -17,7 +21,11 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
- android:layout_gravity="start"/>
+ android:layout_gravity="start|center_vertical"
+ android:textSize="18sp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:lineHeight="24sp"
+ />
<FrameLayout
android:id="@+id/message_dismiss_button"
@@ -25,9 +33,9 @@
android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
android:contentDescription="@string/screenshot_dismiss_work_profile">
<ImageView
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_margin="@dimen/overlay_dismiss_button_margin"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center"
android:src="@drawable/overlay_cancel"/>
</FrameLayout>
</LinearLayout>
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index fff2544..ac81dcc 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -64,4 +64,6 @@
<dimen name="qs_panel_padding_top">@dimen/qqs_layout_margin_top</dimen>
<dimen name="qs_panel_padding_top_combined_headers">@dimen/qs_panel_padding_top</dimen>
+
+ <dimen name="controls_padding_horizontal">16dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index db7fb48..4f24d83 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -51,9 +51,6 @@
<!-- Text size for user name in user switcher -->
<dimen name="kg_user_switcher_text_size">18sp</dimen>
- <dimen name="controls_header_bottom_margin">12dp</dimen>
- <dimen name="controls_top_margin">24dp</dimen>
-
<dimen name="global_actions_grid_item_layout_height">80dp</dimen>
<dimen name="qs_brightness_margin_bottom">16dp</dimen>
diff --git a/packages/SystemUI/res/values-sw720dp-land/dimens.xml b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
index 122806a..2b88e55 100644
--- a/packages/SystemUI/res/values-sw720dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
@@ -17,7 +17,6 @@
*/
-->
<resources>
- <dimen name="controls_padding_horizontal">205dp</dimen>
<dimen name="split_shade_notifications_scrim_margin_bottom">24dp</dimen>
<dimen name="notification_panel_margin_bottom">64dp</dimen>
diff --git a/packages/SystemUI/res/values-sw720dp/dimens.xml b/packages/SystemUI/res/values-sw720dp/dimens.xml
index 927059a..8f59df6 100644
--- a/packages/SystemUI/res/values-sw720dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp/dimens.xml
@@ -19,7 +19,7 @@
<!-- gap on either side of status bar notification icons -->
<dimen name="status_bar_icon_padding">1dp</dimen>
- <dimen name="controls_padding_horizontal">75dp</dimen>
+ <dimen name="controls_padding_horizontal">40dp</dimen>
<dimen name="large_screen_shade_header_height">56dp</dimen>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 2b0021b..d492e53c 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -43,12 +43,18 @@
<dimen name="navigation_edge_panel_height">268dp</dimen>
<!-- The threshold to drag to trigger the edge action -->
<dimen name="navigation_edge_action_drag_threshold">16dp</dimen>
+ <!-- The drag distance to consider evaluating gesture -->
+ <dimen name="navigation_edge_action_min_distance_to_start_animation">24dp</dimen>
<!-- The threshold to progress back animation for edge swipe -->
<dimen name="navigation_edge_action_progress_threshold">412dp</dimen>
<!-- The minimum display position of the arrow on the screen -->
<dimen name="navigation_edge_arrow_min_y">64dp</dimen>
<!-- The amount by which the arrow is shifted to avoid the finger-->
<dimen name="navigation_edge_finger_offset">64dp</dimen>
+ <!-- The threshold to dynamically activate the edge action -->
+ <dimen name="navigation_edge_action_reactivation_drag_threshold">32dp</dimen>
+ <!-- The threshold to dynamically deactivate the edge action -->
+ <dimen name="navigation_edge_action_deactivation_drag_threshold">32dp</dimen>
<!-- The thickness of the arrow -->
<dimen name="navigation_edge_arrow_thickness">4dp</dimen>
@@ -56,37 +62,61 @@
<dimen name="navigation_edge_minimum_x_delta_for_switch">32dp</dimen>
<!-- entry state -->
+ <item name="navigation_edge_entry_scale" format="float" type="dimen">0.98</item>
<dimen name="navigation_edge_entry_margin">4dp</dimen>
- <dimen name="navigation_edge_entry_background_width">8dp</dimen>
- <dimen name="navigation_edge_entry_background_height">60dp</dimen>
- <dimen name="navigation_edge_entry_edge_corners">30dp</dimen>
- <dimen name="navigation_edge_entry_far_corners">30dp</dimen>
- <dimen name="navigation_edge_entry_arrow_length">10dp</dimen>
- <dimen name="navigation_edge_entry_arrow_height">7dp</dimen>
+ <item name="navigation_edge_entry_background_alpha" format="float" type="dimen">1.0</item>
+ <dimen name="navigation_edge_entry_background_width">0dp</dimen>
+ <dimen name="navigation_edge_entry_background_height">48dp</dimen>
+ <dimen name="navigation_edge_entry_edge_corners">6dp</dimen>
+ <dimen name="navigation_edge_entry_far_corners">6dp</dimen>
+ <item name="navigation_edge_entry_arrow_alpha" format="float" type="dimen">0.0</item>
+ <dimen name="navigation_edge_entry_arrow_length">8.6dp</dimen>
+ <dimen name="navigation_edge_entry_arrow_height">5dp</dimen>
<!-- pre-threshold -->
<dimen name="navigation_edge_pre_threshold_margin">4dp</dimen>
- <dimen name="navigation_edge_pre_threshold_background_width">64dp</dimen>
- <dimen name="navigation_edge_pre_threshold_background_height">60dp</dimen>
- <dimen name="navigation_edge_pre_threshold_edge_corners">22dp</dimen>
- <dimen name="navigation_edge_pre_threshold_far_corners">26dp</dimen>
+ <item name="navigation_edge_pre_threshold_background_alpha" format="float" type="dimen">1.0
+ </item>
+ <item name="navigation_edge_pre_threshold_scale" format="float" type="dimen">0.98</item>
+ <dimen name="navigation_edge_pre_threshold_background_width">51dp</dimen>
+ <dimen name="navigation_edge_pre_threshold_background_height">46dp</dimen>
+ <dimen name="navigation_edge_pre_threshold_edge_corners">16dp</dimen>
+ <dimen name="navigation_edge_pre_threshold_far_corners">20dp</dimen>
+ <item name="navigation_edge_pre_threshold_arrow_alpha" format="float" type="dimen">1.0</item>
+ <dimen name="navigation_edge_pre_threshold_arrow_length">8dp</dimen>
+ <dimen name="navigation_edge_pre_threshold_arrow_height">5.6dp</dimen>
- <!-- post-threshold / active -->
+ <!-- active (post-threshold) -->
+ <item name="navigation_edge_active_scale" format="float" type="dimen">1.0</item>
<dimen name="navigation_edge_active_margin">14dp</dimen>
- <dimen name="navigation_edge_active_background_width">60dp</dimen>
- <dimen name="navigation_edge_active_background_height">60dp</dimen>
- <dimen name="navigation_edge_active_edge_corners">30dp</dimen>
- <dimen name="navigation_edge_active_far_corners">30dp</dimen>
- <dimen name="navigation_edge_active_arrow_length">8dp</dimen>
- <dimen name="navigation_edge_active_arrow_height">9dp</dimen>
+ <item name="navigation_edge_active_background_alpha" format="float" type="dimen">1.0</item>
+ <dimen name="navigation_edge_active_background_width">48dp</dimen>
+ <dimen name="navigation_edge_active_background_height">48dp</dimen>
+ <dimen name="navigation_edge_active_edge_corners">24dp</dimen>
+ <dimen name="navigation_edge_active_far_corners">24dp</dimen>
+ <item name="navigation_edge_active_arrow_alpha" format="float" type="dimen">1.0</item>
+ <dimen name="navigation_edge_active_arrow_length">6.4dp</dimen>
+ <dimen name="navigation_edge_active_arrow_height">7.2dp</dimen>
+ <!-- committed -->
+ <item name="navigation_edge_committed_scale" format="float" type="dimen">0.85</item>
+ <item name="navigation_edge_committed_alpha" format="float" type="dimen">0</item>
+
+ <!-- cancelled -->
+ <dimen name="navigation_edge_cancelled_background_width">0dp</dimen>
+
+ <item name="navigation_edge_stretch_scale" format="float" type="dimen">1.0</item>
<dimen name="navigation_edge_stretch_margin">18dp</dimen>
- <dimen name="navigation_edge_stretch_background_width">74dp</dimen>
- <dimen name="navigation_edge_stretch_background_height">60dp</dimen>
- <dimen name="navigation_edge_stretch_edge_corners">30dp</dimen>
- <dimen name="navigation_edge_stretch_far_corners">30dp</dimen>
- <dimen name="navigation_edge_stretched_arrow_length">7dp</dimen>
- <dimen name="navigation_edge_stretched_arrow_height">10dp</dimen>
+ <dimen name="navigation_edge_stretch_background_width">60dp</dimen>
+ <item name="navigation_edge_stretch_background_alpha" format="float" type="dimen">
+ @dimen/navigation_edge_entry_background_alpha
+ </item>
+ <dimen name="navigation_edge_stretch_background_height">48dp</dimen>
+ <dimen name="navigation_edge_stretch_edge_corners">24dp</dimen>
+ <dimen name="navigation_edge_stretch_far_corners">24dp</dimen>
+ <item name="navigation_edge_strech_arrow_alpha" format="float" type="dimen">1.0</item>
+ <dimen name="navigation_edge_stretched_arrow_length">5.6dp</dimen>
+ <dimen name="navigation_edge_stretched_arrow_height">8dp</dimen>
<dimen name="navigation_edge_cancelled_arrow_length">12dp</dimen>
<dimen name="navigation_edge_cancelled_arrow_height">0dp</dimen>
@@ -1115,7 +1145,7 @@
<!-- Home Controls -->
<dimen name="controls_header_menu_size">48dp</dimen>
- <dimen name="controls_header_bottom_margin">24dp</dimen>
+ <dimen name="controls_header_bottom_margin">16dp</dimen>
<dimen name="controls_header_app_icon_size">24dp</dimen>
<dimen name="controls_top_margin">48dp</dimen>
<dimen name="controls_padding_horizontal">0dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 3d2b2e6..227b00e 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -240,7 +240,7 @@
<!-- Content description for the right boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] -->
<string name="screenshot_right_boundary_pct">Right boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string>
<!-- Notification displayed when a screenshot is saved in a work profile. [CHAR LIMIT=NONE] -->
- <string name="screenshot_work_profile_notification">Work screenshots are saved in the <xliff:g id="app" example="Work Files">%1$s</xliff:g> app</string>
+ <string name="screenshot_work_profile_notification">Saved in <xliff:g id="app" example="Files">%1$s</xliff:g> in the work profile</string>
<!-- Default name referring to the app on the device that lets the user browse stored files. [CHAR LIMIT=NONE] -->
<string name="screenshot_default_files_app_name">Files</string>
<!-- A notice shown to the user to indicate that an app has detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index e56e5d5..00a0444 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -48,8 +48,7 @@
app:layout_constrainedWidth="true"
app:layout_constraintStart_toEndOf="@id/clock"
app:layout_constraintEnd_toStartOf="@id/barrier"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintBaseline_toBaselineOf="@id/clock"
app:layout_constraintHorizontal_bias="0"
/>
</Constraint>
diff --git a/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt b/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
index 38fa354..54ae84f9 100644
--- a/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
@@ -16,18 +16,20 @@
package com.android.keyguard
-import android.annotation.IntDef
import android.content.ContentResolver
import android.database.ContentObserver
import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_TIMEOUT
import android.net.Uri
import android.os.Handler
+import android.os.PowerManager
+import android.os.PowerManager.WAKE_REASON_UNFOLD_DEVICE
import android.os.UserHandle
import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL
import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO
import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ERRORS
import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT
import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS
import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_WAKE
import android.util.Log
import com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser
@@ -52,23 +54,26 @@
companion object {
const val TAG = "ActiveUnlockConfig"
-
- const val BIOMETRIC_TYPE_NONE = 0
- const val BIOMETRIC_TYPE_ANY_FACE = 1
- const val BIOMETRIC_TYPE_ANY_FINGERPRINT = 2
- const val BIOMETRIC_TYPE_UNDER_DISPLAY_FINGERPRINT = 3
}
- @Retention(AnnotationRetention.SOURCE)
- @IntDef(BIOMETRIC_TYPE_NONE, BIOMETRIC_TYPE_ANY_FACE, BIOMETRIC_TYPE_ANY_FINGERPRINT,
- BIOMETRIC_TYPE_UNDER_DISPLAY_FINGERPRINT)
- annotation class BiometricType
-
/**
* Indicates the origin for an active unlock request.
*/
- enum class ACTIVE_UNLOCK_REQUEST_ORIGIN {
- WAKE, UNLOCK_INTENT, BIOMETRIC_FAIL, ASSISTANT
+ enum class ActiveUnlockRequestOrigin {
+ WAKE,
+ UNLOCK_INTENT,
+ BIOMETRIC_FAIL,
+ ASSISTANT,
+ }
+
+ /**
+ * Biometric type options.
+ */
+ enum class BiometricType(val intValue: Int) {
+ NONE(0),
+ ANY_FACE(1),
+ ANY_FINGERPRINT(2),
+ UNDER_DISPLAY_FINGERPRINT(3),
}
var keyguardUpdateMonitor: KeyguardUpdateMonitor? = null
@@ -76,9 +81,10 @@
private var requestActiveUnlockOnUnlockIntent = false
private var requestActiveUnlockOnBioFail = false
- private var faceErrorsToTriggerBiometricFailOn = mutableSetOf(FACE_ERROR_TIMEOUT)
+ private var faceErrorsToTriggerBiometricFailOn = mutableSetOf<Int>()
private var faceAcquireInfoToTriggerBiometricFailOn = mutableSetOf<Int>()
- private var onUnlockIntentWhenBiometricEnrolled = mutableSetOf<Int>(BIOMETRIC_TYPE_NONE)
+ private var onUnlockIntentWhenBiometricEnrolled = mutableSetOf<Int>()
+ private var wakeupsConsideredUnlockIntents = mutableSetOf<Int>()
private val settingsObserver = object : ContentObserver(handler) {
private val wakeUri = secureSettings.getUriFor(ACTIVE_UNLOCK_ON_WAKE)
@@ -89,16 +95,19 @@
secureSettings.getUriFor(ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO)
private val unlockIntentWhenBiometricEnrolledUri =
secureSettings.getUriFor(ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED)
+ private val wakeupsConsideredUnlockIntentsUri =
+ secureSettings.getUriFor(ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS)
fun register() {
registerUri(
listOf(
- wakeUri,
- unlockIntentUri,
- bioFailUri,
- faceErrorsUri,
- faceAcquireInfoUri,
- unlockIntentWhenBiometricEnrolledUri
+ wakeUri,
+ unlockIntentUri,
+ bioFailUri,
+ faceErrorsUri,
+ faceAcquireInfoUri,
+ unlockIntentWhenBiometricEnrolledUri,
+ wakeupsConsideredUnlockIntentsUri,
)
)
@@ -153,7 +162,7 @@
secureSettings.getStringForUser(ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO,
getCurrentUser()),
faceAcquireInfoToTriggerBiometricFailOn,
- setOf<Int>())
+ emptySet())
}
if (selfChange || uris.contains(unlockIntentWhenBiometricEnrolledUri)) {
@@ -162,7 +171,16 @@
ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
getCurrentUser()),
onUnlockIntentWhenBiometricEnrolled,
- setOf(BIOMETRIC_TYPE_NONE))
+ setOf(BiometricType.NONE.intValue))
+ }
+
+ if (selfChange || uris.contains(wakeupsConsideredUnlockIntentsUri)) {
+ processStringArray(
+ secureSettings.getStringForUser(
+ ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+ getCurrentUser()),
+ wakeupsConsideredUnlockIntents,
+ setOf(WAKE_REASON_UNFOLD_DEVICE))
}
}
@@ -181,10 +199,12 @@
out.clear()
stringSetting?.let {
for (code: String in stringSetting.split("|")) {
- try {
- out.add(code.toInt())
- } catch (e: NumberFormatException) {
- Log.e(TAG, "Passed an invalid setting=$code")
+ if (code.isNotEmpty()) {
+ try {
+ out.add(code.toInt())
+ } catch (e: NumberFormatException) {
+ Log.e(TAG, "Passed an invalid setting=$code")
+ }
}
}
} ?: out.addAll(default)
@@ -221,22 +241,30 @@
}
/**
+ * Whether the PowerManager wake reason is considered an unlock intent and should use origin
+ * [ActiveUnlockRequestOrigin.UNLOCK_INTENT] instead of [ActiveUnlockRequestOrigin.WAKE].
+ */
+ fun isWakeupConsideredUnlockIntent(pmWakeReason: Int): Boolean {
+ return wakeupsConsideredUnlockIntents.contains(pmWakeReason)
+ }
+
+ /**
* Whether to trigger active unlock based on where the request is coming from and
* the current settings.
*/
- fun shouldAllowActiveUnlockFromOrigin(requestOrigin: ACTIVE_UNLOCK_REQUEST_ORIGIN): Boolean {
+ fun shouldAllowActiveUnlockFromOrigin(requestOrigin: ActiveUnlockRequestOrigin): Boolean {
return when (requestOrigin) {
- ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE -> requestActiveUnlockOnWakeup
+ ActiveUnlockRequestOrigin.WAKE -> requestActiveUnlockOnWakeup
- ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT ->
+ ActiveUnlockRequestOrigin.UNLOCK_INTENT ->
requestActiveUnlockOnUnlockIntent || requestActiveUnlockOnWakeup ||
(shouldRequestActiveUnlockOnUnlockIntentFromBiometricEnrollment())
- ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL ->
+ ActiveUnlockRequestOrigin.BIOMETRIC_FAIL ->
requestActiveUnlockOnBioFail || requestActiveUnlockOnUnlockIntent ||
requestActiveUnlockOnWakeup
- ACTIVE_UNLOCK_REQUEST_ORIGIN.ASSISTANT -> isActiveUnlockEnabled()
+ ActiveUnlockRequestOrigin.ASSISTANT -> isActiveUnlockEnabled()
}
}
@@ -252,18 +280,18 @@
val udfpsEnrolled = it.isUdfpsEnrolled
if (!anyFaceEnrolled && !anyFingerprintEnrolled) {
- return onUnlockIntentWhenBiometricEnrolled.contains(BIOMETRIC_TYPE_NONE)
+ return onUnlockIntentWhenBiometricEnrolled.contains(BiometricType.NONE.intValue)
}
if (!anyFaceEnrolled && anyFingerprintEnrolled) {
return onUnlockIntentWhenBiometricEnrolled.contains(
- BIOMETRIC_TYPE_ANY_FINGERPRINT) ||
+ BiometricType.ANY_FINGERPRINT.intValue) ||
(udfpsEnrolled && onUnlockIntentWhenBiometricEnrolled.contains(
- BIOMETRIC_TYPE_UNDER_DISPLAY_FINGERPRINT))
+ BiometricType.UNDER_DISPLAY_FINGERPRINT.intValue))
}
if (!anyFingerprintEnrolled && anyFaceEnrolled) {
- return onUnlockIntentWhenBiometricEnrolled.contains(BIOMETRIC_TYPE_ANY_FACE)
+ return onUnlockIntentWhenBiometricEnrolled.contains(BiometricType.ANY_FACE.intValue)
}
}
@@ -275,11 +303,15 @@
pw.println(" requestActiveUnlockOnWakeup=$requestActiveUnlockOnWakeup")
pw.println(" requestActiveUnlockOnUnlockIntent=$requestActiveUnlockOnUnlockIntent")
pw.println(" requestActiveUnlockOnBioFail=$requestActiveUnlockOnBioFail")
- pw.println(" requestActiveUnlockOnUnlockIntentWhenBiometricEnrolled=" +
- "$onUnlockIntentWhenBiometricEnrolled")
+ pw.println(" requestActiveUnlockOnUnlockIntentWhenBiometricEnrolled=${
+ onUnlockIntentWhenBiometricEnrolled.map { BiometricType.values()[it] }
+ }")
pw.println(" requestActiveUnlockOnFaceError=$faceErrorsToTriggerBiometricFailOn")
pw.println(" requestActiveUnlockOnFaceAcquireInfo=" +
"$faceAcquireInfoToTriggerBiometricFailOn")
+ pw.println(" activeUnlockWakeupsConsideredUnlockIntents=${
+ wakeupsConsideredUnlockIntents.map { PowerManager.wakeReasonToString(it) }
+ }")
pw.println("Current state:")
keyguardUpdateMonitor?.let {
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 3a940e9..ea079a9 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -25,6 +25,7 @@
import android.util.TypedValue
import android.view.View
import android.widget.FrameLayout
+import android.view.ViewTreeObserver
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
@@ -42,12 +43,16 @@
import com.android.systemui.log.dagger.KeyguardSmallClockLog
import com.android.systemui.log.dagger.KeyguardLargeClockLog
import com.android.systemui.plugins.ClockController
+import com.android.systemui.plugins.ClockFaceController
+import com.android.systemui.plugins.ClockTickRate
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel.DEBUG
import com.android.systemui.shared.regionsampling.RegionSampler
+import com.android.systemui.statusbar.Weather
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
@@ -72,7 +77,7 @@
private val configurationController: ConfigurationController,
@Main private val resources: Resources,
private val context: Context,
- @Main private val mainExecutor: Executor,
+ @Main private val mainExecutor: DelayableExecutor,
@Background private val bgExecutor: Executor,
@KeyguardSmallClockLog private val smallLogBuffer: LogBuffer?,
@KeyguardLargeClockLog private val largeLogBuffer: LogBuffer?,
@@ -92,8 +97,11 @@
if (regionSamplingEnabled) {
clock?.smallClock?.view?.addOnLayoutChangeListener(mLayoutChangedListener)
clock?.largeClock?.view?.addOnLayoutChangeListener(mLayoutChangedListener)
+ } else {
+ updateColors()
}
updateFontSizes()
+ updateTimeListeners()
}
}
@@ -208,6 +216,10 @@
}
var regionSampler: RegionSampler? = null
+ var smallTimeListener: TimeListener? = null
+ var largeTimeListener: TimeListener? = null
+ val shouldTimeListenerRun: Boolean
+ get() = isKeyguardVisible && dozeAmount < DOZE_TICKRATE_THRESHOLD
private var smallClockIsDark = true
private var largeClockIsDark = true
@@ -246,6 +258,9 @@
clock?.animations?.doze(if (isDozing) 1f else 0f)
}
}
+
+ smallTimeListener?.update(shouldTimeListenerRun)
+ largeTimeListener?.update(shouldTimeListenerRun)
}
override fun onTimeFormatChanged(timeFormat: String) {
@@ -259,6 +274,12 @@
override fun onUserSwitchComplete(userId: Int) {
clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context))
}
+
+ override fun onWeatherDataChanged(data: Weather?) {
+ if (data != null) {
+ clock?.events?.onWeatherDataChanged(data)
+ }
+ }
}
fun registerListeners(parent: View) {
@@ -285,6 +306,8 @@
}
}
}
+ smallTimeListener?.update(shouldTimeListenerRun)
+ largeTimeListener?.update(shouldTimeListenerRun)
}
fun unregisterListeners() {
@@ -299,6 +322,25 @@
batteryController.removeCallback(batteryCallback)
keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
regionSampler?.stopRegionSampler()
+ smallTimeListener?.stop()
+ largeTimeListener?.stop()
+ }
+
+ private fun updateTimeListeners() {
+ smallTimeListener?.stop()
+ largeTimeListener?.stop()
+
+ smallTimeListener = null
+ largeTimeListener = null
+
+ clock?.smallClock?.let {
+ smallTimeListener = TimeListener(it, mainExecutor)
+ smallTimeListener?.update(shouldTimeListenerRun)
+ }
+ clock?.largeClock?.let {
+ largeTimeListener = TimeListener(it, mainExecutor)
+ largeTimeListener?.update(shouldTimeListenerRun)
+ }
}
private fun updateFontSizes() {
@@ -308,12 +350,18 @@
resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat())
}
+ private fun handleDoze(doze: Float) {
+ dozeAmount = doze
+ clock?.animations?.doze(dozeAmount)
+ smallTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD)
+ largeTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD)
+ }
+
@VisibleForTesting
internal fun listenForDozeAmount(scope: CoroutineScope): Job {
return scope.launch {
keyguardInteractor.dozeAmount.collect {
- dozeAmount = it
- clock?.animations?.doze(dozeAmount)
+ handleDoze(it)
}
}
}
@@ -322,8 +370,7 @@
internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
return scope.launch {
keyguardTransitionInteractor.dozeAmountTransition.collect {
- dozeAmount = it.value
- clock?.animations?.doze(dozeAmount)
+ handleDoze(it.value)
}
}
}
@@ -338,8 +385,7 @@
keyguardTransitionInteractor.anyStateToAodTransition.filter {
it.transitionState == TransitionState.FINISHED
}.collect {
- dozeAmount = 1f
- clock?.animations?.doze(dozeAmount)
+ handleDoze(1f)
}
}
}
@@ -359,7 +405,54 @@
}
}
+ class TimeListener(val clockFace: ClockFaceController, val executor: DelayableExecutor) {
+ val predrawListener = ViewTreeObserver.OnPreDrawListener {
+ clockFace.events.onTimeTick()
+ true
+ }
+
+ val secondsRunnable = object : Runnable {
+ override fun run() {
+ if (!isRunning) {
+ return
+ }
+
+ executor.executeDelayed(this, 990)
+ clockFace.events.onTimeTick()
+ }
+ }
+
+ var isRunning: Boolean = false
+ private set
+
+ fun start() {
+ if (isRunning) {
+ return
+ }
+
+ isRunning = true
+ when (clockFace.events.tickRate) {
+ ClockTickRate.PER_MINUTE -> {/* Handled by KeyguardClockSwitchController */}
+ ClockTickRate.PER_SECOND -> executor.execute(secondsRunnable)
+ ClockTickRate.PER_FRAME -> {
+ clockFace.view.viewTreeObserver.addOnPreDrawListener(predrawListener)
+ clockFace.view.invalidate()
+ }
+ }
+ }
+
+ fun stop() {
+ if (!isRunning) { return }
+
+ isRunning = false
+ clockFace.view.viewTreeObserver.removeOnPreDrawListener(predrawListener)
+ }
+
+ fun update(shouldRun: Boolean) = if (shouldRun) start() else stop()
+ }
+
companion object {
private val TAG = ClockEventController::class.simpleName!!
+ private val DOZE_TICKRATE_THRESHOLD = 0.99f
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 8684019..a148aa1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -91,6 +91,7 @@
private ViewGroup mStatusArea;
// If the SMARTSPACE flag is set, keyguard_slice_view is replaced by the following views.
+ private ViewGroup mDateWeatherView;
private View mWeatherView;
private View mSmartspaceView;
@@ -201,7 +202,7 @@
// TODO(b/261757708): add content observer for the Settings toggle and add/remove
// weather according to the Settings.
if (mSmartspaceController.isDateWeatherDecoupled()) {
- addWeatherView(viewIndex);
+ addDateWeatherView(viewIndex);
viewIndex += 1;
}
@@ -239,6 +240,14 @@
void onLocaleListChanged() {
if (mSmartspaceController.isEnabled()) {
+ if (mSmartspaceController.isDateWeatherDecoupled()) {
+ mDateWeatherView.removeView(mWeatherView);
+ int index = mStatusArea.indexOfChild(mDateWeatherView);
+ if (index >= 0) {
+ mStatusArea.removeView(mDateWeatherView);
+ addDateWeatherView(index);
+ }
+ }
int index = mStatusArea.indexOfChild(mSmartspaceView);
if (index >= 0) {
mStatusArea.removeView(mSmartspaceView);
@@ -247,16 +256,28 @@
}
}
- private void addWeatherView(int index) {
- mWeatherView = mSmartspaceController.buildAndConnectWeatherView(mView);
+ private void addDateWeatherView(int index) {
+ mDateWeatherView = (ViewGroup) mSmartspaceController.buildAndConnectDateView(mView);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
MATCH_PARENT, WRAP_CONTENT);
- mStatusArea.addView(mWeatherView, index, lp);
+ mStatusArea.addView(mDateWeatherView, index, lp);
int startPadding = getContext().getResources().getDimensionPixelSize(
R.dimen.below_clock_padding_start);
int endPadding = getContext().getResources().getDimensionPixelSize(
R.dimen.below_clock_padding_end);
- mWeatherView.setPaddingRelative(startPadding, 0, endPadding, 0);
+ mDateWeatherView.setPaddingRelative(startPadding, 0, endPadding, 0);
+
+ addWeatherView();
+ }
+
+ private void addWeatherView() {
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ WRAP_CONTENT, WRAP_CONTENT);
+ mWeatherView = mSmartspaceController.buildAndConnectWeatherView(mView);
+ // Place weather right after the date, before the extras
+ final int index = mDateWeatherView.getChildCount() == 0 ? 0 : 1;
+ mDateWeatherView.addView(mWeatherView, index, lp);
+ mWeatherView.setPaddingRelative(0, 0, 4, 0);
}
private void addSmartspaceView(int index) {
@@ -323,7 +344,8 @@
}
ClockController clock = getClock();
if (clock != null) {
- clock.getEvents().onTimeTick();
+ clock.getSmallClock().getEvents().onTimeTick();
+ clock.getLargeClock().getEvents().onTimeTick();
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
index 08e9cf6..2a389b6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
+import android.view.MotionEvent;
import android.widget.FrameLayout;
/**
@@ -33,7 +34,7 @@
public class KeyguardHostView extends FrameLayout {
protected ViewMediatorCallback mViewMediatorCallback;
-
+ private boolean mIsInteractable;
public KeyguardHostView(Context context) {
this(context, null);
@@ -54,4 +55,24 @@
public void setViewMediatorCallback(ViewMediatorCallback viewMediatorCallback) {
mViewMediatorCallback = viewMediatorCallback;
}
+
+ /** Set true if the view can be interacted with */
+ public void setInteractable(boolean isInteractable) {
+ mIsInteractable = isInteractable;
+ }
+
+ /**
+ * Make sure to disallow touches while transitioning the bouncer, otherwise
+ * it can remain interactable even when barely visible.
+ */
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ return !mIsInteractable;
+ }
+
+ /** True to consume any events that are sent to it */
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ return true;
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
index ea84438..6139403 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
@@ -527,4 +527,9 @@
mKeyguardSecurityContainerController.updateKeyguardPosition(x);
}
}
+
+ /** Set true if the view can be interacted with */
+ public void setInteractable(boolean isInteractable) {
+ mView.setInteractable(isInteractable);
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 57bfe54..9fcacce 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -237,7 +237,7 @@
}
if (mUpdateMonitor.isFaceEnrolled()) {
mUpdateMonitor.requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
"swipeUpOnBouncer");
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 54886c3..cd5b7c1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -108,7 +108,6 @@
import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteException;
-import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
@@ -153,6 +152,7 @@
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.Weather;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.telephony.TelephonyListenerManager;
@@ -1311,7 +1311,8 @@
}
public boolean getUserHasTrust(int userId) {
- return !isTrustDisabled() && mUserHasTrust.get(userId);
+ return !isTrustDisabled() && mUserHasTrust.get(userId)
+ && isUnlockingWithTrustAgentAllowed();
}
/**
@@ -1319,12 +1320,19 @@
*/
public boolean getUserUnlockedWithBiometric(int userId) {
BiometricAuthenticated fingerprint = mUserFingerprintAuthenticated.get(userId);
- BiometricAuthenticated face = mUserFaceAuthenticated.get(userId);
boolean fingerprintAllowed = fingerprint != null && fingerprint.mAuthenticated
&& isUnlockingWithBiometricAllowed(fingerprint.mIsStrongBiometric);
- boolean faceAllowed = face != null && face.mAuthenticated
+ return fingerprintAllowed || getUserUnlockedWithFace(userId);
+ }
+
+
+ /**
+ * Returns whether the user is unlocked with face.
+ */
+ public boolean getUserUnlockedWithFace(int userId) {
+ BiometricAuthenticated face = mUserFaceAuthenticated.get(userId);
+ return face != null && face.mAuthenticated
&& isUnlockingWithBiometricAllowed(face.mIsStrongBiometric);
- return fingerprintAllowed || faceAllowed;
}
/**
@@ -1399,6 +1407,10 @@
return mUserTrustIsUsuallyManaged.get(userId);
}
+ private boolean isUnlockingWithTrustAgentAllowed() {
+ return isUnlockingWithBiometricAllowed(true);
+ }
+
public boolean isUnlockingWithBiometricAllowed(boolean isStrongBiometric) {
// StrongAuthTracker#isUnlockingWithBiometricAllowed includes
// STRONG_AUTH_REQUIRED_AFTER_LOCKOUT which is the same as mFingerprintLockedOutPermanent;
@@ -1534,7 +1546,7 @@
FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED);
if (mAssistantVisible) {
requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.ASSISTANT,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.ASSISTANT,
"assistant",
false);
}
@@ -1664,7 +1676,7 @@
@Override
public void onAuthenticationFailed() {
requestActiveUnlockDismissKeyguard(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
"fingerprintFailure");
handleFingerprintAuthFailed();
}
@@ -1733,7 +1745,7 @@
: mPrimaryBouncerFullyShown ? "bouncer"
: "udfpsFpDown";
requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
"faceFailure-" + reason);
handleFaceAuthFailed();
@@ -1760,7 +1772,7 @@
if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceError(errMsgId)) {
requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
"faceError-" + errMsgId);
}
}
@@ -1772,7 +1784,7 @@
if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceAcquireInfo(
acquireInfo)) {
requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
"faceAcquireInfo-" + acquireInfo);
}
}
@@ -1912,8 +1924,11 @@
FACE_AUTH_UPDATED_STARTED_WAKING_UP.setExtraInfo(pmWakeReason);
updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
FACE_AUTH_UPDATED_STARTED_WAKING_UP);
- requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp - "
- + PowerManager.wakeReasonToString(pmWakeReason));
+ requestActiveUnlock(
+ mActiveUnlockConfig.isWakeupConsideredUnlockIntent(pmWakeReason)
+ ? ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT
+ : ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE,
+ "wakingUp - " + PowerManager.wakeReasonToString(pmWakeReason));
} else {
mLogger.logSkipUpdateFaceListeningOnWakeup(pmWakeReason);
}
@@ -2477,7 +2492,7 @@
mAuthInterruptActive = active;
updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
FACE_AUTH_TRIGGERED_ON_REACH_GESTURE_ON_AOD);
- requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "onReach");
+ requestActiveUnlock(ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE, "onReach");
}
/**
@@ -2547,7 +2562,7 @@
* Attempts to trigger active unlock from trust agent.
*/
private void requestActiveUnlock(
- @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+ @NonNull ActiveUnlockConfig.ActiveUnlockRequestOrigin requestOrigin,
String reason,
boolean dismissKeyguard
) {
@@ -2558,7 +2573,7 @@
final boolean allowRequest =
mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(requestOrigin);
- if (requestOrigin == ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE
+ if (requestOrigin == ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE
&& !allowRequest && mActiveUnlockConfig.isActiveUnlockEnabled()) {
// instead of requesting the active unlock, initiate the unlock
initiateActiveUnlock(reason);
@@ -2577,7 +2592,7 @@
* Only dismisses the keyguard under certain conditions.
*/
public void requestActiveUnlock(
- @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+ @NonNull ActiveUnlockConfig.ActiveUnlockRequestOrigin requestOrigin,
String extraReason
) {
final boolean canFaceBypass = isFaceEnrolled() && mKeyguardBypassController != null
@@ -2594,7 +2609,7 @@
* Attempts to trigger active unlock from trust agent with a request to dismiss the keyguard.
*/
public void requestActiveUnlockDismissKeyguard(
- @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+ @NonNull ActiveUnlockConfig.ActiveUnlockRequestOrigin requestOrigin,
String extraReason
) {
requestActiveUnlock(
@@ -2611,7 +2626,7 @@
updateFaceListeningState(BIOMETRIC_ACTION_START,
FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN);
requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
"udfpsBouncer");
}
}
@@ -3217,6 +3232,24 @@
}
/**
+ * @param data the weather data (temp, conditions, unit) for weather clock to use
+ */
+ public void sendWeatherData(Weather data) {
+ mHandler.post(()-> {
+ handleWeatherDataUpdate(data); });
+ }
+
+ private void handleWeatherDataUpdate(Weather data) {
+ Assert.isMainThread();
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+ if (cb != null) {
+ cb.onWeatherDataChanged(data);
+ }
+ }
+ }
+
+ /**
* Handle {@link #MSG_BATTERY_UPDATE}
*/
private void handleBatteryUpdate(BatteryStatus status) {
@@ -3398,7 +3431,7 @@
if (wasPrimaryBouncerFullyShown != mPrimaryBouncerFullyShown) {
if (mPrimaryBouncerFullyShown) {
requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
"bouncerFullyShown");
}
for (int i = 0; i < mCallbacks.size(); i++) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index e6b9ac8..4a7dd24 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -24,6 +24,7 @@
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.Weather;
import java.util.TimeZone;
@@ -58,6 +59,11 @@
public void onTimeFormatChanged(String timeFormat) { }
/**
+ * Called when receive new weather data.
+ */
+ public void onWeatherDataChanged(Weather data) { }
+
+ /**
* Called when the carrier PLMN or SPN changes.
*/
public void onRefreshCarrierInfo() { }
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 201a1d9..c414c08 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -372,7 +372,7 @@
}
fun logUserRequestedUnlock(
- requestOrigin: ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN,
+ requestOrigin: ActiveUnlockConfig.ActiveUnlockRequestOrigin,
reason: String?,
dismissKeyguard: Boolean
) {
diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
index 3e0fa45..54939fd 100644
--- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
@@ -35,6 +35,7 @@
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.settingslib.Utils
import com.android.systemui.animation.Interpolators
+import com.android.systemui.log.ScreenDecorationsLogger
import com.android.systemui.plugins.statusbar.StatusBarStateController
import java.util.concurrent.Executor
@@ -47,7 +48,8 @@
pos: Int,
val statusBarStateController: StatusBarStateController,
val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- val mainExecutor: Executor
+ val mainExecutor: Executor,
+ val logger: ScreenDecorationsLogger,
) : ScreenDecorations.DisplayCutoutView(context, pos) {
private var showScanningAnim = false
private val rimPaint = Paint()
@@ -55,6 +57,7 @@
private var rimAnimator: AnimatorSet? = null
private val rimRect = RectF()
private var cameraProtectionColor = Color.BLACK
+
var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context,
R.attr.wallpaperTextColorAccent)
private var cameraProtectionAnimator: ValueAnimator? = null
@@ -175,15 +178,22 @@
}
if (showScanningAnim) {
// Make sure that our measured height encompasses the extra space for the animation
- mTotalBounds.union(mBoundingRect)
+ mTotalBounds.set(mBoundingRect)
mTotalBounds.union(
rimRect.left.toInt(),
rimRect.top.toInt(),
rimRect.right.toInt(),
rimRect.bottom.toInt())
- setMeasuredDimension(
- resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
- resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0))
+ val measuredWidth = resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0)
+ val measuredHeight = resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)
+ logger.boundingRect(rimRect, "onMeasure: Face scanning animation")
+ logger.boundingRect(mBoundingRect, "onMeasure: Display cutout view bounding rect")
+ logger.boundingRect(mTotalBounds, "onMeasure: TotalBounds")
+ logger.onMeasureDimensions(widthMeasureSpec,
+ heightMeasureSpec,
+ measuredWidth,
+ measuredHeight)
+ setMeasuredDimension(measuredWidth, measuredHeight)
} else {
setMeasuredDimension(
resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 71f98fa..fb65588 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -64,6 +64,7 @@
import com.android.internal.util.Preconditions;
import com.android.settingslib.Utils;
+import com.android.systemui.biometrics.AuthController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.decor.CutoutDecorProviderFactory;
@@ -75,6 +76,7 @@
import com.android.systemui.decor.PrivacyDotDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerResDelegate;
+import com.android.systemui.log.ScreenDecorationsLogger;
import com.android.systemui.qs.SettingObserver;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
@@ -120,6 +122,9 @@
R.id.display_cutout_right,
R.id.display_cutout_bottom
};
+ private final ScreenDecorationsLogger mLogger;
+
+ private final AuthController mAuthController;
private DisplayTracker mDisplayTracker;
@VisibleForTesting
@@ -153,6 +158,7 @@
private WindowManager mWindowManager;
private int mRotation;
private SettingObserver mColorInversionSetting;
+ @Nullable
private DelayableExecutor mExecutor;
private Handler mHandler;
boolean mPendingConfigChange;
@@ -172,6 +178,7 @@
DisplayCutoutView overlay = (DisplayCutoutView) getOverlayView(
mFaceScanningViewId);
if (overlay != null) {
+ mLogger.cameraProtectionBoundsForScanningOverlay(bounds);
overlay.setProtection(protectionPath, bounds);
overlay.enableShowProtection(true);
updateOverlayWindowVisibilityIfViewExists(
@@ -184,6 +191,7 @@
}
if (mScreenDecorHwcLayer != null) {
+ mLogger.hwcLayerCameraProtectionBounds(bounds);
mScreenDecorHwcLayer.setProtection(protectionPath, bounds);
mScreenDecorHwcLayer.enableShowProtection(true);
return;
@@ -197,11 +205,12 @@
}
++setProtectionCnt;
final DisplayCutoutView dcv = (DisplayCutoutView) view;
+ mLogger.dcvCameraBounds(id, bounds);
dcv.setProtection(protectionPath, bounds);
dcv.enableShowProtection(true);
}
if (setProtectionCnt == 0) {
- Log.e(TAG, "CutoutView not initialized showCameraProtection");
+ mLogger.cutoutViewNotInitialized();
}
}
@@ -307,7 +316,9 @@
PrivacyDotViewController dotViewController,
ThreadFactory threadFactory,
PrivacyDotDecorProviderFactory dotFactory,
- FaceScanningProviderFactory faceScanningFactory) {
+ FaceScanningProviderFactory faceScanningFactory,
+ ScreenDecorationsLogger logger,
+ AuthController authController) {
mContext = context;
mMainExecutor = mainExecutor;
mSecureSettings = secureSettings;
@@ -319,8 +330,23 @@
mDotFactory = dotFactory;
mFaceScanningFactory = faceScanningFactory;
mFaceScanningViewId = com.android.systemui.R.id.face_scanning_anim;
+ mLogger = logger;
+ mAuthController = authController;
}
+
+ private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
+ @Override
+ public void onFaceSensorLocationChanged() {
+ mLogger.onSensorLocationChanged();
+ if (mExecutor != null) {
+ mExecutor.execute(
+ () -> updateOverlayProviderViews(
+ new Integer[]{mFaceScanningViewId}));
+ }
+ }
+ };
+
@Override
public void start() {
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
@@ -331,6 +357,7 @@
mExecutor = mThreadFactory.buildDelayableExecutorOnHandler(mHandler);
mExecutor.execute(this::startOnScreenDecorationsThread);
mDotViewController.setUiExecutor(mExecutor);
+ mAuthController.addCallback(mAuthControllerCallback);
}
private boolean isPrivacyDotEnabled() {
@@ -1306,7 +1333,7 @@
if (showProtection) {
// Make sure that our measured height encompasses the protection
- mTotalBounds.union(mBoundingRect);
+ mTotalBounds.set(mBoundingRect);
mTotalBounds.union((int) protectionRect.left, (int) protectionRect.top,
(int) protectionRect.right, (int) protectionRect.bottom);
setMeasuredDimension(
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 191ac76..b62217f 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -40,6 +40,7 @@
import android.util.TimingsTraceLog;
import android.view.SurfaceControl;
import android.view.ThreadedRenderer;
+import android.view.View;
import com.android.internal.protolog.common.ProtoLog;
import com.android.systemui.dagger.GlobalRootComponent;
@@ -114,6 +115,11 @@
// the theme set there.
setTheme(R.style.Theme_SystemUI);
+ View.setTraceLayoutSteps(
+ SystemProperties.getBoolean("persist.debug.trace_layouts", false));
+ View.setTracedRequestLayoutClassClass(
+ SystemProperties.get("persist.debug.trace_request_layout_class", null));
+
if (Process.myUserHandle().equals(UserHandle.SYSTEM)) {
IntentFilter bootCompletedFilter = new
IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
index 436f9df..1f6f6d9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
@@ -46,6 +46,8 @@
private var isDeviceFolded: Boolean = false
private val isSideFps: Boolean
+ private val isReverseDefaultRotation =
+ context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
private val screenSizeFoldProvider: ScreenSizeFoldProvider = ScreenSizeFoldProvider(context)
var iconLayoutParamSize: Pair<Int, Int> = Pair(1, 1)
set(value) {
@@ -76,7 +78,7 @@
isSideFps = sideFps
val displayInfo = DisplayInfo()
context.display?.getDisplayInfo(displayInfo)
- if (isSideFps && displayInfo.rotation == Surface.ROTATION_180) {
+ if (isSideFps && getRotationFromDefault(displayInfo.rotation) == Surface.ROTATION_180) {
iconView.rotation = 180f
}
screenSizeFoldProvider.registerCallback(this, context.mainExecutor)
@@ -86,7 +88,7 @@
private fun updateIconSideFps(@BiometricState lastState: Int, @BiometricState newState: Int) {
val displayInfo = DisplayInfo()
context.display?.getDisplayInfo(displayInfo)
- val rotation = displayInfo.rotation
+ val rotation = getRotationFromDefault(displayInfo.rotation)
val iconAnimation = getSideFpsAnimationForTransition(rotation)
val iconViewOverlayAnimation =
getSideFpsOverlayAnimationForTransition(lastState, newState, rotation) ?: return
@@ -104,7 +106,7 @@
iconView.frame = 0
iconViewOverlay.frame = 0
- if (shouldAnimateIconViewForTransition(lastState, newState)) {
+ if (shouldAnimateSfpsIconViewForTransition(lastState, newState)) {
iconView.playAnimation()
}
@@ -169,6 +171,18 @@
STATE_HELP,
STATE_ERROR -> true
STATE_AUTHENTICATING_ANIMATING_IN,
+ STATE_AUTHENTICATING -> oldState == STATE_ERROR || oldState == STATE_HELP
+ STATE_AUTHENTICATED -> true
+ else -> false
+ }
+
+ private fun shouldAnimateSfpsIconViewForTransition(
+ @BiometricState oldState: Int,
+ @BiometricState newState: Int
+ ) = when (newState) {
+ STATE_HELP,
+ STATE_ERROR -> true
+ STATE_AUTHENTICATING_ANIMATING_IN,
STATE_AUTHENTICATING ->
oldState == STATE_ERROR || oldState == STATE_HELP || oldState == STATE_IDLE
STATE_AUTHENTICATED -> true
@@ -217,6 +231,9 @@
return if (id != null) return id else null
}
+ private fun getRotationFromDefault(rotation: Int): Int =
+ if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
+
@RawRes
private fun getSideFpsAnimationForTransition(rotation: Int): Int = when (rotation) {
Surface.ROTATION_90 -> if (isDeviceFolded) {
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
index fb0c0a6..5ca36ab 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
@@ -72,7 +72,7 @@
height = WindowManager.LayoutParams.MATCH_PARENT
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
format = PixelFormat.TRANSLUCENT
- type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
+ type = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
fitInsetsTypes = 0 // Ignore insets from all system bars
title = "Wired Charging Animation"
flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
index 1c26841..82bb723 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
@@ -21,6 +21,7 @@
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN;
+import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
@@ -35,6 +36,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
import javax.inject.Inject;
import javax.inject.Provider;
@@ -57,6 +59,7 @@
private final Provider<ClipboardOverlayController> mOverlayProvider;
private final ClipboardToast mClipboardToast;
private final ClipboardManager mClipboardManager;
+ private final FeatureFlags mFeatureFlags;
private final UiEventLogger mUiEventLogger;
private ClipboardOverlay mClipboardOverlay;
@@ -65,11 +68,13 @@
Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
ClipboardToast clipboardToast,
ClipboardManager clipboardManager,
+ FeatureFlags featureFlags,
UiEventLogger uiEventLogger) {
mContext = context;
mOverlayProvider = clipboardOverlayControllerProvider;
mClipboardToast = clipboardToast;
mClipboardManager = clipboardManager;
+ mFeatureFlags = featureFlags;
mUiEventLogger = uiEventLogger;
}
@@ -107,7 +112,11 @@
} else {
mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
}
- mClipboardOverlay.setClipData(clipData, clipSource);
+ if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
+ mClipboardOverlay.setClipData(clipData, clipSource);
+ } else {
+ mClipboardOverlay.setClipDataLegacy(clipData, clipSource);
+ }
mClipboardOverlay.setOnSessionCompleteListener(() -> {
// Session is complete, free memory until it's needed again.
mClipboardOverlay = null;
@@ -150,6 +159,8 @@
}
interface ClipboardOverlay {
+ void setClipDataLegacy(ClipData clipData, String clipSource);
+
void setClipData(ClipData clipData, String clipSource);
void setOnSessionCompleteListener(Runnable runnable);
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt
new file mode 100644
index 0000000..c7aaf09
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.clipboardoverlay
+
+import android.content.ClipData
+import android.content.ClipDescription.EXTRA_IS_SENSITIVE
+import android.content.Context
+import android.graphics.Bitmap
+import android.text.TextUtils
+import android.util.Log
+import android.util.Size
+import com.android.systemui.R
+import java.io.IOException
+
+data class ClipboardModel(
+ val clipData: ClipData?,
+ val source: String,
+ val type: Type = Type.OTHER,
+ val item: ClipData.Item? = null,
+ val isSensitive: Boolean = false,
+ val isRemote: Boolean = false,
+) {
+ private var _bitmap: Bitmap? = null
+
+ fun dataMatches(other: ClipboardModel?): Boolean {
+ if (other == null) {
+ return false
+ }
+ return source == other.source &&
+ type == other.type &&
+ item?.text == other.item?.text &&
+ item?.uri == other.item?.uri &&
+ isSensitive == other.isSensitive
+ }
+
+ fun loadThumbnail(context: Context): Bitmap? {
+ if (_bitmap == null && type == Type.IMAGE && item?.uri != null) {
+ try {
+ val size = context.resources.getDimensionPixelSize(R.dimen.overlay_x_scale)
+ _bitmap =
+ context.contentResolver.loadThumbnail(item.uri, Size(size, size * 4), null)
+ } catch (e: IOException) {
+ Log.e(TAG, "Thumbnail loading failed!", e)
+ }
+ }
+ return _bitmap
+ }
+
+ internal companion object {
+ private val TAG: String = "ClipboardModel"
+
+ @JvmStatic
+ fun fromClipData(
+ context: Context,
+ utils: ClipboardOverlayUtils,
+ clipData: ClipData?,
+ source: String
+ ): ClipboardModel {
+ if (clipData == null || clipData.itemCount == 0) {
+ return ClipboardModel(clipData, source)
+ }
+ val sensitive = clipData.description?.extras?.getBoolean(EXTRA_IS_SENSITIVE) ?: false
+ val item = clipData.getItemAt(0)!!
+ val type = getType(context, item)
+ val remote = utils.isRemoteCopy(context, clipData, source)
+ return ClipboardModel(clipData, source, type, item, sensitive, remote)
+ }
+
+ private fun getType(context: Context, item: ClipData.Item): Type {
+ return if (!TextUtils.isEmpty(item.text)) {
+ Type.TEXT
+ } else if (
+ item.uri != null &&
+ context.contentResolver.getType(item.uri)?.startsWith("image") == true
+ ) {
+ Type.IMAGE
+ } else {
+ Type.OTHER
+ }
+ }
+ }
+
+ enum class Type {
+ TEXT,
+ IMAGE,
+ OTHER
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index 8c8ee8a..b41f308 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.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;
@@ -31,10 +30,9 @@
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 com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
import static com.android.systemui.flags.Flags.CLIPBOARD_REMOTE_BEHAVIOR;
-import static java.util.Objects.requireNonNull;
-
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.RemoteAction;
@@ -47,7 +45,6 @@
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
-import android.hardware.display.DisplayManager;
import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.Looper;
@@ -55,14 +52,15 @@
import android.text.TextUtils;
import android.util.Log;
import android.util.Size;
-import android.view.Display;
import android.view.InputEvent;
import android.view.InputEventReceiver;
import android.view.InputMonitor;
import android.view.MotionEvent;
+import android.view.WindowInsets;
import androidx.annotation.NonNull;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -71,7 +69,6 @@
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.screenshot.TimeoutHandler;
-import com.android.systemui.settings.DisplayTracker;
import java.io.IOException;
import java.util.Optional;
@@ -95,8 +92,6 @@
private final Context mContext;
private final ClipboardLogger mClipboardLogger;
private final BroadcastDispatcher mBroadcastDispatcher;
- private final DisplayManager mDisplayManager;
- private final DisplayTracker mDisplayTracker;
private final ClipboardOverlayWindow mWindow;
private final TimeoutHandler mTimeoutHandler;
private final ClipboardOverlayUtils mClipboardUtils;
@@ -122,6 +117,9 @@
private Runnable mOnUiUpdate;
+ private boolean mIsMinimized;
+ private ClipboardModel mClipboardModel;
+
private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks =
new ClipboardOverlayView.ClipboardOverlayCallbacks() {
@Override
@@ -175,6 +173,13 @@
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
animateOut();
}
+
+ @Override
+ public void onMinimizedViewTapped() {
+ if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
+ animateFromMinimized();
+ }
+ }
};
@Inject
@@ -187,33 +192,28 @@
FeatureFlags featureFlags,
ClipboardOverlayUtils clipboardUtils,
@Background Executor bgExecutor,
- UiEventLogger uiEventLogger,
- DisplayTracker displayTracker) {
+ UiEventLogger uiEventLogger) {
+ mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
- mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
- mDisplayTracker = displayTracker;
- final Context displayContext = context.createDisplayContext(getDefaultDisplay());
- mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null);
mClipboardLogger = new ClipboardLogger(uiEventLogger);
mView = clipboardOverlayView;
mWindow = clipboardOverlayWindow;
- mWindow.init(mView::setInsets, () -> {
+ mWindow.init(this::onInsetsChanged, () -> {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
hideImmediate();
});
+ mFeatureFlags = featureFlags;
mTimeoutHandler = timeoutHandler;
mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
- mFeatureFlags = featureFlags;
mClipboardUtils = clipboardUtils;
mBgExecutor = bgExecutor;
mView.setCallbacks(mClipboardCallbacks);
-
mWindow.withWindowAttached(() -> {
mWindow.setContentView(mView);
mView.setInsets(mWindow.getWindowInsets(),
@@ -258,8 +258,135 @@
broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
}
+ @VisibleForTesting
+ void onInsetsChanged(WindowInsets insets, int orientation) {
+ mView.setInsets(insets, orientation);
+ if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
+ if (shouldShowMinimized(insets) && !mIsMinimized) {
+ mIsMinimized = true;
+ mView.setMinimized(true);
+ }
+ }
+ }
+
@Override // ClipboardListener.ClipboardOverlay
- public void setClipData(ClipData clipData, String clipSource) {
+ public void setClipData(ClipData data, String source) {
+ ClipboardModel model = ClipboardModel.fromClipData(mContext, mClipboardUtils, data, source);
+ if (mExitAnimator != null && mExitAnimator.isRunning()) {
+ mExitAnimator.cancel();
+ }
+ boolean shouldAnimate = !model.dataMatches(mClipboardModel);
+ mClipboardModel = model;
+ mClipboardLogger.setClipSource(mClipboardModel.getSource());
+ if (shouldAnimate) {
+ reset();
+ mClipboardLogger.setClipSource(mClipboardModel.getSource());
+ if (shouldShowMinimized(mWindow.getWindowInsets())) {
+ mIsMinimized = true;
+ mView.setMinimized(true);
+ } else {
+ setExpandedView();
+ }
+ animateIn();
+ mView.announceForAccessibility(getAccessibilityAnnouncement(mClipboardModel.getType()));
+ } else if (!mIsMinimized) {
+ setExpandedView();
+ }
+ if (mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR) && mClipboardModel.isRemote()) {
+ mTimeoutHandler.cancelTimeout();
+ mOnUiUpdate = null;
+ } else {
+ mOnUiUpdate = mTimeoutHandler::resetTimeout;
+ mOnUiUpdate.run();
+ }
+ }
+
+ private void setExpandedView() {
+ final ClipboardModel model = mClipboardModel;
+ mView.setMinimized(false);
+ switch (model.getType()) {
+ case TEXT:
+ if ((mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR) && model.isRemote())
+ || DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
+ if (model.getItem().getTextLinks() != null) {
+ classifyText(model);
+ }
+ }
+ if (model.isSensitive()) {
+ mView.showTextPreview(mContext.getString(R.string.clipboard_asterisks), true);
+ } else {
+ mView.showTextPreview(model.getItem().getText(), false);
+ }
+ mView.setEditAccessibilityAction(true);
+ mOnPreviewTapped = this::editText;
+ break;
+ case IMAGE:
+ if (model.isSensitive() || model.loadThumbnail(mContext) != null) {
+ mView.showImagePreview(
+ model.isSensitive() ? null : model.loadThumbnail(mContext));
+ mView.setEditAccessibilityAction(true);
+ mOnPreviewTapped = () -> editImage(model.getItem().getUri());
+ } else {
+ // image loading failed
+ mView.showDefaultTextPreview();
+ }
+ break;
+ case OTHER:
+ mView.showDefaultTextPreview();
+ break;
+ }
+ if (mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR)) {
+ if (!model.isRemote()) {
+ maybeShowRemoteCopy(model.getClipData());
+ }
+ } else {
+ maybeShowRemoteCopy(model.getClipData());
+ }
+ if (model.getType() != ClipboardModel.Type.OTHER) {
+ mOnShareTapped = () -> shareContent(model.getClipData());
+ mView.showShareChip();
+ }
+ }
+
+ private boolean shouldShowMinimized(WindowInsets insets) {
+ return insets.getInsets(WindowInsets.Type.ime()).bottom > 0;
+ }
+
+ private void animateFromMinimized() {
+ mIsMinimized = false;
+ setExpandedView();
+ animateIn();
+ }
+
+ private String getAccessibilityAnnouncement(ClipboardModel.Type type) {
+ if (type == ClipboardModel.Type.TEXT) {
+ return mContext.getString(R.string.clipboard_text_copied);
+ } else if (type == ClipboardModel.Type.IMAGE) {
+ return mContext.getString(R.string.clipboard_image_copied);
+ } else {
+ return mContext.getString(R.string.clipboard_content_copied);
+ }
+ }
+
+ private void classifyText(ClipboardModel model) {
+ mBgExecutor.execute(() -> {
+ Optional<RemoteAction> remoteAction =
+ mClipboardUtils.getAction(model.getItem(), model.getSource());
+ if (model.equals(mClipboardModel)) {
+ remoteAction.ifPresent(action -> {
+ mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_ACTION_SHOWN);
+ mView.setActionChip(action, () -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
+ animateOut();
+ });
+ });
+ }
+ });
+ }
+
+ @Override // ClipboardListener.ClipboardOverlay
+ public void setClipDataLegacy(ClipData clipData, String clipSource) {
if (mExitAnimator != null && mExitAnimator.isRunning()) {
mExitAnimator.cancel();
}
@@ -516,10 +643,6 @@
mClipboardLogger.reset();
}
- private Display getDefaultDisplay() {
- return mDisplayManager.getDisplay(mDisplayTracker.getDefaultDisplayId());
- }
-
static class ClipboardLogger {
private final UiEventLogger mUiEventLogger;
private String mClipSource;
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
index 2d33157..c9e01ce 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
@@ -18,8 +18,6 @@
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;
@@ -77,6 +75,8 @@
void onShareButtonTapped();
void onPreviewTapped();
+
+ void onMinimizedViewTapped();
}
private static final String TAG = "ClipboardView";
@@ -92,6 +92,7 @@
private ImageView mImagePreview;
private TextView mTextPreview;
private TextView mHiddenPreview;
+ private LinearLayout mMinimizedPreview;
private View mPreviewBorder;
private OverlayActionChip mEditChip;
private OverlayActionChip mShareChip;
@@ -117,18 +118,18 @@
@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));
+ mActionContainerBackground = requireViewById(R.id.actions_container_background);
+ mActionContainer = requireViewById(R.id.actions);
+ mClipboardPreview = requireViewById(R.id.clipboard_preview);
+ mPreviewBorder = requireViewById(R.id.preview_border);
+ mImagePreview = requireViewById(R.id.image_preview);
+ mTextPreview = requireViewById(R.id.text_preview);
+ mHiddenPreview = requireViewById(R.id.hidden_preview);
+ mMinimizedPreview = requireViewById(R.id.minimized_preview);
+ mEditChip = requireViewById(R.id.edit_chip);
+ mShareChip = requireViewById(R.id.share_chip);
+ mRemoteCopyChip = requireViewById(R.id.remote_copy_chip);
+ mDismissButton = requireViewById(R.id.dismiss_button);
mEditChip.setAlpha(1);
mShareChip.setAlpha(1);
@@ -163,6 +164,7 @@
mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped());
mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped());
mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped());
+ mMinimizedPreview.setOnClickListener(v -> clipboardCallbacks.onMinimizedViewTapped());
}
void setEditAccessibilityAction(boolean editable) {
@@ -177,12 +179,28 @@
}
}
+ void setMinimized(boolean minimized) {
+ if (minimized) {
+ mMinimizedPreview.setVisibility(View.VISIBLE);
+ mClipboardPreview.setVisibility(View.GONE);
+ mPreviewBorder.setVisibility(View.GONE);
+ mActionContainer.setVisibility(View.GONE);
+ mActionContainerBackground.setVisibility(View.GONE);
+ } else {
+ mMinimizedPreview.setVisibility(View.GONE);
+ mClipboardPreview.setVisibility(View.VISIBLE);
+ mPreviewBorder.setVisibility(View.VISIBLE);
+ mActionContainer.setVisibility(View.VISIBLE);
+ }
+ }
+
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();
@@ -204,6 +222,12 @@
(int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
touchRegion.op(tmpRect, Region.Op.UNION);
+ mMinimizedPreview.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);
@@ -298,6 +322,8 @@
scaleAnim.setDuration(333);
scaleAnim.addUpdateListener(animation -> {
float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
+ mMinimizedPreview.setScaleX(previewScale);
+ mMinimizedPreview.setScaleY(previewScale);
mClipboardPreview.setScaleX(previewScale);
mClipboardPreview.setScaleY(previewScale);
mPreviewBorder.setScaleX(previewScale);
@@ -319,12 +345,14 @@
alphaAnim.setDuration(283);
alphaAnim.addUpdateListener(animation -> {
float alpha = animation.getAnimatedFraction();
+ mMinimizedPreview.setAlpha(alpha);
mClipboardPreview.setAlpha(alpha);
mPreviewBorder.setAlpha(alpha);
mDismissButton.setAlpha(alpha);
mActionContainer.setAlpha(alpha);
});
+ mMinimizedPreview.setAlpha(0);
mActionContainer.setAlpha(0);
mPreviewBorder.setAlpha(0);
mClipboardPreview.setAlpha(0);
@@ -356,6 +384,8 @@
scaleAnim.setDuration(250);
scaleAnim.addUpdateListener(animation -> {
float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
+ mMinimizedPreview.setScaleX(previewScale);
+ mMinimizedPreview.setScaleY(previewScale);
mClipboardPreview.setScaleX(previewScale);
mClipboardPreview.setScaleY(previewScale);
mPreviewBorder.setScaleX(previewScale);
@@ -377,6 +407,7 @@
alphaAnim.setDuration(166);
alphaAnim.addUpdateListener(animation -> {
float alpha = 1 - animation.getAnimatedFraction();
+ mMinimizedPreview.setAlpha(alpha);
mClipboardPreview.setAlpha(alpha);
mPreviewBorder.setAlpha(alpha);
mDismissButton.setAlpha(alpha);
@@ -399,6 +430,7 @@
mTextPreview.setVisibility(View.GONE);
mImagePreview.setVisibility(View.GONE);
mHiddenPreview.setVisibility(View.GONE);
+ mMinimizedPreview.setVisibility(View.GONE);
v.setVisibility(View.VISIBLE);
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
index dbe301d..860149d 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
@@ -28,12 +28,13 @@
import android.content.pm.ServiceInfo
import android.os.UserHandle
import android.service.controls.ControlsProviderService
+import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import com.android.settingslib.applications.DefaultAppInfo
import com.android.systemui.R
import java.util.Objects
-class ControlsServiceInfo(
+open class ControlsServiceInfo(
private val context: Context,
val serviceInfo: ServiceInfo
) : DefaultAppInfo(
@@ -64,7 +65,7 @@
* [R.array.config_controlsPreferredPackages] can declare activities for use as a panel.
*/
var panelActivity: ComponentName? = null
- private set
+ protected set
private var resolved: Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index 1cbfe01..278ee70 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -121,16 +121,13 @@
userChanging = false
}
- private val userTrackerCallback = object : UserTracker.Callback {
- override fun onUserChanged(newUser: Int, userContext: Context) {
- userChanging = true
- val newUserHandle = UserHandle.of(newUser)
- if (currentUser == newUserHandle) {
- userChanging = false
- return
- }
- setValuesForUser(newUserHandle)
+ override fun changeUser(newUser: UserHandle) {
+ userChanging = true
+ if (currentUser == newUser) {
+ userChanging = false
+ return
}
+ setValuesForUser(newUser)
}
@VisibleForTesting
@@ -231,7 +228,6 @@
dumpManager.registerDumpable(javaClass.name, this)
resetFavorites()
userChanging = false
- userTracker.addCallback(userTrackerCallback, executor)
context.registerReceiver(
restoreFinishedReceiver,
IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
@@ -243,7 +239,6 @@
}
fun destroy() {
- userTracker.removeCallback(userTrackerCallback)
context.unregisterReceiver(restoreFinishedReceiver)
listingController.removeCallback(listingCallback)
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/StartControlsStartableModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/StartControlsStartableModule.kt
new file mode 100644
index 0000000..3f20c26
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/StartControlsStartableModule.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.controls.dagger
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.controls.start.ControlsStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+abstract class StartControlsStartableModule {
+ @Binds
+ @IntoMap
+ @ClassKey(ControlsStartable::class)
+ abstract fun bindFeature(impl: ControlsStartable): CoreStartable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt
new file mode 100644
index 0000000..9d99253
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.controls.start
+
+import android.content.Context
+import android.content.res.Resources
+import android.os.UserHandle
+import com.android.systemui.CoreStartable
+import com.android.systemui.R
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.ui.SelectedItem
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.UserTracker
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * Started with SystemUI to perform early operations for device controls subsystem (only if enabled)
+ *
+ * In particular, it will perform the following:
+ * * If there is no preferred selection for provider and at least one of the preferred packages
+ * provides a panel, it will select the first one that does.
+ * * If the preferred selection provides a panel, it will bind to that service (to reduce latency on
+ * displaying the panel).
+ *
+ * It will also perform those operations on user change.
+ */
+@SysUISingleton
+class ControlsStartable
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ @Background private val executor: Executor,
+ private val controlsComponent: ControlsComponent,
+ private val userTracker: UserTracker
+) : CoreStartable {
+
+ // These two controllers can only be accessed after `start` method once we've checked if the
+ // feature is enabled
+ private val controlsController: ControlsController
+ get() = controlsComponent.getControlsController().get()
+
+ private val controlsListingController: ControlsListingController
+ get() = controlsComponent.getControlsListingController().get()
+
+ private val userTrackerCallback =
+ object : UserTracker.Callback {
+ override fun onUserChanged(newUser: Int, userContext: Context) {
+ controlsController.changeUser(UserHandle.of(newUser))
+ startForUser()
+ }
+ }
+
+ override fun start() {
+ if (!controlsComponent.isEnabled()) {
+ // Controls is disabled, we don't need this anymore
+ return
+ }
+ startForUser()
+ userTracker.addCallback(userTrackerCallback, executor)
+ }
+
+ private fun startForUser() {
+ selectDefaultPanelIfNecessary()
+ bindToPanel()
+ }
+
+ private fun selectDefaultPanelIfNecessary() {
+ val currentSelection = controlsController.getPreferredSelection()
+ if (currentSelection == SelectedItem.EMPTY_SELECTION) {
+ val availableServices = controlsListingController.getCurrentServices()
+ val panels = availableServices.filter { it.panelActivity != null }
+ resources
+ .getStringArray(R.array.config_controlsPreferredPackages)
+ // Looking for the first element in the string array such that there is one package
+ // that has a panel. It will return null if there are no packages in the array,
+ // or if no packages in the array have a panel associated with it.
+ .firstNotNullOfOrNull { name ->
+ panels.firstOrNull { it.componentName.packageName == name }
+ }
+ ?.let { info ->
+ controlsController.setPreferredSelection(
+ SelectedItem.PanelItem(info.loadLabel(), info.componentName)
+ )
+ }
+ }
+ }
+
+ private fun bindToPanel() {
+ val currentSelection = controlsController.getPreferredSelection()
+ val panels =
+ controlsListingController.getCurrentServices().filter { it.panelActivity != null }
+ if (
+ currentSelection is SelectedItem.PanelItem &&
+ panels.firstOrNull { it.componentName == currentSelection.componentName } != null
+ ) {
+ controlsController.bindComponentForPanel(currentSelection.componentName)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 9e71bef..58f4835 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -28,6 +28,7 @@
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
+import android.os.Trace
import android.service.controls.Control
import android.service.controls.ControlsProviderService
import android.util.Log
@@ -224,6 +225,7 @@
activityContext: Context
) {
Log.d(ControlsUiController.TAG, "show()")
+ Trace.instant(Trace.TRACE_TAG_APP, "ControlsUiControllerImpl#show")
this.parent = parent
this.onDismiss = onDismiss
this.activityContext = activityContext
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
index f5764c2..3b6ab20 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
@@ -27,6 +27,7 @@
import android.graphics.Color
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RoundRectShape
+import android.os.Trace
import com.android.systemui.R
import com.android.systemui.util.boundsOnScreen
import com.android.wm.shell.TaskView
@@ -84,6 +85,7 @@
options,
taskView.boundsOnScreen
)
+ Trace.instant(Trace.TRACE_TAG_APP, "PanelTaskViewController - startActivity")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 2dfcf70..cb7c765 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -26,6 +26,7 @@
import com.android.systemui.accessibility.WindowMagnification
import com.android.systemui.biometrics.AuthController
import com.android.systemui.clipboardoverlay.ClipboardListener
+import com.android.systemui.controls.dagger.StartControlsStartableModule
import com.android.systemui.dagger.qualifiers.PerUser
import com.android.systemui.dreams.DreamMonitor
import com.android.systemui.globalactions.GlobalActionsComponent
@@ -65,7 +66,10 @@
/**
* Collection of {@link CoreStartable}s that should be run on AOSP.
*/
-@Module(includes = [MultiUserUtilsModule::class])
+@Module(includes = [
+ MultiUserUtilsModule::class,
+ StartControlsStartableModule::class
+])
abstract class SystemUICoreStartableModule {
/** Inject into AuthController. */
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 9ad7b8c..6274a26 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -218,6 +218,10 @@
abstract BcSmartspaceConfigPlugin optionalBcSmartspaceConfigPlugin();
@BindsOptionalOf
+ @Named(SmartspaceModule.DATE_SMARTSPACE_DATA_PLUGIN)
+ abstract BcSmartspaceDataPlugin optionalDateSmartspaceConfigPlugin();
+
+ @BindsOptionalOf
@Named(SmartspaceModule.WEATHER_SMARTSPACE_DATA_PLUGIN)
abstract BcSmartspaceDataPlugin optionalWeatherSmartspaceConfigPlugin();
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index 976afd4..88c0c50 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -34,6 +34,7 @@
import com.android.systemui.biometrics.AuthController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.ScreenDecorationsLogger
import com.android.systemui.plugins.statusbar.StatusBarStateController
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -45,6 +46,7 @@
private val statusBarStateController: StatusBarStateController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@Main private val mainExecutor: Executor,
+ private val logger: ScreenDecorationsLogger,
) : DecorProviderFactory() {
private val display = context.display
private val displayInfo = DisplayInfo()
@@ -82,7 +84,8 @@
authController,
statusBarStateController,
keyguardUpdateMonitor,
- mainExecutor
+ mainExecutor,
+ logger,
)
)
}
@@ -104,7 +107,8 @@
private val authController: AuthController,
private val statusBarStateController: StatusBarStateController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- private val mainExecutor: Executor
+ private val mainExecutor: Executor,
+ private val logger: ScreenDecorationsLogger,
) : BoundDecorProvider() {
override val viewId: Int = com.android.systemui.R.id.face_scanning_anim
@@ -136,7 +140,8 @@
alignedBound,
statusBarStateController,
keyguardUpdateMonitor,
- mainExecutor
+ mainExecutor,
+ logger,
)
view.id = viewId
view.setColor(tintColor)
@@ -155,8 +160,9 @@
layoutParams.let { lp ->
lp.width = ViewGroup.LayoutParams.MATCH_PARENT
lp.height = ViewGroup.LayoutParams.MATCH_PARENT
+ logger.faceSensorLocation(authController.faceSensorLocation)
authController.faceSensorLocation?.y?.let { faceAuthSensorHeight ->
- val faceScanningHeight = (faceAuthSensorHeight * 2).toInt()
+ val faceScanningHeight = (faceAuthSensorHeight * 2)
when (rotation) {
Surface.ROTATION_0, Surface.ROTATION_180 ->
lp.height = faceScanningHeight
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 6032da6..256c4ff 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -85,7 +85,7 @@
// TODO(b/259217907)
@JvmField
val NOTIFICATION_GROUP_DISMISSAL_ANIMATION =
- unreleasedFlag(259217907, "notification_group_dismissal_animation", teamfood = true)
+ releasedFlag(259217907, "notification_group_dismissal_animation")
// TODO(b/257506350): Tracking Bug
@JvmField val FSI_CHROME = unreleasedFlag(117, "fsi_chrome")
@@ -208,9 +208,7 @@
unreleasedFlag(226, "enable_wallet_contextual_loyalty_cards", teamfood = false)
// TODO(b/242908637): Tracking Bug
- @JvmField
- val WALLPAPER_FULLSCREEN_PREVIEW =
- unreleasedFlag(227, "wallpaper_fullscreen_preview", teamfood = true)
+ @JvmField val WALLPAPER_FULLSCREEN_PREVIEW = releasedFlag(227, "wallpaper_fullscreen_preview")
/** Whether the long-press gesture to open wallpaper picker is enabled. */
// TODO(b/266242192): Tracking Bug
@@ -312,9 +310,7 @@
val SCREEN_CONTENTS_TRANSLATION = unreleasedFlag(803, "screen_contents_translation")
// 804 - monochromatic themes
- @JvmField
- val MONOCHROMATIC_THEMES =
- sysPropBooleanFlag(804, "persist.sysui.monochromatic", default = false)
+ @JvmField val MONOCHROMATIC_THEME = unreleasedFlag(804, "monochromatic", teamfood = true)
// 900 - media
// TODO(b/254512697): Tracking Bug
@@ -344,8 +340,7 @@
@JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
// TODO(b/263272731): Tracking Bug
- val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
- unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
+ val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE = releasedFlag(910, "media_ttt_receiver_success_ripple")
// TODO(b/263512203): Tracking Bug
val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true)
@@ -361,6 +356,12 @@
@JvmField
val MEDIA_RECOMMENDATION_CARD_UPDATE = unreleasedFlag(914, "media_recommendation_card_update")
+ // TODO(b/267007629): Tracking Bug
+ val MEDIA_RESUME_PROGRESS = unreleasedFlag(915, "media_resume_progress")
+
+ // TODO(b/267166152) : Tracking Bug
+ val MEDIA_RETAIN_RECOMMENDATIONS = unreleasedFlag(916, "media_retain_recommendations")
+
// 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
@@ -472,7 +473,7 @@
// TODO(b/254512728): Tracking Bug
@JvmField
- val NEW_BACK_AFFORDANCE = unreleasedFlag(1203, "new_back_affordance", teamfood = false)
+ val NEW_BACK_AFFORDANCE = unreleasedFlag(1203, "new_back_affordance", teamfood = true)
// TODO(b/255854141): Tracking Bug
@JvmField
@@ -491,7 +492,7 @@
// TODO(b/238475428): Tracking Bug
@JvmField
val WM_SHADE_ALLOW_BACK_GESTURE =
- unreleasedFlag(1207, "persist.wm.debug.shade_allow_back_gesture", teamfood = false)
+ sysPropBooleanFlag(1207, "persist.wm.debug.shade_allow_back_gesture", default = false)
// TODO(b/238475428): Tracking Bug
@JvmField
@@ -506,11 +507,10 @@
// 1300 - screenshots
// TODO(b/254513155): Tracking Bug
@JvmField
- val SCREENSHOT_WORK_PROFILE_POLICY =
- unreleasedFlag(1301, "screenshot_work_profile_policy", teamfood = true)
+ val SCREENSHOT_WORK_PROFILE_POLICY = releasedFlag(1301, "screenshot_work_profile_policy")
// TODO(b/264916608): Tracking Bug
- @JvmField val SCREENSHOT_METADATA = unreleasedFlag(1302, "screenshot_metadata")
+ @JvmField val SCREENSHOT_METADATA = unreleasedFlag(1302, "screenshot_metadata", teamfood = true)
// TODO(b/266955521): Tracking bug
@JvmField val SCREENSHOT_DETECTION = unreleasedFlag(1303, "screenshot_detection")
@@ -525,16 +525,23 @@
// 1500 - chooser aka sharesheet
// TODO(b/254512507): Tracking Bug
- val CHOOSER_UNBUNDLED = unreleasedFlag(1500, "chooser_unbundled", teamfood = true)
+ val CHOOSER_UNBUNDLED = releasedFlag(1500, "chooser_unbundled")
// TODO(b/266983432) Tracking Bug
- val SHARESHEET_CUSTOM_ACTIONS = unreleasedFlag(1501, "sharesheet_custom_actions")
+ val SHARESHEET_CUSTOM_ACTIONS =
+ unreleasedFlag(1501, "sharesheet_custom_actions", teamfood = true)
// TODO(b/266982749) Tracking Bug
- val SHARESHEET_RESELECTION_ACTION = unreleasedFlag(1502, "sharesheet_reselection_action")
+ val SHARESHEET_RESELECTION_ACTION =
+ unreleasedFlag(1502, "sharesheet_reselection_action", teamfood = true)
// TODO(b/266983474) Tracking Bug
- val SHARESHEET_IMAGE_AND_TEXT_PREVIEW = unreleasedFlag(1503, "sharesheet_image_text_preview")
+ val SHARESHEET_IMAGE_AND_TEXT_PREVIEW =
+ unreleasedFlag(1503, "sharesheet_image_text_preview", teamfood = true)
+
+ // TODO(b/267355521) Tracking Bug
+ val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW =
+ unreleasedFlag(1504, "sharesheet_scrollable_image_preview")
// 1600 - accessibility
@JvmField
@@ -543,6 +550,8 @@
// 1700 - clipboard
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
+ // TODO(b/267162944): Tracking bug
+ @JvmField val CLIPBOARD_MINIMIZED_LAYOUT = unreleasedFlag(1702, "clipboard_data_model")
// 1800 - shade container
@JvmField
@@ -574,7 +583,7 @@
@JvmField val UDFPS_ELLIPSE_DETECTION = unreleasedFlag(2202, "udfps_ellipse_detection")
// 2300 - stylus
- @JvmField val TRACK_STYLUS_EVER_USED = unreleasedFlag(2300, "track_stylus_ever_used")
+ @JvmField val TRACK_STYLUS_EVER_USED = releasedFlag(2300, "track_stylus_ever_used")
@JvmField val ENABLE_STYLUS_CHARGING_UI = unreleasedFlag(2301, "enable_stylus_charging_ui")
@JvmField
val ENABLE_USI_BATTERY_NOTIFICATIONS = unreleasedFlag(2302, "enable_usi_battery_notifications")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
index d085db9..da91572 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
@@ -27,16 +27,24 @@
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.util.RingerModeTracker
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import javax.inject.Inject
@SysUISingleton
@@ -46,6 +54,9 @@
private val userFileManager: UserFileManager,
private val ringerModeTracker: RingerModeTracker,
private val audioManager: AudioManager,
+ @Application private val coroutineScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
) : KeyguardQuickAffordanceConfig {
private var previousNonSilentMode: Int = DEFAULT_LAST_NON_SILENT_VALUE
@@ -58,7 +69,7 @@
override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
ringerModeTracker.ringerModeInternal.asFlow()
- .onStart { emit(getLastNonSilentRingerMode()) }
+ .onStart { getLastNonSilentRingerMode() }
.distinctUntilChanged()
.onEach { mode ->
// only remember last non-SILENT ringer mode
@@ -87,54 +98,60 @@
activationState,
)
}
+ .flowOn(backgroundDispatcher)
override fun onTriggered(
expandable: Expandable?
): KeyguardQuickAffordanceConfig.OnTriggeredResult {
- val newRingerMode: Int
- val currentRingerMode =
- ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
- if (currentRingerMode == AudioManager.RINGER_MODE_SILENT) {
- newRingerMode = previousNonSilentMode
- } else {
- previousNonSilentMode = currentRingerMode
- newRingerMode = AudioManager.RINGER_MODE_SILENT
- }
+ coroutineScope.launch(backgroundDispatcher) {
+ val newRingerMode: Int
+ val currentRingerMode = audioManager.ringerModeInternal
+ if (currentRingerMode == AudioManager.RINGER_MODE_SILENT) {
+ newRingerMode = previousNonSilentMode
+ } else {
+ previousNonSilentMode = currentRingerMode
+ newRingerMode = AudioManager.RINGER_MODE_SILENT
+ }
- if (currentRingerMode != newRingerMode) {
- audioManager.ringerModeInternal = newRingerMode
+ if (currentRingerMode != newRingerMode) {
+ audioManager.ringerModeInternal = newRingerMode
+ }
}
return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
}
override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState =
- if (audioManager.isVolumeFixed) {
- KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
- } else {
- KeyguardQuickAffordanceConfig.PickerScreenState.Default()
+ withContext(backgroundDispatcher) {
+ if (audioManager.isVolumeFixed) {
+ KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+ } else {
+ KeyguardQuickAffordanceConfig.PickerScreenState.Default()
+ }
}
/**
* Gets the last non-silent ringer mode from shared-preferences if it exists. This is
* cached by [MuteQuickAffordanceCoreStartable] while this affordance is selected
*/
- private fun getLastNonSilentRingerMode(): Int =
- userFileManager.getSharedPreferences(
- MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
- Context.MODE_PRIVATE,
- userTracker.userId
- ).getInt(
- LAST_NON_SILENT_RINGER_MODE_KEY,
- ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
- )
+ private suspend fun getLastNonSilentRingerMode(): Int =
+ withContext(backgroundDispatcher) {
+ userFileManager.getSharedPreferences(
+ MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
+ Context.MODE_PRIVATE,
+ userTracker.userId
+ ).getInt(
+ LAST_NON_SILENT_RINGER_MODE_KEY,
+ ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
+ )
+ }
private fun <T> LiveData<T>.asFlow(): Flow<T?> =
- conflatedCallbackFlow {
- val observer = Observer { value: T -> trySend(value) }
- observeForever(observer)
- send(value)
- awaitClose { removeObserver(observer) }
- }
+ conflatedCallbackFlow {
+ val observer = Observer { value: T -> trySend(value) }
+ observeForever(observer)
+ send(value)
+ awaitClose { removeObserver(observer) }
+ }.flowOn(mainDispatcher)
companion object {
const val LAST_NON_SILENT_RINGER_MODE_KEY = "key_last_non_silent_ringer_mode"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt
index 12a6310..cd0805e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt
@@ -23,15 +23,18 @@
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.util.RingerModeTracker
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
import javax.inject.Inject
/**
@@ -45,6 +48,7 @@
private val userFileManager: UserFileManager,
private val keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository,
@Application private val coroutineScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
) : CoreStartable {
private val observer = Observer(this::updateLastNonSilentRingerMode)
@@ -72,15 +76,17 @@
}
private fun updateLastNonSilentRingerMode(lastRingerMode: Int) {
- if (AudioManager.RINGER_MODE_SILENT != lastRingerMode) {
- userFileManager.getSharedPreferences(
- MuteQuickAffordanceConfig.MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
- Context.MODE_PRIVATE,
- userTracker.userId
- )
- .edit()
- .putInt(MuteQuickAffordanceConfig.LAST_NON_SILENT_RINGER_MODE_KEY, lastRingerMode)
- .apply()
+ coroutineScope.launch(backgroundDispatcher) {
+ if (AudioManager.RINGER_MODE_SILENT != lastRingerMode) {
+ userFileManager.getSharedPreferences(
+ MuteQuickAffordanceConfig.MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
+ Context.MODE_PRIVATE,
+ userTracker.userId
+ )
+ .edit()
+ .putInt(MuteQuickAffordanceConfig.LAST_NON_SILENT_RINGER_MODE_KEY, lastRingerMode)
+ .apply()
+ }
}
}
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricRepository.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
index 25d8f40..0af596a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
@@ -47,12 +47,13 @@
import kotlinx.coroutines.flow.transformLatest
/**
- * Acts as source of truth for biometric features.
+ * Acts as source of truth for biometric authentication related settings like enrollments, device
+ * policy, etc.
*
* Abstracts-away data sources and their schemas so the rest of the app doesn't need to worry about
* upstream changes.
*/
-interface BiometricRepository {
+interface BiometricSettingsRepository {
/** Whether any fingerprints are enrolled for the current user. */
val isFingerprintEnrolled: StateFlow<Boolean>
@@ -68,7 +69,7 @@
}
@SysUISingleton
-class BiometricRepositoryImpl
+class BiometricSettingsRepositoryImpl
@Inject
constructor(
context: Context,
@@ -80,7 +81,7 @@
@Application scope: CoroutineScope,
@Background backgroundDispatcher: CoroutineDispatcher,
@Main looper: Looper,
-) : BiometricRepository {
+) : BiometricSettingsRepository {
/** UserId of the current selected user. */
private val selectedUserId: Flow<Int> =
@@ -88,7 +89,7 @@
override val isFingerprintEnrolled: StateFlow<Boolean> =
selectedUserId
- .flatMapLatest { userId ->
+ .flatMapLatest {
conflatedCallbackFlow {
val callback =
object : AuthController.Callback {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
index 3e17136..4ac6ac8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
@@ -41,31 +41,96 @@
*
* Make sure to add newly added flows to the logger.
*/
+interface KeyguardBouncerRepository {
+ /** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
+ val primaryBouncerVisible: StateFlow<Boolean>
+ val primaryBouncerShow: StateFlow<KeyguardBouncerModel?>
+ val primaryBouncerShowingSoon: StateFlow<Boolean>
+ val primaryBouncerHide: StateFlow<Boolean>
+ val primaryBouncerStartingToHide: StateFlow<Boolean>
+ val primaryBouncerStartingDisappearAnimation: StateFlow<Runnable?>
+ /** Determines if we want to instantaneously show the primary bouncer instead of translating. */
+ val primaryBouncerScrimmed: StateFlow<Boolean>
+ /**
+ * Set how much of the notification panel is showing on the screen.
+ * ```
+ * 0f = panel fully hidden = bouncer fully showing
+ * 1f = panel fully showing = bouncer fully hidden
+ * ```
+ */
+ val panelExpansionAmount: StateFlow<Float>
+ val keyguardPosition: StateFlow<Float>
+ val onScreenTurnedOff: StateFlow<Boolean>
+ val isBackButtonEnabled: StateFlow<Boolean?>
+ /** Determines if user is already unlocked */
+ val keyguardAuthenticated: StateFlow<Boolean?>
+ val showMessage: StateFlow<BouncerShowMessageModel?>
+ val resourceUpdateRequests: StateFlow<Boolean>
+ val bouncerPromptReason: Int
+ val bouncerErrorMessage: CharSequence?
+ val isAlternateBouncerVisible: StateFlow<Boolean>
+ val isAlternateBouncerUIAvailable: StateFlow<Boolean>
+ var lastAlternateBouncerVisibleTime: Long
+
+ fun setPrimaryScrimmed(isScrimmed: Boolean)
+
+ fun setPrimaryVisible(isVisible: Boolean)
+
+ fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?)
+
+ fun setPrimaryShowingSoon(showingSoon: Boolean)
+
+ fun setPrimaryHide(hide: Boolean)
+
+ fun setPrimaryStartingToHide(startingToHide: Boolean)
+
+ fun setPrimaryStartDisappearAnimation(runnable: Runnable?)
+
+ fun setPanelExpansion(panelExpansion: Float)
+
+ fun setKeyguardPosition(keyguardPosition: Float)
+
+ fun setResourceUpdateRequests(willUpdateResources: Boolean)
+
+ fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?)
+
+ fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?)
+
+ fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean)
+
+ fun setOnScreenTurnedOff(onScreenTurnedOff: Boolean)
+
+ fun setAlternateVisible(isVisible: Boolean)
+
+ fun setAlternateBouncerUIAvailable(isAvailable: Boolean)
+}
+
@SysUISingleton
-class KeyguardBouncerRepository
+class KeyguardBouncerRepositoryImpl
@Inject
constructor(
private val viewMediatorCallback: ViewMediatorCallback,
private val clock: SystemClock,
@Application private val applicationScope: CoroutineScope,
@BouncerLog private val buffer: TableLogBuffer,
-) {
+) : KeyguardBouncerRepository {
/** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
private val _primaryBouncerVisible = MutableStateFlow(false)
- val primaryBouncerVisible = _primaryBouncerVisible.asStateFlow()
+ override val primaryBouncerVisible = _primaryBouncerVisible.asStateFlow()
private val _primaryBouncerShow = MutableStateFlow<KeyguardBouncerModel?>(null)
- val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
+ override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
private val _primaryBouncerShowingSoon = MutableStateFlow(false)
- val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.asStateFlow()
+ override val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.asStateFlow()
private val _primaryBouncerHide = MutableStateFlow(false)
- val primaryBouncerHide = _primaryBouncerHide.asStateFlow()
+ override val primaryBouncerHide = _primaryBouncerHide.asStateFlow()
private val _primaryBouncerStartingToHide = MutableStateFlow(false)
- val primaryBouncerStartingToHide = _primaryBouncerStartingToHide.asStateFlow()
+ override val primaryBouncerStartingToHide = _primaryBouncerStartingToHide.asStateFlow()
private val _primaryBouncerDisappearAnimation = MutableStateFlow<Runnable?>(null)
- val primaryBouncerStartingDisappearAnimation = _primaryBouncerDisappearAnimation.asStateFlow()
+ override val primaryBouncerStartingDisappearAnimation =
+ _primaryBouncerDisappearAnimation.asStateFlow()
/** Determines if we want to instantaneously show the primary bouncer instead of translating. */
private val _primaryBouncerScrimmed = MutableStateFlow(false)
- val primaryBouncerScrimmed = _primaryBouncerScrimmed.asStateFlow()
+ override val primaryBouncerScrimmed = _primaryBouncerScrimmed.asStateFlow()
/**
* Set how much of the notification panel is showing on the screen.
* ```
@@ -74,23 +139,23 @@
* ```
*/
private val _panelExpansionAmount = MutableStateFlow(EXPANSION_HIDDEN)
- val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
+ override val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
private val _keyguardPosition = MutableStateFlow(0f)
- val keyguardPosition = _keyguardPosition.asStateFlow()
+ override val keyguardPosition = _keyguardPosition.asStateFlow()
private val _onScreenTurnedOff = MutableStateFlow(false)
- val onScreenTurnedOff = _onScreenTurnedOff.asStateFlow()
+ override val onScreenTurnedOff = _onScreenTurnedOff.asStateFlow()
private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
- val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
+ override val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null)
/** Determines if user is already unlocked */
- val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
+ override val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
private val _showMessage = MutableStateFlow<BouncerShowMessageModel?>(null)
- val showMessage = _showMessage.asStateFlow()
+ override val showMessage = _showMessage.asStateFlow()
private val _resourceUpdateRequests = MutableStateFlow(false)
- val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
- val bouncerPromptReason: Int
+ override val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
+ override val bouncerPromptReason: Int
get() = viewMediatorCallback.bouncerPromptReason
- val bouncerErrorMessage: CharSequence?
+ override val bouncerErrorMessage: CharSequence?
get() = viewMediatorCallback.consumeCustomMessage()
init {
@@ -99,21 +164,21 @@
/** Values associated with the AlternateBouncer */
private val _isAlternateBouncerVisible = MutableStateFlow(false)
- val isAlternateBouncerVisible = _isAlternateBouncerVisible.asStateFlow()
- var lastAlternateBouncerVisibleTime: Long = NOT_VISIBLE
+ override val isAlternateBouncerVisible = _isAlternateBouncerVisible.asStateFlow()
+ override var lastAlternateBouncerVisibleTime: Long = NOT_VISIBLE
private val _isAlternateBouncerUIAvailable = MutableStateFlow<Boolean>(false)
- val isAlternateBouncerUIAvailable: StateFlow<Boolean> =
+ override val isAlternateBouncerUIAvailable: StateFlow<Boolean> =
_isAlternateBouncerUIAvailable.asStateFlow()
- fun setPrimaryScrimmed(isScrimmed: Boolean) {
+ override fun setPrimaryScrimmed(isScrimmed: Boolean) {
_primaryBouncerScrimmed.value = isScrimmed
}
- fun setPrimaryVisible(isVisible: Boolean) {
+ override fun setPrimaryVisible(isVisible: Boolean) {
_primaryBouncerVisible.value = isVisible
}
- fun setAlternateVisible(isVisible: Boolean) {
+ override fun setAlternateVisible(isVisible: Boolean) {
if (isVisible && !_isAlternateBouncerVisible.value) {
lastAlternateBouncerVisibleTime = clock.uptimeMillis()
} else if (!isVisible) {
@@ -122,55 +187,55 @@
_isAlternateBouncerVisible.value = isVisible
}
- fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
+ override fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
_isAlternateBouncerUIAvailable.value = isAvailable
}
- fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?) {
+ override fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?) {
_primaryBouncerShow.value = keyguardBouncerModel
}
- fun setPrimaryShowingSoon(showingSoon: Boolean) {
+ override fun setPrimaryShowingSoon(showingSoon: Boolean) {
_primaryBouncerShowingSoon.value = showingSoon
}
- fun setPrimaryHide(hide: Boolean) {
+ override fun setPrimaryHide(hide: Boolean) {
_primaryBouncerHide.value = hide
}
- fun setPrimaryStartingToHide(startingToHide: Boolean) {
+ override fun setPrimaryStartingToHide(startingToHide: Boolean) {
_primaryBouncerStartingToHide.value = startingToHide
}
- fun setPrimaryStartDisappearAnimation(runnable: Runnable?) {
+ override fun setPrimaryStartDisappearAnimation(runnable: Runnable?) {
_primaryBouncerDisappearAnimation.value = runnable
}
- fun setPanelExpansion(panelExpansion: Float) {
+ override fun setPanelExpansion(panelExpansion: Float) {
_panelExpansionAmount.value = panelExpansion
}
- fun setKeyguardPosition(keyguardPosition: Float) {
+ override fun setKeyguardPosition(keyguardPosition: Float) {
_keyguardPosition.value = keyguardPosition
}
- fun setResourceUpdateRequests(willUpdateResources: Boolean) {
+ override fun setResourceUpdateRequests(willUpdateResources: Boolean) {
_resourceUpdateRequests.value = willUpdateResources
}
- fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?) {
+ override fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?) {
_showMessage.value = bouncerShowMessageModel
}
- fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?) {
+ override fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?) {
_keyguardAuthenticated.value = keyguardAuthenticated
}
- fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean) {
+ override fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean) {
_isBackButtonEnabled.value = isBackButtonEnabled
}
- fun setOnScreenTurnedOff(onScreenTurnedOff: Boolean) {
+ override fun setOnScreenTurnedOff(onScreenTurnedOff: Boolean) {
_onScreenTurnedOff.value = onScreenTurnedOff
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
index cc99eb7..4a262f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
@@ -31,10 +31,16 @@
@Binds
fun lightRevealScrimRepository(impl: LightRevealScrimRepositoryImpl): LightRevealScrimRepository
- @Binds fun biometricRepository(impl: BiometricRepositoryImpl): BiometricRepository
+ @Binds
+ fun biometricSettingsRepository(
+ impl: BiometricSettingsRepositoryImpl
+ ): BiometricSettingsRepository
@Binds
fun deviceEntryFingerprintAuthRepository(
impl: DeviceEntryFingerprintAuthRepositoryImpl
): DeviceEntryFingerprintAuthRepository
+
+ @Binds
+ fun keyguardBouncerRepository(impl: KeyguardBouncerRepositoryImpl): KeyguardBouncerRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt
index 6020ef8..6452e0e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt
@@ -20,7 +20,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.BiometricRepository
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.LegacyAlternateBouncer
@@ -34,7 +34,7 @@
@Inject
constructor(
private val bouncerRepository: KeyguardBouncerRepository,
- private val biometricRepository: BiometricRepository,
+ private val biometricSettingsRepository: BiometricSettingsRepository,
private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
private val systemClock: SystemClock,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@@ -99,9 +99,9 @@
fun canShowAlternateBouncerForFingerprint(): Boolean {
return if (isModernAlternateBouncerEnabled) {
bouncerRepository.isAlternateBouncerUIAvailable.value &&
- biometricRepository.isFingerprintEnrolled.value &&
- biometricRepository.isStrongBiometricAllowed.value &&
- biometricRepository.isFingerprintEnabledByDevicePolicy.value &&
+ biometricSettingsRepository.isFingerprintEnrolled.value &&
+ biometricSettingsRepository.isStrongBiometricAllowed.value &&
+ biometricSettingsRepository.isFingerprintEnabledByDevicePolicy.value &&
!deviceEntryFingerprintAuthRepository.isLockedOut.value
} else {
legacyAlternateBouncer != null &&
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index ce61f2f..86f65dde 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -21,6 +21,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake
@@ -44,6 +45,7 @@
override fun start() {
listenForDozingToLockscreen()
+ listenForDozingToGone()
}
private fun listenForDozingToLockscreen() {
@@ -68,6 +70,28 @@
}
}
+ private fun listenForDozingToGone() {
+ scope.launch {
+ keyguardInteractor.biometricUnlockState
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { (biometricUnlockState, lastStartedTransition) ->
+ if (
+ lastStartedTransition.to == KeyguardState.DOZING &&
+ isWakeAndUnlock(biometricUnlockState)
+ ) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.DOZING,
+ KeyguardState.GONE,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
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 57c3b31..b2da793 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
@@ -367,6 +367,10 @@
name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_FULLSCREEN_PREVIEW,
value = featureFlags.isEnabled(Flags.WALLPAPER_FULLSCREEN_PREVIEW),
),
+ KeyguardPickerFlag(
+ name = Contract.FlagsTable.FLAG_NAME_MONOCHROMATIC_THEME,
+ value = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME)
+ )
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index ad6dbea..53c80f6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -19,7 +19,6 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER
@@ -31,9 +30,6 @@
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.shared.model.TransitionStep
import javax.inject.Inject
-import kotlin.math.max
-import kotlin.math.min
-import kotlin.time.Duration
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
@@ -104,38 +100,4 @@
/* The last completed [KeyguardState] transition */
val finishedKeyguardState: Flow<KeyguardState> =
finishedKeyguardTransitionStep.map { step -> step.to }
-
- /**
- * Transitions will occur over a [totalDuration] with [TransitionStep]s being emitted in the
- * range of [0, 1]. View animations should begin and end within a subset of this range. This
- * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
- */
- fun transitionStepAnimation(
- flow: Flow<TransitionStep>,
- params: AnimationParams,
- totalDuration: Duration,
- ): Flow<Float> {
- val start = (params.startTime / totalDuration).toFloat()
- val chunks = (totalDuration / params.duration).toFloat()
- var isRunning = false
- return flow
- .map { step ->
- val value = (step.value - start) * chunks
- if (step.transitionState == STARTED) {
- // When starting, make sure to always emit. If a transition is started from the
- // middle, it is possible this animation is being skipped but we need to inform
- // the ViewModels of the last update
- isRunning = true
- max(0f, min(1f, value))
- } else if (isRunning && value >= 1f) {
- // Always send a final value of 1. Because of rounding, [value] may never be
- // exactly 1.
- isRunning = false
- 1f
- } else {
- value
- }
- }
- .filter { value -> value >= 0f && value <= 1f }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
index a92540d..96bf815 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
@@ -113,6 +113,8 @@
0f
}
}
+ /** Allow for interaction when just about fully visible */
+ val isInteractable: Flow<Boolean> = bouncerExpansion.map { it > 0.9 }
// TODO(b/243685699): Move isScrimmed logic to data layer.
// TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
new file mode 100644
index 0000000..ca1e27c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.ui
+
+import android.view.animation.Interpolator
+import com.android.systemui.animation.Interpolators.LINEAR
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+/**
+ * For the given transition params, construct a flow using [createFlow] for the specified portion of
+ * the overall transition.
+ */
+class KeyguardTransitionAnimationFlow(
+ private val transitionDuration: Duration,
+ private val transitionFlow: Flow<TransitionStep>,
+) {
+ /**
+ * Transitions will occur over a [transitionDuration] with [TransitionStep]s being emitted in
+ * the range of [0, 1]. View animations should begin and end within a subset of this range. This
+ * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
+ */
+ fun createFlow(
+ duration: Duration,
+ onStep: (Float) -> Float,
+ startTime: Duration = 0.milliseconds,
+ onCancel: (() -> Float)? = null,
+ onFinish: (() -> Float)? = null,
+ interpolator: Interpolator = LINEAR,
+ ): Flow<Float> {
+ if (!duration.isPositive()) {
+ throw IllegalArgumentException("duration must be a positive number: $duration")
+ }
+ if ((startTime + duration).compareTo(transitionDuration) > 0) {
+ throw IllegalArgumentException(
+ "startTime($startTime) + duration($duration) must be" +
+ " <= transitionDuration($transitionDuration)"
+ )
+ }
+
+ val start = (startTime / transitionDuration).toFloat()
+ val chunks = (transitionDuration / duration).toFloat()
+ var isComplete = true
+
+ fun stepToValue(step: TransitionStep): Float? {
+ val value = (step.value - start) * chunks
+ return when (step.transitionState) {
+ // When starting, make sure to always emit. If a transition is started from the
+ // middle, it is possible this animation is being skipped but we need to inform
+ // the ViewModels of the last update
+ STARTED -> {
+ isComplete = false
+ max(0f, min(1f, value))
+ }
+ // Always send a final value of 1. Because of rounding, [value] may never be
+ // exactly 1.
+ RUNNING ->
+ if (isComplete) {
+ null
+ } else if (value >= 1f) {
+ isComplete = true
+ 1f
+ } else if (value >= 0f) {
+ value
+ } else {
+ null
+ }
+ else -> null
+ }?.let { onStep(interpolator.getInterpolation(it)) }
+ }
+
+ return transitionFlow
+ .map { step ->
+ when (step.transitionState) {
+ STARTED -> stepToValue(step)
+ RUNNING -> stepToValue(step)
+ CANCELED -> onCancel?.invoke()
+ FINISHED -> onFinish?.invoke()
+ }
+ }
+ .filterNotNull()
+ }
+}
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 3319f9d..ab009f4 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
@@ -381,82 +381,87 @@
return when (event?.actionMasked) {
MotionEvent.ACTION_DOWN ->
if (viewModel.configKey != null) {
- longPressAnimator =
- view
- .animate()
- .scaleX(PRESSED_SCALE)
- .scaleY(PRESSED_SCALE)
- .setDuration(longPressDurationMs)
- .withEndAction {
- view.setOnClickListener {
- vibratorHelper?.vibrate(
- if (viewModel.isActivated) {
- Vibrations.Activated
- } else {
- Vibrations.Deactivated
- }
- )
- viewModel.onClicked(
- KeyguardQuickAffordanceViewModel.OnClickedParameters(
- configKey = viewModel.configKey,
- expandable = Expandable.fromView(view),
- )
- )
+ if (isUsingAccurateTool(event)) {
+ // For accurate tool types (stylus, mouse, etc.), we don't require a
+ // long-press.
+ } else {
+ // When not using a stylus, we require a long-press to activate the
+ // quick affordance, mostly to do "falsing" (e.g. protect from false
+ // clicks in the pocket/bag).
+ longPressAnimator =
+ view
+ .animate()
+ .scaleX(PRESSED_SCALE)
+ .scaleY(PRESSED_SCALE)
+ .setDuration(longPressDurationMs)
+ .withEndAction {
+ dispatchClick(viewModel.configKey)
+ cancel()
}
- view.performClick()
- view.setOnClickListener(null)
- cancel()
- }
+ }
true
} else {
false
}
MotionEvent.ACTION_MOVE -> {
- if (event.historySize > 0) {
- val distance =
- sqrt(
- (event.y - event.getHistoricalY(0)).pow(2) +
- (event.x - event.getHistoricalX(0)).pow(2)
- )
- if (distance > ViewConfiguration.getTouchSlop()) {
+ if (!isUsingAccurateTool(event)) {
+ // Moving too far while performing a long-press gesture cancels that
+ // gesture.
+ val distanceMoved = distanceMoved(event)
+ if (distanceMoved > ViewConfiguration.getTouchSlop()) {
cancel()
}
}
true
}
MotionEvent.ACTION_UP -> {
- cancel(
- onAnimationEnd =
- if (event.eventTime - event.downTime < longPressDurationMs) {
- Runnable {
- messageDisplayer.invoke(
- R.string.keyguard_affordance_press_too_short
- )
- val amplitude =
- view.context.resources
- .getDimensionPixelSize(
- R.dimen.keyguard_affordance_shake_amplitude
- )
- .toFloat()
- val shakeAnimator =
- ObjectAnimator.ofFloat(
- view,
- "translationX",
- -amplitude / 2,
- amplitude / 2,
+ if (isUsingAccurateTool(event)) {
+ // When using an accurate tool type (stylus, mouse, etc.), we don't require
+ // a long-press gesture to activate the quick affordance. Therefore, lifting
+ // the pointer performs a click.
+ if (
+ viewModel.configKey != null &&
+ distanceMoved(event) <= ViewConfiguration.getTouchSlop()
+ ) {
+ dispatchClick(viewModel.configKey)
+ }
+ } else {
+ // When not using a stylus, lifting the finger/pointer will actually cancel
+ // the long-press gesture. Calling cancel after the quick affordance was
+ // already long-press activated is a no-op, so it's safe to call from here.
+ cancel(
+ onAnimationEnd =
+ if (event.eventTime - event.downTime < longPressDurationMs) {
+ Runnable {
+ messageDisplayer.invoke(
+ R.string.keyguard_affordance_press_too_short
)
- shakeAnimator.duration =
- ShakeAnimationDuration.inWholeMilliseconds
- shakeAnimator.interpolator =
- CycleInterpolator(ShakeAnimationCycles)
- shakeAnimator.start()
+ val amplitude =
+ view.context.resources
+ .getDimensionPixelSize(
+ R.dimen.keyguard_affordance_shake_amplitude
+ )
+ .toFloat()
+ val shakeAnimator =
+ ObjectAnimator.ofFloat(
+ view,
+ "translationX",
+ -amplitude / 2,
+ amplitude / 2,
+ )
+ shakeAnimator.duration =
+ ShakeAnimationDuration.inWholeMilliseconds
+ shakeAnimator.interpolator =
+ CycleInterpolator(ShakeAnimationCycles)
+ shakeAnimator.start()
- vibratorHelper?.vibrate(Vibrations.Shake)
+ vibratorHelper?.vibrate(Vibrations.Shake)
+ }
+ } else {
+ null
}
- } else {
- null
- }
- )
+ )
+ }
true
}
MotionEvent.ACTION_CANCEL -> {
@@ -467,6 +472,28 @@
}
}
+ private fun dispatchClick(
+ configKey: String,
+ ) {
+ view.setOnClickListener {
+ vibratorHelper?.vibrate(
+ if (viewModel.isActivated) {
+ Vibrations.Activated
+ } else {
+ Vibrations.Deactivated
+ }
+ )
+ viewModel.onClicked(
+ KeyguardQuickAffordanceViewModel.OnClickedParameters(
+ configKey = configKey,
+ expandable = Expandable.fromView(view),
+ )
+ )
+ }
+ view.performClick()
+ view.setOnClickListener(null)
+ }
+
private fun cancel(onAnimationEnd: Runnable? = null) {
longPressAnimator?.cancel()
longPressAnimator = null
@@ -475,6 +502,40 @@
companion object {
private const val PRESSED_SCALE = 1.5f
+
+ /**
+ * Returns `true` if the tool type at the given pointer index is an accurate tool (like
+ * stylus or mouse), which means we can trust it to not be a false click; `false`
+ * otherwise.
+ */
+ private fun isUsingAccurateTool(
+ event: MotionEvent,
+ pointerIndex: Int = 0,
+ ): Boolean {
+ return when (event.getToolType(pointerIndex)) {
+ MotionEvent.TOOL_TYPE_STYLUS -> true
+ MotionEvent.TOOL_TYPE_MOUSE -> true
+ else -> false
+ }
+ }
+
+ /**
+ * Returns the amount of distance the pointer moved since the historical record at the
+ * [since] index.
+ */
+ private fun distanceMoved(
+ event: MotionEvent,
+ since: Int = 0,
+ ): Float {
+ return if (event.historySize > 0) {
+ sqrt(
+ (event.y - event.getHistoricalY(since)).pow(2) +
+ (event.x - event.getHistoricalX(since)).pow(2)
+ )
+ } else {
+ 0f
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
index c1731e0..9f09d53 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
@@ -154,6 +154,12 @@
}
launch {
+ viewModel.isInteractable.collect { isInteractable ->
+ hostViewController.setInteractable(isInteractable)
+ }
+ }
+
+ launch {
viewModel.isBouncerVisible
.filter { !it }
.collect {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 8808574..adde595 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -177,7 +177,8 @@
val receiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
- clockController.clock?.events?.onTimeTick()
+ clockController.clock?.smallClock?.events?.onTimeTick()
+ clockController.clock?.largeClock?.events?.onTimeTick()
}
}
broadcastDispatcher.registerReceiver(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index 6627865..8d6545a4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -21,15 +21,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
-import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
/**
* Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -41,39 +36,46 @@
constructor(
private val interactor: KeyguardTransitionInteractor,
) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_LOCKSCREEN_DURATION,
+ transitionFlow = interactor.dreamingToLockscreenTransition,
+ )
/** Dream overlay y-translation on exit */
fun dreamOverlayTranslationY(translatePx: Int): Flow<Float> {
- return flowForAnimation(DREAM_OVERLAY_TRANSLATION_Y).map { value ->
- EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx
- }
+ return transitionAnimation.createFlow(
+ duration = 600.milliseconds,
+ onStep = { it * translatePx },
+ interpolator = EMPHASIZED_ACCELERATE,
+ )
}
/** Dream overlay views alpha - fade out */
- val dreamOverlayAlpha: Flow<Float> = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it }
+ val dreamOverlayAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ duration = 250.milliseconds,
+ onStep = { 1f - it },
+ )
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return merge(
- flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
- },
- // On end, reset the translation to 0
- interactor.dreamingToLockscreenTransition
- .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
- .map { 0f }
+ return transitionAnimation.createFlow(
+ duration = TO_LOCKSCREEN_DURATION,
+ onStep = { value -> -translatePx + value * translatePx },
+ // Reset on cancel or finish
+ onFinish = { 0f },
+ onCancel = { 0f },
+ interpolator = EMPHASIZED_DECELERATE,
)
}
/** Lockscreen views alpha */
- val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
-
- private fun flowForAnimation(params: AnimationParams): Flow<Float> {
- return interactor.transitionStepAnimation(
- interactor.dreamingToLockscreenTransition,
- params,
- totalDuration = TO_LOCKSCREEN_DURATION
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ startTime = 233.milliseconds,
+ duration = 250.milliseconds,
+ onStep = { it },
)
- }
companion object {
/* Length of time before ending the dream activity, in order to start unoccluding */
@@ -81,11 +83,5 @@
@JvmField
val LOCKSCREEN_ANIMATION_DURATION_MS =
(TO_LOCKSCREEN_DURATION - DREAM_ANIMATION_DURATION).inWholeMilliseconds
-
- val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 600.milliseconds)
- val DREAM_OVERLAY_ALPHA = AnimationParams(duration = 250.milliseconds)
- val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
- val LOCKSCREEN_ALPHA =
- AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
index 5a47960..f16827d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
@@ -20,15 +20,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
-import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
/** Breaks down GONE->DREAMING transition into discrete steps for corresponding views to consume. */
@SysUISingleton
@@ -38,32 +33,28 @@
private val interactor: KeyguardTransitionInteractor,
) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_DREAMING_DURATION,
+ transitionFlow = interactor.goneToDreamingTransition,
+ )
+
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return merge(
- flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx)
- },
- // On end, reset the translation to 0
- interactor.goneToDreamingTransition
- .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
- .map { 0f }
+ return transitionAnimation.createFlow(
+ duration = 500.milliseconds,
+ onStep = { it * translatePx },
+ // Reset on cancel or finish
+ onFinish = { 0f },
+ onCancel = { 0f },
+ interpolator = EMPHASIZED_ACCELERATE,
)
}
/** Lockscreen views alpha */
- val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it }
-
- private fun flowForAnimation(params: AnimationParams): Flow<Float> {
- return interactor.transitionStepAnimation(
- interactor.goneToDreamingTransition,
- params,
- totalDuration = TO_DREAMING_DURATION
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ duration = 250.milliseconds,
+ onStep = { 1f - it },
)
- }
-
- companion object {
- val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds)
- val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds)
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
index 737c35d..b8b3a8e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -39,6 +39,9 @@
/** Observe on bouncer visibility. */
val isBouncerVisible: Flow<Boolean> = interactor.isVisible
+ /** Can the user interact with the view? */
+ val isInteractable: Flow<Boolean> = interactor.isInteractable
+
/** Observe whether bouncer is showing. */
val show: Flow<KeyguardBouncerModel> = interactor.show
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
index e05adbd..bc9dc4f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
@@ -20,15 +20,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
-import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
/**
* Breaks down LOCKSCREEN->DREAMING transition into discrete steps for corresponding views to
@@ -40,35 +35,32 @@
constructor(
private val interactor: KeyguardTransitionInteractor,
) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_DREAMING_DURATION,
+ transitionFlow = interactor.lockscreenToDreamingTransition,
+ )
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return merge(
- flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx)
- },
- // On end, reset the translation to 0
- interactor.lockscreenToDreamingTransition
- .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
- .map { 0f }
+ return transitionAnimation.createFlow(
+ duration = 500.milliseconds,
+ onStep = { it * translatePx },
+ // Reset on cancel or finish
+ onFinish = { 0f },
+ onCancel = { 0f },
+ interpolator = EMPHASIZED_ACCELERATE,
)
}
/** Lockscreen views alpha */
- val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it }
-
- private fun flowForAnimation(params: AnimationParams): Flow<Float> {
- return interactor.transitionStepAnimation(
- interactor.lockscreenToDreamingTransition,
- params,
- totalDuration = TO_DREAMING_DURATION
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ duration = 250.milliseconds,
+ onStep = { 1f - it },
)
- }
companion object {
@JvmField val DREAMING_ANIMATION_DURATION_MS = TO_DREAMING_DURATION.inWholeMilliseconds
-
- val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds)
- val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
index 22d292e..a60665a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
@@ -20,14 +20,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
/**
* Breaks down LOCKSCREEN->OCCLUDED transition into discrete steps for corresponding views to
@@ -39,33 +35,28 @@
constructor(
private val interactor: KeyguardTransitionInteractor,
) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_OCCLUDED_DURATION,
+ transitionFlow = interactor.lockscreenToOccludedTransition,
+ )
+
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ duration = 250.milliseconds,
+ onStep = { 1f - it },
+ )
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return merge(
- flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx)
- },
- // On end, reset the translation to 0
- interactor.lockscreenToOccludedTransition
- .filter { step -> step.transitionState == TransitionState.FINISHED }
- .map { 0f }
+ return transitionAnimation.createFlow(
+ duration = TO_OCCLUDED_DURATION,
+ onStep = { value -> value * translatePx },
+ // Reset on cancel or finish
+ onFinish = { 0f },
+ onCancel = { 0f },
+ interpolator = EMPHASIZED_ACCELERATE,
)
}
-
- /** Lockscreen views alpha */
- val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it }
-
- private fun flowForAnimation(params: AnimationParams): Flow<Float> {
- return interactor.transitionStepAnimation(
- interactor.lockscreenToOccludedTransition,
- params,
- totalDuration = TO_OCCLUDED_DURATION
- )
- }
-
- companion object {
- val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_OCCLUDED_DURATION)
- val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds)
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
index e804562..5770f3e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
@@ -20,11 +20,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
/**
* Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -36,28 +35,26 @@
constructor(
private val interactor: KeyguardTransitionInteractor,
) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_LOCKSCREEN_DURATION,
+ transitionFlow = interactor.occludedToLockscreenTransition,
+ )
+
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
- }
- }
-
- /** Lockscreen views alpha */
- val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
-
- private fun flowForAnimation(params: AnimationParams): Flow<Float> {
- return interactor.transitionStepAnimation(
- interactor.occludedToLockscreenTransition,
- params,
- totalDuration = TO_LOCKSCREEN_DURATION
+ return transitionAnimation.createFlow(
+ duration = TO_LOCKSCREEN_DURATION,
+ onStep = { value -> -translatePx + value * translatePx },
+ interpolator = EMPHASIZED_DECELERATE,
)
}
- companion object {
- @JvmField val LOCKSCREEN_ANIMATION_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds
- val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
- val LOCKSCREEN_ALPHA =
- AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
- }
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ startTime = 233.milliseconds,
+ duration = 250.milliseconds,
+ onStep = { it },
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt
new file mode 100644
index 0000000..5acaa46
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.RectF
+import androidx.core.graphics.toRectF
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.ScreenDecorationsLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import javax.inject.Inject
+
+private const val TAG = "ScreenDecorationsLog"
+
+/**
+ * Helper class for logging for [com.android.systemui.ScreenDecorations]
+ *
+ * To enable logcat echoing for an entire buffer:
+ *
+ * ```
+ * adb shell settings put global systemui/buffer/ScreenDecorationsLog <logLevel>
+ *
+ * ```
+ */
+@SysUISingleton
+class ScreenDecorationsLogger
+@Inject
+constructor(
+ @ScreenDecorationsLog private val logBuffer: LogBuffer,
+) {
+ fun cameraProtectionBoundsForScanningOverlay(bounds: Rect) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ { str1 = bounds.toShortString() },
+ { "Face scanning overlay present camera protection bounds: $str1" }
+ )
+ }
+
+ fun hwcLayerCameraProtectionBounds(bounds: Rect) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ { str1 = bounds.toShortString() },
+ { "Hwc layer present camera protection bounds: $str1" }
+ )
+ }
+
+ fun dcvCameraBounds(id: Int, bounds: Rect) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = bounds.toShortString()
+ int1 = id
+ },
+ { "DisplayCutoutView id=$int1 present, camera protection bounds: $str1" }
+ )
+ }
+
+ fun cutoutViewNotInitialized() {
+ logBuffer.log(TAG, ERROR, "CutoutView not initialized showCameraProtection")
+ }
+
+ fun boundingRect(boundingRectangle: RectF, context: String) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = context
+ str2 = boundingRectangle.toShortString()
+ },
+ { "Bounding rect $str1 : $str2" }
+ )
+ }
+
+ fun boundingRect(boundingRectangle: Rect, context: String) {
+ boundingRect(boundingRectangle.toRectF(), context)
+ }
+
+ fun onMeasureDimensions(
+ widthMeasureSpec: Int,
+ heightMeasureSpec: Int,
+ measuredWidth: Int,
+ measuredHeight: Int
+ ) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ long1 = widthMeasureSpec.toLong()
+ long2 = heightMeasureSpec.toLong()
+ int1 = measuredWidth
+ int2 = measuredHeight
+ },
+ {
+ "Face scanning animation: widthMeasureSpec: $long1 measuredWidth: $int1, " +
+ "heightMeasureSpec: $long2 measuredHeight: $int2"
+ }
+ )
+ }
+
+ fun faceSensorLocation(faceSensorLocation: Point?) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ int1 = faceSensorLocation?.y?.times(2) ?: 0
+ str1 = "$faceSensorLocation"
+ },
+ { "Reinflating view: Face sensor location: $str1, faceScanningHeight: $int1" }
+ )
+ }
+
+ fun onSensorLocationChanged() {
+ logBuffer.log(TAG, DEBUG, "AuthControllerCallback in ScreenDecorations triggered")
+ }
+}
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 6c6f7e9..4177480 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -350,6 +350,16 @@
}
/**
+ * Provides a {@link LogBuffer} for use by {@link com.android.systemui.ScreenDecorations}.
+ */
+ @Provides
+ @SysUISingleton
+ @ScreenDecorationsLog
+ public static LogBuffer provideScreenDecorationsLog(LogBufferFactory factory) {
+ return factory.create("ScreenDecorationsLog", 200);
+ }
+
+ /**
* Provides a {@link LogBuffer} for bluetooth-related logs.
*/
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/ScreenDecorationsLog.kt
similarity index 60%
rename from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
rename to packages/SystemUI/src/com/android/systemui/log/dagger/ScreenDecorationsLog.kt
index 67733e9..de2a8b6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ScreenDecorationsLog.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -11,15 +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.keyguard.shared.model
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
+package com.android.systemui.log.dagger
-/** Animation parameters */
-data class AnimationParams(
- val startTime: Duration = 0.milliseconds,
- val duration: Duration,
-)
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for ScreenDecorations added by SysUI. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ScreenDecorationsLog
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index a692ad7..52d4171 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -28,12 +28,15 @@
import android.os.UserHandle
import android.view.ViewGroup
import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyStateProvider
import com.android.internal.app.AbstractMultiProfilePagerAdapter.MyUserIdProvider
import com.android.internal.app.ChooserActivity
import com.android.internal.app.ResolverListController
import com.android.internal.app.chooser.NotSelectableTargetInfo
import com.android.internal.app.chooser.TargetInfo
import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorComponent
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
@@ -47,6 +50,7 @@
class MediaProjectionAppSelectorActivity(
private val componentFactory: MediaProjectionAppSelectorComponent.Factory,
private val activityLauncher: AsyncActivityLauncher,
+ private val featureFlags: FeatureFlags,
/** This is used to override the dependency in a screenshot test */
@VisibleForTesting
private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
@@ -56,7 +60,8 @@
constructor(
componentFactory: MediaProjectionAppSelectorComponent.Factory,
activityLauncher: AsyncActivityLauncher,
- ) : this(componentFactory, activityLauncher, null)
+ featureFlags: FeatureFlags
+ ) : this(componentFactory, activityLauncher, featureFlags, listControllerFactory = null)
private lateinit var configurationController: ConfigurationController
private lateinit var controller: MediaProjectionAppSelectorController
@@ -91,6 +96,13 @@
override fun appliedThemeResId(): Int = R.style.Theme_SystemUI_MediaProjectionAppSelector
+ override fun createBlockerEmptyStateProvider(): EmptyStateProvider =
+ if (featureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
+ component.emptyStateProvider
+ } else {
+ super.createBlockerEmptyStateProvider()
+ }
+
override fun createListController(userHandle: UserHandle): ResolverListController =
listControllerFactory?.invoke(userHandle) ?: super.createListController(userHandle)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
index d830fc4..c4e76b2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@@ -45,33 +45,46 @@
import android.util.Log;
import android.view.Window;
-import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
import com.android.systemui.screenrecord.MediaProjectionPermissionDialog;
import com.android.systemui.screenrecord.ScreenShareOption;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.Utils;
+import javax.inject.Inject;
+
+import dagger.Lazy;
+
public class MediaProjectionPermissionActivity extends Activity
implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
private static final String TAG = "MediaProjectionPermissionActivity";
private static final float MAX_APP_NAME_SIZE_PX = 500f;
private static final String ELLIPSIS = "\u2026";
+ private final FeatureFlags mFeatureFlags;
+ private final Lazy<ScreenCaptureDevicePolicyResolver> mScreenCaptureDevicePolicyResolver;
+
private String mPackageName;
private int mUid;
private IMediaProjectionManager mService;
- private FeatureFlags mFeatureFlags;
private AlertDialog mDialog;
+ @Inject
+ public MediaProjectionPermissionActivity(FeatureFlags featureFlags,
+ Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver) {
+ mFeatureFlags = featureFlags;
+ mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver;
+ }
+
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
- mFeatureFlags = Dependency.get(FeatureFlags.class);
mPackageName = getCallingPackage();
IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
mService = IMediaProjectionManager.Stub.asInterface(b);
@@ -104,6 +117,12 @@
return;
}
+ if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
+ if (showScreenCaptureDisabledDialogIfNeeded()) {
+ return;
+ }
+ }
+
TextPaint paint = new TextPaint();
paint.setTextSize(42);
@@ -171,16 +190,7 @@
mDialog = dialogBuilder.create();
}
- SystemUIDialog.registerDismissListener(mDialog);
- SystemUIDialog.applyFlags(mDialog);
- SystemUIDialog.setDialogSize(mDialog);
-
- mDialog.setOnCancelListener(this);
- mDialog.create();
- mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
-
- final Window w = mDialog.getWindow();
- w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ setUpDialog(mDialog);
mDialog.show();
}
@@ -200,6 +210,32 @@
}
}
+ private void setUpDialog(AlertDialog dialog) {
+ SystemUIDialog.registerDismissListener(dialog);
+ SystemUIDialog.applyFlags(dialog);
+ SystemUIDialog.setDialogSize(dialog);
+
+ dialog.setOnCancelListener(this);
+ dialog.create();
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
+
+ final Window w = dialog.getWindow();
+ w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ }
+
+ private boolean showScreenCaptureDisabledDialogIfNeeded() {
+ final UserHandle hostUserHandle = getHostUserHandle();
+ if (mScreenCaptureDevicePolicyResolver.get()
+ .isScreenCaptureCompletelyDisabled(hostUserHandle)) {
+ AlertDialog dialog = new ScreenCaptureDisabledDialog(this);
+ setUpDialog(dialog);
+ dialog.show();
+ return true;
+ }
+
+ return false;
+ }
+
private void grantMediaProjectionPermission(int screenShareMode) {
try {
if (screenShareMode == ENTIRE_SCREEN) {
@@ -211,7 +247,7 @@
intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
projection.asBinder());
intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
- UserHandle.getUserHandleForUid(getLaunchedFromUid()));
+ getHostUserHandle());
intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
// Start activity from the current foreground user to avoid creating a separate
@@ -230,6 +266,10 @@
}
}
+ private UserHandle getHostUserHandle() {
+ return UserHandle.getUserHandleForUid(getLaunchedFromUid());
+ }
+
private IMediaProjection createProjection(int uid, String packageName) throws RemoteException {
return mService.createProjection(uid, packageName,
MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */);
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
index be18cbe..b7a2522 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
@@ -92,6 +92,9 @@
/** Whether explicit indicator exists */
val isExplicit: Boolean = false,
+
+ /** Track progress (0 - 1) to display for players where [resumption] is true */
+ val resumeProgress: Double? = null,
) {
companion object {
/** Media is playing on the local device */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
index bba5f35..a057c9f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
@@ -238,6 +238,24 @@
}
/**
+ * Set the progress to a fixed percentage value that cannot be changed by the user.
+ *
+ * @param percent value between 0 and 1
+ */
+ fun updateStaticProgress(percent: Double) {
+ val position = (percent * 100).toInt()
+ _data =
+ Progress(
+ enabled = true,
+ seekAvailable = false,
+ playing = false,
+ scrubbing = false,
+ elapsedTime = position,
+ duration = 100,
+ )
+ }
+
+ /**
* Puts the seek bar into a resumption state.
*
* This should be called when the media session behind the controller has been destroyed.
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
index 8c1ec16..70f2dee 100644
--- 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
@@ -20,6 +20,7 @@
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
+import android.widget.SeekBar
import android.widget.TextView
import com.android.internal.widget.CachingIconView
import com.android.systemui.R
@@ -37,6 +38,7 @@
// Recommendation screen
lateinit var cardIcon: ImageView
lateinit var mediaAppIcons: List<CachingIconView>
+ lateinit var mediaProgressBars: List<SeekBar>
lateinit var cardTitle: TextView
val mediaCoverContainers =
@@ -82,6 +84,13 @@
if (updatedView) {
mediaAppIcons = mediaCoverContainers.map { it.requireViewById(R.id.media_rec_app_icon) }
cardTitle = itemView.requireViewById(R.id.media_rec_title)
+ mediaProgressBars =
+ mediaCoverContainers.map {
+ it.requireViewById<SeekBar?>(R.id.media_progress_bar).apply {
+ // Media playback is in the direction of tape, not time, so it stays LTR
+ layoutDirection = View.LAYOUT_DIRECTION_LTR
+ }
+ }
} else {
cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
index 1df42c6..dc7a4f1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
@@ -41,10 +41,12 @@
val recommendations: List<SmartspaceAction>,
/** Intent for the user's initiated dismissal. */
val dismissIntent: Intent?,
- /** The timestamp in milliseconds that headphone is connected. */
+ /** The timestamp in milliseconds that the card was generated */
val headphoneConnectionTimeMillis: Long,
/** Instance ID for [MediaUiEventLogger] */
- val instanceId: InstanceId
+ val instanceId: InstanceId,
+ /** The timestamp in milliseconds indicating when the card should be removed */
+ val expiryTimeMs: Long,
) {
/**
* Indicates if all the data is valid.
@@ -86,5 +88,12 @@
}
}
+/** Key for extras [SmartspaceMediaData.cardAction] indicating why the card was sent */
+const val EXTRA_KEY_TRIGGER_SOURCE = "MEDIA_RECOMMENDATION_TRIGGER_SOURCE"
+/** Value for [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent on headphone connection */
+const val EXTRA_VALUE_TRIGGER_HEADPHONE = "HEADPHONE_CONNECTION"
+/** Value for key [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent as a regular update */
+const val EXTRA_VALUE_TRIGGER_PERIODIC = "PERIODIC_TRIGGER"
+
const val NUM_REQUIRED_RECOMMENDATIONS = 3
private val TAG = SmartspaceMediaData::class.simpleName!!
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
index cf71d67..27f7b97 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
@@ -24,6 +24,7 @@
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.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
@@ -66,7 +67,8 @@
private val lockscreenUserManager: NotificationLockscreenUserManager,
@Main private val executor: Executor,
private val systemClock: SystemClock,
- private val logger: MediaUiEventLogger
+ private val logger: MediaUiEventLogger,
+ private val mediaFlags: MediaFlags,
) : MediaDataManager.Listener {
private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
internal val listeners: Set<MediaDataManager.Listener>
@@ -121,7 +123,9 @@
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
- if (!data.isActive) {
+ // With persistent recommendation card, we could get a background update while inactive
+ // Otherwise, consider it an invalid update
+ if (!data.isActive && !mediaFlags.isPersistentSsCardEnabled()) {
Log.d(TAG, "Inactive recommendation data. Skip triggering.")
return
}
@@ -141,7 +145,7 @@
}
}
- val shouldReactivate = !hasActiveMedia() && hasAnyMedia()
+ val shouldReactivate = !hasActiveMedia() && hasAnyMedia() && data.isActive
if (timeSinceActive < smartspaceMaxAgeMillis) {
// It could happen there are existing active media resume cards, then we don't need to
@@ -169,7 +173,7 @@
)
}
}
- } else {
+ } else if (data.isActive) {
// Mark to prioritize Smartspace card if no recent media.
shouldPrioritizeMutable = true
}
@@ -252,7 +256,7 @@
if (dismissIntent == null) {
Log.w(
TAG,
- "Cannot create dismiss action click action: " + "extras missing dismiss_intent."
+ "Cannot create dismiss action click action: extras missing dismiss_intent."
)
} else if (
dismissIntent.getComponent() != null &&
@@ -264,15 +268,21 @@
} else {
broadcastSender.sendBroadcast(dismissIntent)
}
- smartspaceMediaData =
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = smartspaceMediaData.targetId,
- instanceId = smartspaceMediaData.instanceId
+
+ if (mediaFlags.isPersistentSsCardEnabled()) {
+ smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+ mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId)
+ } else {
+ smartspaceMediaData =
+ EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+ targetId = smartspaceMediaData.targetId,
+ instanceId = smartspaceMediaData.instanceId,
+ )
+ mediaDataManager.dismissSmartspaceRecommendation(
+ smartspaceMediaData.targetId,
+ delay = 0L,
)
- mediaDataManager.dismissSmartspaceRecommendation(
- smartspaceMediaData.targetId,
- delay = 0L
- )
+ }
}
}
@@ -283,8 +293,15 @@
(smartspaceMediaData.isValid() || reactivatedKey != null))
/** Are there any media entries we should display? */
- fun hasAnyMediaOrRecommendation() =
- userEntries.isNotEmpty() || (smartspaceMediaData.isActive && smartspaceMediaData.isValid())
+ fun hasAnyMediaOrRecommendation(): Boolean {
+ val hasSmartspace =
+ if (mediaFlags.isPersistentSsCardEnabled()) {
+ smartspaceMediaData.isValid()
+ } else {
+ smartspaceMediaData.isActive && smartspaceMediaData.isValid()
+ }
+ return userEntries.isNotEmpty() || hasSmartspace
+ }
/** Are there any media notifications active (excluding the recommendation)? */
fun hasActiveMedia() = userEntries.any { it.value.active }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
index b11f628..0a94803 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
@@ -49,7 +49,6 @@
import android.text.TextUtils
import android.util.Log
import androidx.media.utils.MediaConstants
-import com.android.internal.annotations.VisibleForTesting
import com.android.internal.logging.InstanceId
import com.android.systemui.Dumpable
import com.android.systemui.R
@@ -63,10 +62,13 @@
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.EXTRA_KEY_TRIGGER_SOURCE
+import com.android.systemui.media.controls.models.recommendation.EXTRA_VALUE_TRIGGER_PERIODIC
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.MediaDataUtils
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.plugins.ActivityStarter
@@ -118,7 +120,6 @@
appUid = Process.INVALID_UID
)
-@VisibleForTesting
internal val EMPTY_SMARTSPACE_MEDIA_DATA =
SmartspaceMediaData(
targetId = "INVALID",
@@ -128,7 +129,8 @@
recommendations = emptyList(),
dismissIntent = null,
headphoneConnectionTimeMillis = 0,
- instanceId = InstanceId.fakeInstanceId(-1)
+ instanceId = InstanceId.fakeInstanceId(-1),
+ expiryTimeMs = 0,
)
fun isMediaNotification(sbn: StatusBarNotification): Boolean {
@@ -547,6 +549,11 @@
if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
onMediaDataLoaded(key, key, it)
}
+
+ if (key == smartspaceMediaData.targetId) {
+ if (DEBUG) Log.d(TAG, "smartspace card expired")
+ dismissSmartspaceRecommendation(key, delay = 0L)
+ }
}
/** Called when the player's [PlaybackState] has been updated with new actions and/or state */
@@ -604,8 +611,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 removed by the user. This will remove
+ * the recommendation card entirely from the carousel.
*/
fun dismissSmartspaceRecommendation(key: String, delay: Long) {
if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
@@ -627,6 +634,23 @@
)
}
+ /** Called when the recommendation card should no longer be visible in QQS or lockscreen */
+ fun setRecommendationInactive(key: String) {
+ if (!mediaFlags.isPersistentSsCardEnabled()) {
+ Log.e(TAG, "Only persistent recommendation can be inactive!")
+ return
+ }
+ if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive")
+
+ if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
+ // If this doesn't match, or we've already invalidated the data, no action needed
+ return
+ }
+
+ smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+ notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
+ }
+
private fun loadMediaDataInBgForResumption(
userId: Int,
desc: MediaDescription,
@@ -667,6 +691,11 @@
MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT &&
mediaFlags.isExplicitIndicatorEnabled()
+ val progress =
+ if (mediaFlags.isResumeProgressEnabled()) {
+ MediaDataUtils.getDescriptionProgress(desc.extras)
+ } else null
+
val mediaAction = getResumeMediaAction(resumeAction)
val lastActive = systemClock.elapsedRealtime()
foregroundExecutor.execute {
@@ -697,6 +726,7 @@
instanceId = instanceId,
appUid = appUid,
isExplicit = isExplicit,
+ resumeProgress = progress,
)
)
}
@@ -1258,12 +1288,25 @@
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
+ if (mediaFlags.isPersistentSsCardEnabled()) {
+ // Smartspace uses this signal to hide the card (e.g. when it expires or user
+ // disconnects headphones), so treat as setting inactive when flag is on
+ smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+ notifySmartspaceMediaDataLoaded(
+ smartspaceMediaData.targetId,
+ smartspaceMediaData,
)
- notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
+ } else {
+ smartspaceMediaData =
+ EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+ targetId = smartspaceMediaData.targetId,
+ instanceId = smartspaceMediaData.instanceId,
+ )
+ notifySmartspaceMediaDataRemoved(
+ smartspaceMediaData.targetId,
+ immediately = false,
+ )
+ }
}
1 -> {
val newMediaTarget = mediaTargets.get(0)
@@ -1272,7 +1315,7 @@
return
}
if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
- smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true)
+ smartspaceMediaData = toSmartspaceMediaData(newMediaTarget)
notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
}
else -> {
@@ -1281,7 +1324,7 @@
Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
notifySmartspaceMediaDataRemoved(
smartspaceMediaData.targetId,
- false /* immediately */
+ immediately = false,
)
smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
}
@@ -1487,21 +1530,28 @@
}
/**
- * Converts the pass-in SmartspaceTarget to SmartspaceMediaData with the pass-in active status.
+ * Converts the pass-in SmartspaceTarget to SmartspaceMediaData
*
* @return An empty SmartspaceMediaData with the valid target Id is returned if the
* SmartspaceTarget's data is invalid.
*/
- private fun toSmartspaceMediaData(
- target: SmartspaceTarget,
- isActive: Boolean
- ): SmartspaceMediaData {
+ private fun toSmartspaceMediaData(target: SmartspaceTarget): 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?
}
+
+ val isActive =
+ when {
+ !mediaFlags.isPersistentSsCardEnabled() -> true
+ target.baseAction == null -> true
+ else ->
+ target.baseAction.extras.getString(EXTRA_KEY_TRIGGER_SOURCE) !=
+ EXTRA_VALUE_TRIGGER_PERIODIC
+ }
+
packageName(target)?.let {
return SmartspaceMediaData(
targetId = target.smartspaceTargetId,
@@ -1511,7 +1561,8 @@
recommendations = target.iconGrid,
dismissIntent = dismissIntent,
headphoneConnectionTimeMillis = target.creationTimeMillis,
- instanceId = logger.getNewInstanceId()
+ instanceId = logger.getNewInstanceId(),
+ expiryTimeMs = target.expiryTimeMillis,
)
}
return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
@@ -1519,7 +1570,8 @@
isActive = isActive,
dismissIntent = dismissIntent,
headphoneConnectionTimeMillis = target.creationTimeMillis,
- instanceId = logger.getNewInstanceId()
+ instanceId = logger.getNewInstanceId(),
+ expiryTimeMs = target.expiryTimeMillis,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
index a898b00..aa46b14 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
@@ -23,7 +23,9 @@
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.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -49,10 +51,12 @@
@Main private val mainExecutor: DelayableExecutor,
private val logger: MediaTimeoutLogger,
statusBarStateController: SysuiStatusBarStateController,
- private val systemClock: SystemClock
+ private val systemClock: SystemClock,
+ private val mediaFlags: MediaFlags,
) : MediaDataManager.Listener {
private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
+ private val recommendationListeners: MutableMap<String, RecommendationListener> = mutableMapOf()
/**
* Callback representing that a media object is now expired:
@@ -93,6 +97,16 @@
listener.doTimeout()
}
}
+
+ recommendationListeners.forEach { (key, listener) ->
+ if (
+ listener.cancellation != null &&
+ listener.expiration <= systemClock.currentTimeMillis()
+ ) {
+ logger.logTimeoutCancelled(key, "Timed out while dozing")
+ listener.doTimeout()
+ }
+ }
}
}
}
@@ -155,6 +169,30 @@
mediaListeners.remove(key)?.destroy()
}
+ override fun onSmartspaceMediaDataLoaded(
+ key: String,
+ data: SmartspaceMediaData,
+ shouldPrioritize: Boolean
+ ) {
+ if (!mediaFlags.isPersistentSsCardEnabled()) return
+
+ // First check if we already have a listener
+ recommendationListeners.get(key)?.let {
+ if (!it.destroyed) {
+ it.recommendationData = data
+ return
+ }
+ }
+
+ // Otherwise, create a new one
+ recommendationListeners[key] = RecommendationListener(key, data)
+ }
+
+ override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+ if (!mediaFlags.isPersistentSsCardEnabled()) return
+ recommendationListeners.remove(key)?.destroy()
+ }
+
fun isTimedOut(key: String): Boolean {
return mediaListeners[key]?.timedOut ?: false
}
@@ -335,4 +373,53 @@
}
return true
}
+
+ /** Listens to changes in recommendation card data and schedules a timeout for its expiration */
+ private inner class RecommendationListener(var key: String, data: SmartspaceMediaData) {
+ private var timedOut = false
+ var destroyed = false
+ var expiration = Long.MAX_VALUE
+ private set
+ var cancellation: Runnable? = null
+ private set
+
+ var recommendationData: SmartspaceMediaData = data
+ set(value) {
+ destroyed = false
+ field = value
+ processUpdate()
+ }
+
+ init {
+ recommendationData = data
+ }
+
+ fun destroy() {
+ cancellation?.run()
+ cancellation = null
+ destroyed = true
+ }
+
+ private fun processUpdate() {
+ if (recommendationData.expiryTimeMs != expiration) {
+ // The expiry time changed - cancel and reschedule
+ val timeout =
+ recommendationData.expiryTimeMs -
+ recommendationData.headphoneConnectionTimeMillis
+ logger.logRecommendationTimeoutScheduled(key, timeout)
+ cancellation?.run()
+ cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout)
+ expiration = recommendationData.expiryTimeMs
+ }
+ }
+
+ fun doTimeout() {
+ cancellation?.run()
+ cancellation = null
+ logger.logTimeout(key)
+ timedOut = true
+ expiration = Long.MAX_VALUE
+ timeoutCallback(key, timedOut)
+ }
+ }
}
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
index 8f3f054..f731dc0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt
@@ -107,6 +107,17 @@
str1 = key
str2 = reason
},
- { "media timeout cancelled for $str1, reason: $str2" }
+ { "timeout cancelled for $str1, reason: $str2" }
+ )
+
+ fun logRecommendationTimeoutScheduled(key: String, timeout: Long) =
+ buffer.log(
+ TAG,
+ LogLevel.VERBOSE,
+ {
+ str1 = key
+ long1 = timeout
+ },
+ { "recommendation timeout scheduled for $str1 in $long1 ms" }
)
}
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
index b2ad155..fac1d5e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
@@ -368,7 +368,7 @@
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
- debugLogger.logRecommendationLoaded(key)
+ debugLogger.logRecommendationLoaded(key, data.isActive)
// Log the case where the hidden media carousel with the existed inactive resume
// media is shown by the Smartspace signal.
if (data.isActive) {
@@ -442,7 +442,12 @@
logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
}
} else {
- onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
+ if (!mediaFlags.isPersistentSsCardEnabled()) {
+ // Handle update to inactive as a removal
+ onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
+ } else {
+ addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
+ }
}
}
@@ -633,7 +638,19 @@
) =
traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
- if (MediaPlayerData.getMediaPlayer(key) != null) {
+ MediaPlayerData.getMediaPlayer(key)?.let {
+ if (mediaFlags.isPersistentSsCardEnabled()) {
+ // The card exists, but could have changed active state, so update for sorting
+ MediaPlayerData.addMediaRecommendation(
+ key,
+ data,
+ it,
+ shouldPrioritize,
+ systemClock,
+ debugLogger,
+ update = true,
+ )
+ }
Log.w(TAG, "Skip adding smartspace target in carousel")
return
}
@@ -672,7 +689,7 @@
newRecs,
shouldPrioritize,
systemClock,
- debugLogger
+ debugLogger,
)
updatePlayerToState(newRecs, noAnimation = true)
reorderAllPlayers(curVisibleMediaKey)
@@ -1225,17 +1242,18 @@
player: MediaControlPanel,
shouldPrioritize: Boolean,
clock: SystemClock,
- debugLogger: MediaCarouselControllerLogger? = null
+ debugLogger: MediaCarouselControllerLogger? = null,
+ update: Boolean = false
) {
shouldPrioritizeSs = shouldPrioritize
val removedPlayer = removeMediaPlayer(key)
- if (removedPlayer != null && removedPlayer != player) {
+ if (!update && removedPlayer != null && removedPlayer != player) {
debugLogger?.logPotentialMemoryLeak(key)
}
val sortKey =
MediaSortKey(
isSsMediaRec = true,
- EMPTY.copy(isPlaying = false),
+ EMPTY.copy(active = data.isActive, isPlaying = false),
key,
clock.currentTimeMillis(),
isSsReactivated = true
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
index eed1bd7..35bda15 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
@@ -48,8 +48,16 @@
fun logMediaRemoved(key: String) =
buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "removing player $str1" })
- fun logRecommendationLoaded(key: String) =
- buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add recommendation $str1" })
+ fun logRecommendationLoaded(key: String, isActive: Boolean) =
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = key
+ bool1 = isActive
+ },
+ { "add recommendation $str1, active $bool1" }
+ )
fun logRecommendationRemoved(key: String, immediately: Boolean) =
buffer.log(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index 9250a58..0b4b668a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -56,6 +56,7 @@
import android.view.animation.Interpolator;
import android.widget.ImageButton;
import android.widget.ImageView;
+import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
@@ -115,8 +116,6 @@
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.time.SystemClock;
-import dagger.Lazy;
-
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@@ -124,6 +123,7 @@
import javax.inject.Inject;
+import dagger.Lazy;
import kotlin.Triple;
import kotlin.Unit;
@@ -523,8 +523,13 @@
}
// Seek Bar
- final MediaController controller = getController();
- mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
+ if (data.getResumption() && data.getResumeProgress() != null) {
+ double progress = data.getResumeProgress();
+ mSeekBarViewModel.updateStaticProgress(progress);
+ } else {
+ final MediaController controller = getController();
+ mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
+ }
// Show the broadcast dialog button only when the le audio is enabled.
mShowBroadcastDialogButton =
@@ -729,9 +734,14 @@
contentDescription =
mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText();
} else if (data != null) {
- contentDescription = mContext.getString(
- R.string.controls_media_smartspace_rec_description,
- data.getAppName(mContext));
+ if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+ contentDescription = mContext.getString(
+ R.string.controls_media_smartspace_rec_header);
+ } else {
+ contentDescription = mContext.getString(
+ R.string.controls_media_smartspace_rec_description,
+ data.getAppName(mContext));
+ }
} else {
contentDescription = null;
}
@@ -1362,6 +1372,24 @@
hasSubtitle |= !TextUtils.isEmpty(subtitle);
TextView subtitleView = mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
subtitleView.setText(subtitle);
+
+ // Set up progress bar
+ if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+ SeekBar mediaProgressBar =
+ mRecommendationViewHolder.getMediaProgressBars().get(itemIndex);
+ TextView mediaSubtitle =
+ mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
+ // show progress bar if the recommended album is played.
+ Double progress = MediaDataUtils.getDescriptionProgress(recommendation.getExtras());
+ if (progress == null || progress <= 0.0) {
+ mediaProgressBar.setVisibility(View.GONE);
+ mediaSubtitle.setVisibility(View.VISIBLE);
+ } else {
+ mediaProgressBar.setProgress((int) (progress * 100));
+ mediaProgressBar.setVisibility(View.VISIBLE);
+ mediaSubtitle.setVisibility(View.GONE);
+ }
+ }
}
mSmartspaceMediaItemsCount = NUM_REQUIRED_RECOMMENDATIONS;
@@ -1436,6 +1464,12 @@
(title) -> title.setTextColor(textPrimaryColor));
mRecommendationViewHolder.getMediaSubtitles().forEach(
(subtitle) -> subtitle.setTextColor(textSecondaryColor));
+ if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+ mRecommendationViewHolder.getMediaProgressBars().forEach(
+ (progressBar) -> progressBar.setProgressTintList(
+ ColorStateList.valueOf(textPrimaryColor))
+ );
+ }
mRecommendationViewHolder.getGutsViewHolder().setColors(colorScheme);
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
index bcfceaa..85282a1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
@@ -19,8 +19,12 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.os.Bundle;
import android.text.TextUtils;
+import androidx.core.math.MathUtils;
+import androidx.media.utils.MediaConstants;
+
/**
* Utility class with common methods for media controls
*/
@@ -50,4 +54,35 @@
: unknownName);
return applicationName;
}
+
+ /**
+ * Check the bundle for extras indicating the progress percentage
+ *
+ * @param extras
+ * @return the progress value between 0-1 inclusive if prsent, otherwise null
+ */
+ public static Double getDescriptionProgress(Bundle extras) {
+ if (!extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS)) {
+ return null;
+ }
+
+ int status = extras.getInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS);
+ switch (status) {
+ case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED:
+ return 0.0;
+ case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED:
+ return 1.0;
+ case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED: {
+ if (extras
+ .containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE)) {
+ double percent = extras
+ .getDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE);
+ return MathUtils.clamp(percent, 0.0, 1.0);
+ } else {
+ return 0.5;
+ }
+ }
+ }
+ return null;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 81efa36..c3fa76e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -55,4 +55,10 @@
/** Check whether we show the updated recommendation card. */
fun isRecommendationCardUpdateEnabled() =
featureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)
+
+ /** Check whether to get progress information for resume players */
+ fun isResumeProgressEnabled() = featureFlags.isEnabled(Flags.MEDIA_RESUME_PROGRESS)
+
+ /** If true, do not automatically dismiss the recommendation card */
+ fun isPersistentSsCardEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_RECOMMENDATIONS)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index f1acae8..997370b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -34,7 +34,7 @@
init {
setupShader(RippleShader.RippleShape.CIRCLE)
- setRippleFill(true)
+ setupRippleFadeParams()
setSparkleStrength(0f)
isStarted = false
}
@@ -72,7 +72,7 @@
animator.removeAllUpdateListeners()
// Only show the outline as ripple expands and disappears when animation ends.
- setRippleFill(false)
+ removeRippleFill()
val startingPercentage = calculateStartingPercentage(newHeight)
animator.duration = EXPAND_TO_FULL_DURATION
@@ -103,6 +103,32 @@
return 1 - remainingPercentage
}
+ private fun setupRippleFadeParams() {
+ with(rippleShader) {
+ // No fade out for the base ring.
+ baseRingFadeParams.fadeOutStart = 1f
+ baseRingFadeParams.fadeOutEnd = 1f
+
+ // No fade in and outs for the center fill, as we always draw it.
+ centerFillFadeParams.fadeInStart = 0f
+ centerFillFadeParams.fadeInEnd = 0f
+ centerFillFadeParams.fadeOutStart = 1f
+ centerFillFadeParams.fadeOutEnd = 1f
+ }
+ }
+
+ private fun removeRippleFill() {
+ with(rippleShader) {
+ baseRingFadeParams.fadeOutStart = RippleShader.DEFAULT_BASE_RING_FADE_OUT_START
+ baseRingFadeParams.fadeOutEnd = RippleShader.DEFAULT_FADE_OUT_END
+
+ centerFillFadeParams.fadeInStart = RippleShader.DEFAULT_FADE_IN_START
+ centerFillFadeParams.fadeInEnd = RippleShader.DEFAULT_CENTER_FILL_FADE_IN_END
+ centerFillFadeParams.fadeOutStart = RippleShader.DEFAULT_CENTER_FILL_FADE_OUT_START
+ centerFillFadeParams.fadeOutEnd = RippleShader.DEFAULT_CENTER_FILL_FADE_OUT_END
+ }
+ }
+
companion object {
const val DEFAULT_DURATION = 333L
const val EXPAND_TO_FULL_DURATION = 1000L
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 1f27582..537dbb9 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
@@ -136,9 +136,9 @@
),
) {
override fun isValidNextState(nextState: ChipStateSender): Boolean {
- return nextState == FAR_FROM_RECEIVER ||
- nextState == ALMOST_CLOSE_TO_START_CAST ||
- nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED
+ // Since _SUCCEEDED is the end of a transfer sequence, we should be able to move to any
+ // state that represents the beginning of a new sequence.
+ return stateIsStartOfSequence(nextState)
}
},
@@ -158,9 +158,9 @@
),
) {
override fun isValidNextState(nextState: ChipStateSender): Boolean {
- return nextState == FAR_FROM_RECEIVER ||
- nextState == ALMOST_CLOSE_TO_END_CAST ||
- nextState == TRANSFER_TO_RECEIVER_TRIGGERED
+ // Since _SUCCEEDED is the end of a transfer sequence, we should be able to move to any
+ // state that represents the beginning of a new sequence.
+ return stateIsStartOfSequence(nextState)
}
},
@@ -173,9 +173,9 @@
endItem = SenderEndItem.Error,
) {
override fun isValidNextState(nextState: ChipStateSender): Boolean {
- return nextState == FAR_FROM_RECEIVER ||
- nextState == ALMOST_CLOSE_TO_START_CAST ||
- nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED
+ // Since _FAILED is the end of a transfer sequence, we should be able to move to any
+ // state that represents the beginning of a new sequence.
+ return stateIsStartOfSequence(nextState)
}
},
@@ -188,9 +188,9 @@
endItem = SenderEndItem.Error,
) {
override fun isValidNextState(nextState: ChipStateSender): Boolean {
- return nextState == FAR_FROM_RECEIVER ||
- nextState == ALMOST_CLOSE_TO_END_CAST ||
- nextState == TRANSFER_TO_RECEIVER_TRIGGERED
+ // Since _FAILED is the end of a transfer sequence, we should be able to move to any
+ // state that represents the beginning of a new sequence.
+ return stateIsStartOfSequence(nextState)
}
},
@@ -210,9 +210,9 @@
}
override fun isValidNextState(nextState: ChipStateSender): Boolean {
- return nextState == FAR_FROM_RECEIVER ||
- nextState.transferStatus == TransferStatus.NOT_STARTED ||
- nextState.transferStatus == TransferStatus.IN_PROGRESS
+ // When far away, we can go to any state that represents the start of a transfer
+ // sequence.
+ return stateIsStartOfSequence(nextState)
}
};
@@ -227,6 +227,20 @@
return Text.Loaded(context.getString(stringResId!!, otherDeviceName))
}
+ /**
+ * Returns true if moving from this state to [nextState] is a valid transition.
+ *
+ * In general, we expect a media transfer go to through a sequence of states:
+ * For push-to-receiver:
+ * - ALMOST_CLOSE_TO_START_CAST => TRANSFER_TO_RECEIVER_TRIGGERED =>
+ * TRANSFER_TO_RECEIVER_(SUCCEEDED|FAILED)
+ * - ALMOST_CLOSE_TO_END_CAST => TRANSFER_TO_THIS_DEVICE_TRIGGERED =>
+ * TRANSFER_TO_THIS_DEVICE_(SUCCEEDED|FAILED)
+ *
+ * This method should validate that the states go through approximately that sequence.
+ *
+ * See b/221265848 for more details.
+ */
abstract fun isValidNextState(nextState: ChipStateSender): Boolean
companion object {
@@ -276,6 +290,18 @@
return currentState.isValidNextState(desiredState)
}
+
+ /**
+ * Returns true if [state] represents a state at the beginning of a sequence and false
+ * otherwise.
+ */
+ private fun stateIsStartOfSequence(state: ChipStateSender): Boolean {
+ return state == FAR_FROM_RECEIVER ||
+ state.transferStatus == TransferStatus.NOT_STARTED ||
+ // It's possible to skip the NOT_STARTED phase and go immediately into the
+ // IN_PROGRESS phase.
+ state.transferStatus == TransferStatus.IN_PROGRESS
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
index e665d83..1678c6e 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
@@ -24,6 +24,7 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.media.MediaProjectionAppSelectorActivity
import com.android.systemui.media.MediaProjectionAppSelectorActivity.Companion.EXTRA_HOST_APP_USER_HANDLE
+import com.android.systemui.media.MediaProjectionPermissionActivity
import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerLabelLoader
import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader
import com.android.systemui.mediaprojection.appselector.data.AppIconLoader
@@ -45,10 +46,10 @@
import dagger.Subcomponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
-import javax.inject.Qualifier
-import javax.inject.Scope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
+import javax.inject.Qualifier
+import javax.inject.Scope
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class MediaProjectionAppSelector
@@ -67,6 +68,11 @@
fun provideMediaProjectionAppSelectorActivity(
activity: MediaProjectionAppSelectorActivity
): Activity
+
+ @Binds
+ @IntoMap
+ @ClassKey(MediaProjectionPermissionActivity::class)
+ fun bindsMediaProjectionPermissionActivity(impl: MediaProjectionPermissionActivity): Activity
}
/**
@@ -149,6 +155,7 @@
val controller: MediaProjectionAppSelectorController
val recentsViewController: MediaProjectionRecentsViewController
+ val emptyStateProvider: MediaProjectionBlockerEmptyStateProvider
@get:HostUserHandle val hostUserHandle: UserHandle
@get:PersonalProfile val personalProfileUserHandle: UserHandle
diff --git a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
index 1324d2c..c4a1ed4 100644
--- a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
@@ -19,7 +19,6 @@
import android.view.WindowManagerGlobal
import com.android.app.motiontool.DdmHandleMotionTool
import com.android.app.motiontool.MotionToolManager
-import com.android.app.viewcapture.ViewCapture
import com.android.systemui.CoreStartable
import dagger.Binds
import dagger.Module
@@ -38,17 +37,12 @@
}
@Provides
- fun provideMotionToolManager(
- viewCapture: ViewCapture,
- windowManagerGlobal: WindowManagerGlobal
- ): MotionToolManager {
- return MotionToolManager.getInstance(viewCapture, windowManagerGlobal)
+ fun provideMotionToolManager(windowManagerGlobal: WindowManagerGlobal): MotionToolManager {
+ return MotionToolManager.getInstance(windowManagerGlobal)
}
@Provides
fun provideWindowManagerGlobal(): WindowManagerGlobal = WindowManagerGlobal.getInstance()
-
- @Provides fun provideViewCapture(): ViewCapture = ViewCapture.getInstance()
}
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
index 2822435..f335733 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
@@ -1,14 +1,20 @@
package com.android.systemui.navigationbar.gestural
import android.content.Context
+import android.content.res.Configuration
+import android.content.res.TypedArray
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
+import android.util.MathUtils.min
+import android.util.TypedValue
import android.view.View
+import androidx.appcompat.view.ContextThemeWrapper
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
+import com.android.internal.R.style.Theme_DeviceDefault
import com.android.internal.util.LatencyTracker
import com.android.settingslib.Utils
import com.android.systemui.navigationbar.gestural.BackPanelController.DelayedOnAnimationEndListener
@@ -16,7 +22,10 @@
private const val TAG = "BackPanel"
private const val DEBUG = false
-class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : View(context) {
+class BackPanel(
+ context: Context,
+ private val latencyTracker: LatencyTracker
+) : View(context) {
var arrowsPointLeft = false
set(value) {
@@ -45,52 +54,54 @@
/**
* The length of the arrow measured horizontally. Used for animating [arrowPath]
*/
- private var arrowLength = AnimatedFloat("arrowLength", SpringForce())
+ private var arrowLength = AnimatedFloat(
+ name = "arrowLength",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS
+ )
/**
* The height of the arrow measured vertically from its center to its top (i.e. half the total
* height). Used for animating [arrowPath]
*/
- private var arrowHeight = AnimatedFloat("arrowHeight", SpringForce())
-
- private val backgroundWidth = AnimatedFloat(
- name = "backgroundWidth",
- SpringForce().apply {
- stiffness = 600f
- dampingRatio = 0.65f
- }
+ var arrowHeight = AnimatedFloat(
+ name = "arrowHeight",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ROTATION_DEGREES
)
- private val backgroundHeight = AnimatedFloat(
- name = "backgroundHeight",
- SpringForce().apply {
- stiffness = 600f
- dampingRatio = 0.65f
- }
+ val backgroundWidth = AnimatedFloat(
+ name = "backgroundWidth",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
+ minimumValue = 0f,
+ )
+
+ val backgroundHeight = AnimatedFloat(
+ name = "backgroundHeight",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
+ minimumValue = 0f,
)
/**
* Corners of the background closer to the edge of the screen (where the arrow appeared from).
* Used for animating [arrowBackgroundRect]
*/
- private val backgroundEdgeCornerRadius = AnimatedFloat(
- name = "backgroundEdgeCornerRadius",
- SpringForce().apply {
- stiffness = 400f
- dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
- }
- )
+ val backgroundEdgeCornerRadius = AnimatedFloat("backgroundEdgeCornerRadius")
/**
* Corners of the background further from the edge of the screens (toward the direction the
* arrow is being dragged). Used for animating [arrowBackgroundRect]
*/
- private val backgroundFarCornerRadius = AnimatedFloat(
- name = "backgroundDragCornerRadius",
- SpringForce().apply {
- stiffness = 2200f
- dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
- }
+ val backgroundFarCornerRadius = AnimatedFloat("backgroundFarCornerRadius")
+
+ var scale = AnimatedFloat(
+ name = "scale",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_SCALE,
+ minimumValue = 0f
+ )
+
+ val scalePivotX = AnimatedFloat(
+ name = "scalePivotX",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
+ minimumValue = backgroundWidth.pos / 2,
)
/**
@@ -98,34 +109,40 @@
* background's margin relative to the screen edge. The arrow will be centered within the
* background.
*/
- private var horizontalTranslation = AnimatedFloat("horizontalTranslation", SpringForce())
+ var horizontalTranslation = AnimatedFloat(name = "horizontalTranslation")
- private val currentAlpha: FloatPropertyCompat<BackPanel> =
- object : FloatPropertyCompat<BackPanel>("currentAlpha") {
- override fun setValue(panel: BackPanel, value: Float) {
- panel.alpha = value
- }
+ var arrowAlpha = AnimatedFloat(
+ name = "arrowAlpha",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
+ minimumValue = 0f,
+ maximumValue = 1f
+ )
- override fun getValue(panel: BackPanel): Float = panel.alpha
- }
+ val backgroundAlpha = AnimatedFloat(
+ name = "backgroundAlpha",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
+ minimumValue = 0f,
+ maximumValue = 1f
+ )
- private val alphaAnimation = SpringAnimation(this, currentAlpha)
- .setSpring(
- SpringForce()
- .setStiffness(60f)
- .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
- )
+ private val allAnimatedFloat = setOf(
+ arrowLength,
+ arrowHeight,
+ backgroundWidth,
+ backgroundEdgeCornerRadius,
+ backgroundFarCornerRadius,
+ scalePivotX,
+ scale,
+ horizontalTranslation,
+ arrowAlpha,
+ backgroundAlpha
+ )
/**
* Canvas vertical translation. How far up/down the arrow and background appear relative to the
* canvas.
*/
- private var verticalTranslation: AnimatedFloat = AnimatedFloat(
- name = "verticalTranslation",
- SpringForce().apply {
- stiffness = SpringForce.STIFFNESS_MEDIUM
- }
- )
+ var verticalTranslation = AnimatedFloat("verticalTranslation")
/**
* Use for drawing debug info. Can only be set if [DEBUG]=true
@@ -136,28 +153,67 @@
}
internal fun updateArrowPaint(arrowThickness: Float) {
- // Arrow constants
+
arrowPaint.strokeWidth = arrowThickness
- arrowPaint.color =
- Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
- arrowBackgroundPaint.color = Utils.getColorAccentDefaultColor(context)
+ val isDeviceInNightTheme = resources.configuration.uiMode and
+ Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
+
+ val colorControlActivated = ContextThemeWrapper(context, Theme_DeviceDefault)
+ .run {
+ val typedValue = TypedValue()
+ val a: TypedArray = obtainStyledAttributes(typedValue.data,
+ intArrayOf(android.R.attr.colorControlActivated))
+ val color = a.getColor(0, 0)
+ a.recycle()
+ color
+ }
+
+ val colorPrimary =
+ Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
+
+ arrowPaint.color = Utils.getColorAccentDefaultColor(context)
+
+ arrowBackgroundPaint.color = if (isDeviceInNightTheme) {
+ colorPrimary
+ } else {
+ colorControlActivated
+ }
}
- private inner class AnimatedFloat(name: String, springForce: SpringForce) {
+ inner class AnimatedFloat(
+ name: String,
+ private val minimumVisibleChange: Float? = null,
+ private val minimumValue: Float? = null,
+ private val maximumValue: Float? = null,
+ ) {
+
// The resting position when not stretched by a touch drag
private var restingPosition = 0f
// The current position as updated by the SpringAnimation
var pos = 0f
- set(v) {
+ private set(v) {
if (field != v) {
field = v
invalidate()
}
}
- val animation: SpringAnimation
+ private val animation: SpringAnimation
+ var spring: SpringForce
+ get() = animation.spring
+ set(value) {
+ animation.cancel()
+ animation.spring = value
+ }
+
+ val isRunning: Boolean
+ get() = animation.isRunning
+
+ fun addEndListener(listener: DelayedOnAnimationEndListener) {
+ animation.addEndListener(listener)
+ }
init {
val floatProp = object : FloatPropertyCompat<AnimatedFloat>(name) {
@@ -167,8 +223,12 @@
override fun getValue(animatedFloat: AnimatedFloat): Float = animatedFloat.pos
}
- animation = SpringAnimation(this, floatProp)
- animation.spring = springForce
+ animation = SpringAnimation(this, floatProp).apply {
+ spring = SpringForce()
+ this@AnimatedFloat.minimumValue?.let { setMinValue(it) }
+ this@AnimatedFloat.maximumValue?.let { setMaxValue(it) }
+ this@AnimatedFloat.minimumVisibleChange?.let { minimumVisibleChange = it }
+ }
}
fun snapTo(newPosition: Float) {
@@ -178,8 +238,24 @@
pos = newPosition
}
- fun stretchTo(stretchAmount: Float) {
- animation.animateToFinalPosition(restingPosition + stretchAmount)
+ fun snapToRestingPosition() {
+ snapTo(restingPosition)
+ }
+
+
+ fun stretchTo(
+ stretchAmount: Float,
+ startingVelocity: Float? = null,
+ springForce: SpringForce? = null
+ ) {
+ animation.apply {
+ startingVelocity?.let {
+ cancel()
+ setStartVelocity(it)
+ }
+ springForce?.let { spring = springForce }
+ animateToFinalPosition(restingPosition + stretchAmount)
+ }
}
/**
@@ -188,18 +264,23 @@
*
* The [restingPosition] will remain unchanged. Only the animation is updated.
*/
- fun stretchBy(finalPosition: Float, amount: Float) {
- val stretchedAmount = amount * (finalPosition - restingPosition)
+ fun stretchBy(finalPosition: Float?, amount: Float) {
+ val stretchedAmount = amount * ((finalPosition ?: 0f) - restingPosition)
animation.animateToFinalPosition(restingPosition + stretchedAmount)
}
- fun updateRestingPosition(pos: Float, animated: Boolean) {
+ fun updateRestingPosition(pos: Float?, animated: Boolean = true) {
+ if (pos == null) return
+
restingPosition = pos
- if (animated)
+ if (animated) {
animation.animateToFinalPosition(restingPosition)
- else
+ } else {
snapTo(restingPosition)
+ }
}
+
+ fun cancel() = animation.cancel()
}
init {
@@ -224,126 +305,203 @@
return arrowPath
}
- fun addEndListener(endListener: DelayedOnAnimationEndListener): Boolean {
- return if (alphaAnimation.isRunning) {
- alphaAnimation.addEndListener(endListener)
- true
- } else if (horizontalTranslation.animation.isRunning) {
- horizontalTranslation.animation.addEndListener(endListener)
+ fun addAnimationEndListener(
+ animatedFloat: AnimatedFloat,
+ endListener: DelayedOnAnimationEndListener
+ ): Boolean {
+ return if (animatedFloat.isRunning) {
+ animatedFloat.addEndListener(endListener)
true
} else {
- endListener.runNow()
+ endListener.run()
false
}
}
+ fun cancelAnimations() {
+ allAnimatedFloat.forEach { it.cancel() }
+ }
+
fun setStretch(
- horizontalTranslationStretchAmount: Float,
- arrowStretchAmount: Float,
- backgroundWidthStretchAmount: Float,
- fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens
+ horizontalTranslationStretchAmount: Float,
+ arrowStretchAmount: Float,
+ arrowAlphaStretchAmount: Float,
+ backgroundAlphaStretchAmount: Float,
+ backgroundWidthStretchAmount: Float,
+ backgroundHeightStretchAmount: Float,
+ edgeCornerStretchAmount: Float,
+ farCornerStretchAmount: Float,
+ fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens
) {
horizontalTranslation.stretchBy(
- finalPosition = fullyStretchedDimens.horizontalTranslation,
- amount = horizontalTranslationStretchAmount
+ finalPosition = fullyStretchedDimens.horizontalTranslation,
+ amount = horizontalTranslationStretchAmount
)
arrowLength.stretchBy(
- finalPosition = fullyStretchedDimens.arrowDimens.length,
- amount = arrowStretchAmount
+ finalPosition = fullyStretchedDimens.arrowDimens.length,
+ amount = arrowStretchAmount
)
arrowHeight.stretchBy(
- finalPosition = fullyStretchedDimens.arrowDimens.height,
- amount = arrowStretchAmount
+ finalPosition = fullyStretchedDimens.arrowDimens.height,
+ amount = arrowStretchAmount
+ )
+ arrowAlpha.stretchBy(
+ finalPosition = fullyStretchedDimens.arrowDimens.alpha,
+ amount = arrowAlphaStretchAmount
+ )
+ backgroundAlpha.stretchBy(
+ finalPosition = fullyStretchedDimens.backgroundDimens.alpha,
+ amount = backgroundAlphaStretchAmount
)
backgroundWidth.stretchBy(
- finalPosition = fullyStretchedDimens.backgroundDimens.width,
- amount = backgroundWidthStretchAmount
+ finalPosition = fullyStretchedDimens.backgroundDimens.width,
+ amount = backgroundWidthStretchAmount
+ )
+ backgroundHeight.stretchBy(
+ finalPosition = fullyStretchedDimens.backgroundDimens.height,
+ amount = backgroundHeightStretchAmount
+ )
+ backgroundEdgeCornerRadius.stretchBy(
+ finalPosition = fullyStretchedDimens.backgroundDimens.edgeCornerRadius,
+ amount = edgeCornerStretchAmount
+ )
+ backgroundFarCornerRadius.stretchBy(
+ finalPosition = fullyStretchedDimens.backgroundDimens.farCornerRadius,
+ amount = farCornerStretchAmount
)
}
+ fun popOffEdge(startingVelocity: Float) {
+ val heightStretchAmount = startingVelocity * 50
+ val widthStretchAmount = startingVelocity * 150
+ val scaleStretchAmount = startingVelocity * 0.8f
+ backgroundHeight.stretchTo(stretchAmount = 0f, startingVelocity = -heightStretchAmount)
+ backgroundWidth.stretchTo(stretchAmount = 0f, startingVelocity = widthStretchAmount)
+ scale.stretchTo(stretchAmount = 0f, startingVelocity = -scaleStretchAmount)
+ }
+
+ fun popScale(startingVelocity: Float) {
+ scalePivotX.snapTo(backgroundWidth.pos / 2)
+ scale.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity)
+ }
+
+ fun popArrowAlpha(startingVelocity: Float, springForce: SpringForce? = null) {
+ arrowAlpha.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity,
+ springForce = springForce)
+ }
+
fun resetStretch() {
- horizontalTranslation.stretchTo(0f)
- arrowLength.stretchTo(0f)
- arrowHeight.stretchTo(0f)
- backgroundWidth.stretchTo(0f)
- backgroundHeight.stretchTo(0f)
- backgroundEdgeCornerRadius.stretchTo(0f)
- backgroundFarCornerRadius.stretchTo(0f)
+ backgroundAlpha.snapTo(1f)
+ verticalTranslation.snapTo(0f)
+ scale.snapTo(1f)
+
+ horizontalTranslation.snapToRestingPosition()
+ arrowLength.snapToRestingPosition()
+ arrowHeight.snapToRestingPosition()
+ arrowAlpha.snapToRestingPosition()
+ backgroundWidth.snapToRestingPosition()
+ backgroundHeight.snapToRestingPosition()
+ backgroundEdgeCornerRadius.snapToRestingPosition()
+ backgroundFarCornerRadius.snapToRestingPosition()
}
/**
* Updates resting arrow and background size not accounting for stretch
*/
internal fun setRestingDimens(
- restingParams: EdgePanelParams.BackIndicatorDimens,
- animate: Boolean
+ restingParams: EdgePanelParams.BackIndicatorDimens,
+ animate: Boolean = true
) {
- horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation, animate)
+ horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation)
+ scale.updateRestingPosition(restingParams.scale)
+ arrowAlpha.updateRestingPosition(restingParams.arrowDimens.alpha)
+ backgroundAlpha.updateRestingPosition(restingParams.backgroundDimens.alpha)
+
arrowLength.updateRestingPosition(restingParams.arrowDimens.length, animate)
arrowHeight.updateRestingPosition(restingParams.arrowDimens.height, animate)
+ scalePivotX.updateRestingPosition(restingParams.backgroundDimens.width, animate)
backgroundWidth.updateRestingPosition(restingParams.backgroundDimens.width, animate)
backgroundHeight.updateRestingPosition(restingParams.backgroundDimens.height, animate)
backgroundEdgeCornerRadius.updateRestingPosition(
- restingParams.backgroundDimens.edgeCornerRadius,
- animate
+ restingParams.backgroundDimens.edgeCornerRadius, animate
)
backgroundFarCornerRadius.updateRestingPosition(
- restingParams.backgroundDimens.farCornerRadius,
- animate
+ restingParams.backgroundDimens.farCornerRadius, animate
)
}
fun animateVertically(yPos: Float) = verticalTranslation.stretchTo(yPos)
- fun setArrowStiffness(arrowStiffness: Float, arrowDampingRatio: Float) {
- arrowLength.animation.spring.apply {
- stiffness = arrowStiffness
- dampingRatio = arrowDampingRatio
- }
- arrowHeight.animation.spring.apply {
- stiffness = arrowStiffness
- dampingRatio = arrowDampingRatio
- }
+ fun setSpring(
+ horizontalTranslation: SpringForce? = null,
+ verticalTranslation: SpringForce? = null,
+ scale: SpringForce? = null,
+ arrowLength: SpringForce? = null,
+ arrowHeight: SpringForce? = null,
+ arrowAlpha: SpringForce? = null,
+ backgroundAlpha: SpringForce? = null,
+ backgroundFarCornerRadius: SpringForce? = null,
+ backgroundEdgeCornerRadius: SpringForce? = null,
+ backgroundWidth: SpringForce? = null,
+ backgroundHeight: SpringForce? = null,
+ ) {
+ arrowLength?.let { this.arrowLength.spring = it }
+ arrowHeight?.let { this.arrowHeight.spring = it }
+ arrowAlpha?.let { this.arrowAlpha.spring = it }
+ backgroundAlpha?.let { this.backgroundAlpha.spring = it }
+ backgroundFarCornerRadius?.let { this.backgroundFarCornerRadius.spring = it }
+ backgroundEdgeCornerRadius?.let { this.backgroundEdgeCornerRadius.spring = it }
+ scale?.let { this.scale.spring = it }
+ backgroundWidth?.let { this.backgroundWidth.spring = it }
+ backgroundHeight?.let { this.backgroundHeight.spring = it }
+ horizontalTranslation?.let { this.horizontalTranslation.spring = it }
+ verticalTranslation?.let { this.verticalTranslation.spring = it }
}
override fun hasOverlappingRendering() = false
override fun onDraw(canvas: Canvas) {
- var edgeCorner = backgroundEdgeCornerRadius.pos
+ val edgeCorner = backgroundEdgeCornerRadius.pos
val farCorner = backgroundFarCornerRadius.pos
val halfHeight = backgroundHeight.pos / 2
+ val canvasWidth = width
+ val backgroundWidth = backgroundWidth.pos
+ val scalePivotX = scalePivotX.pos
canvas.save()
- if (!isLeftPanel) canvas.scale(-1f, 1f, width / 2.0f, 0f)
+ if (!isLeftPanel) canvas.scale(-1f, 1f, canvasWidth / 2.0f, 0f)
canvas.translate(
- horizontalTranslation.pos,
- height * 0.5f + verticalTranslation.pos
+ horizontalTranslation.pos,
+ height * 0.5f + verticalTranslation.pos
)
+ canvas.scale(scale.pos, scale.pos, scalePivotX, 0f)
+
val arrowBackground = arrowBackgroundRect.apply {
left = 0f
top = -halfHeight
- right = backgroundWidth.pos
+ right = backgroundWidth
bottom = halfHeight
}.toPathWithRoundCorners(
- topLeft = edgeCorner,
- bottomLeft = edgeCorner,
- topRight = farCorner,
- bottomRight = farCorner
+ topLeft = edgeCorner,
+ bottomLeft = edgeCorner,
+ topRight = farCorner,
+ bottomRight = farCorner
)
- canvas.drawPath(arrowBackground, arrowBackgroundPaint)
+ canvas.drawPath(arrowBackground,
+ arrowBackgroundPaint.apply { alpha = (255 * backgroundAlpha.pos).toInt() })
val dx = arrowLength.pos
val dy = arrowHeight.pos
// How far the arrow bounding box should be from the edge of the screen. Measured from
// either the tip or the back of the arrow, whichever is closer
- var arrowOffset = (backgroundWidth.pos - dx) / 2
+ val arrowOffset = (backgroundWidth - dx) / 2
canvas.translate(
- /* dx= */ arrowOffset,
- /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */
+ /* dx= */ arrowOffset,
+ /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */
)
val arrowPointsAwayFromEdge = !arrowsPointLeft.xor(isLeftPanel)
@@ -355,6 +513,8 @@
}
val arrowPath = calculateArrowPath(dx = dx, dy = dy)
+ val arrowPaint = arrowPaint
+ .apply { alpha = (255 * min(arrowAlpha.pos, backgroundAlpha.pos)).toInt() }
canvas.drawPath(arrowPath, arrowPaint)
canvas.restore()
@@ -372,26 +532,17 @@
}
private fun RectF.toPathWithRoundCorners(
- topLeft: Float = 0f,
- topRight: Float = 0f,
- bottomRight: Float = 0f,
- bottomLeft: Float = 0f
+ topLeft: Float = 0f,
+ topRight: Float = 0f,
+ bottomRight: Float = 0f,
+ bottomLeft: Float = 0f
): Path = Path().apply {
val corners = floatArrayOf(
- topLeft, topLeft,
- topRight, topRight,
- bottomRight, bottomRight,
- bottomLeft, bottomLeft
+ topLeft, topLeft,
+ topRight, topRight,
+ bottomRight, bottomRight,
+ bottomLeft, bottomLeft
)
addRoundRect(this@toPathWithRoundCorners, corners, Path.Direction.CW)
}
-
- fun cancelAlphaAnimations() {
- alphaAnimation.cancel()
- alpha = 1f
- }
-
- fun fadeOut() {
- alphaAnimation.animateToFinalPosition(0f)
- }
-}
+}
\ No newline at end of file
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 6e927b0..367d125 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -24,18 +24,16 @@
import android.os.SystemClock
import android.os.VibrationEffect
import android.util.Log
-import android.util.MathUtils.constrain
-import android.util.MathUtils.saturate
+import android.util.MathUtils
import android.view.Gravity
import android.view.MotionEvent
import android.view.VelocityTracker
-import android.view.View
import android.view.ViewConfiguration
import android.view.WindowManager
-import android.view.animation.DecelerateInterpolator
-import android.view.animation.PathInterpolator
+import androidx.annotation.VisibleForTesting
+import androidx.core.os.postDelayed
+import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation
-import androidx.dynamicanimation.animation.SpringForce
import com.android.internal.util.LatencyTracker
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.NavigationEdgeBackPlugin
@@ -50,58 +48,42 @@
import kotlin.math.sign
private const val TAG = "BackPanelController"
-private const val DEBUG = false
-
private const val ENABLE_FAILSAFE = true
-private const val FAILSAFE_DELAY_MS: Long = 350
+private const val PX_PER_SEC = 1000
+private const val PX_PER_MS = 1
-/**
- * The time required between the arrow-appears vibration effect and the back-committed vibration
- * effect. If the arrow is flung quickly, the phone only vibrates once. However, if the arrow is
- * held on the screen for a long time, it will vibrate a second time when the back gesture is
- * committed.
- */
-private const val GESTURE_DURATION_FOR_CLICK_MS = 400
+internal const val MIN_DURATION_ACTIVE_ANIMATION = 300L
+private const val MIN_DURATION_CANCELLED_ANIMATION = 200L
+private const val MIN_DURATION_COMMITTED_ANIMATION = 200L
+private const val MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION = 50L
+private const val MIN_DURATION_CONSIDERED_AS_FLING = 100L
-/**
- * The min duration arrow remains on screen during a fling event.
- */
-private const val FLING_MIN_APPEARANCE_DURATION = 235L
+private const val FAILSAFE_DELAY_MS = 350L
+private const val POP_ON_FLING_DELAY = 160L
-/**
- * The min duration arrow remains on screen during a fling event.
- */
-private const val MIN_FLING_VELOCITY = 3000
+internal val VIBRATE_ACTIVATED_EFFECT =
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
-/**
- * The amount of rubber banding we do for the vertical translation
- */
-private const val RUBBER_BAND_AMOUNT = 15
+internal val VIBRATE_DEACTIVATED_EFFECT =
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
-private const val ARROW_APPEAR_STIFFNESS = 600f
-private const val ARROW_APPEAR_DAMPING_RATIO = 0.4f
-private const val ARROW_DISAPPEAR_STIFFNESS = 1200f
-private const val ARROW_DISAPPEAR_DAMPING_RATIO = SpringForce.DAMPING_RATIO_NO_BOUNCY
+private const val DEBUG = false
-/**
- * The interpolator used to rubber band
- */
-private val RUBBER_BAND_INTERPOLATOR = PathInterpolator(1.0f / 5.0f, 1.0f, 1.0f, 1.0f)
-
-private val DECELERATE_INTERPOLATOR = DecelerateInterpolator()
-
-private val DECELERATE_INTERPOLATOR_SLOW = DecelerateInterpolator(0.7f)
-
-class BackPanelController private constructor(
- context: Context,
- private val windowManager: WindowManager,
- private val viewConfiguration: ViewConfiguration,
- @Main private val mainHandler: Handler,
- private val vibratorHelper: VibratorHelper,
- private val configurationController: ConfigurationController,
- latencyTracker: LatencyTracker
-) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
+class BackPanelController internal constructor(
+ context: Context,
+ private val windowManager: WindowManager,
+ private val viewConfiguration: ViewConfiguration,
+ @Main private val mainHandler: Handler,
+ private val vibratorHelper: VibratorHelper,
+ private val configurationController: ConfigurationController,
+ private val latencyTracker: LatencyTracker
+) : ViewController<BackPanel>(
+ BackPanel(
+ context,
+ latencyTracker
+ )
+), NavigationEdgeBackPlugin {
/**
* Injectable instance to create a new BackPanelController.
@@ -110,44 +92,44 @@
* BackPanelController, and we need to match EdgeBackGestureHandler's context.
*/
class Factory @Inject constructor(
- private val windowManager: WindowManager,
- private val viewConfiguration: ViewConfiguration,
- @Main private val mainHandler: Handler,
- private val vibratorHelper: VibratorHelper,
- private val configurationController: ConfigurationController,
- private val latencyTracker: LatencyTracker
+ private val windowManager: WindowManager,
+ private val viewConfiguration: ViewConfiguration,
+ @Main private val mainHandler: Handler,
+ private val vibratorHelper: VibratorHelper,
+ private val configurationController: ConfigurationController,
+ private val latencyTracker: LatencyTracker
) {
/** Construct a [BackPanelController]. */
fun create(context: Context): BackPanelController {
val backPanelController = BackPanelController(
- context,
- windowManager,
- viewConfiguration,
- mainHandler,
- vibratorHelper,
- configurationController,
- latencyTracker
+ context,
+ windowManager,
+ viewConfiguration,
+ mainHandler,
+ vibratorHelper,
+ configurationController,
+ latencyTracker
)
backPanelController.init()
return backPanelController
}
}
- private var params: EdgePanelParams = EdgePanelParams(resources)
- private var currentState: GestureState = GestureState.GONE
+ @VisibleForTesting
+ internal var params: EdgePanelParams = EdgePanelParams(resources)
+ @VisibleForTesting
+ internal var currentState: GestureState = GestureState.GONE
private var previousState: GestureState = GestureState.GONE
- // Phone should only vibrate the first time the arrow is activated
- private var hasHapticPlayed = false
-
// Screen attributes
private lateinit var layoutParams: WindowManager.LayoutParams
private val displaySize = Point()
private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
-
+ private var previousXTranslationOnActiveOffset = 0f
private var previousXTranslation = 0f
private var totalTouchDelta = 0f
+ private var touchDeltaStartX = 0f
private var velocityTracker: VelocityTracker? = null
set(value) {
if (field != value) field?.recycle()
@@ -161,8 +143,18 @@
// The x,y position of the first touch event
private var startX = 0f
private var startY = 0f
+ private var startIsLeft: Boolean? = null
- private var gestureStartTime = 0L
+ private var gestureSinceActionDown = 0L
+ private var gestureEntryTime = 0L
+ private var gestureActiveTime = 0L
+ private var gestureInactiveOrEntryTime = 0L
+ private var gestureArrowStrokeVisibleTime = 0L
+
+ private val elapsedTimeSinceActionDown
+ get() = SystemClock.uptimeMillis() - gestureSinceActionDown
+ private val elapsedTimeSinceEntry
+ get() = SystemClock.uptimeMillis() - gestureEntryTime
// Whether the current gesture has moved a sufficiently large amount,
// so that we can unambiguously start showing the ENTRY animation
@@ -170,7 +162,7 @@
private val failsafeRunnable = Runnable { onFailsafe() }
- private enum class GestureState {
+ internal enum class GestureState {
/* Arrow is off the screen and invisible */
GONE,
@@ -191,17 +183,6 @@
/* back action currently cancelling, arrow soon to be GONE */
CANCELLED;
-
- /**
- * @return true if the current state responds to touch move events in some way (e.g. by
- * stretching the back indicator)
- */
- fun isInteractive(): Boolean {
- return when (this) {
- ENTRY, ACTIVE, INACTIVE -> true
- GONE, FLUNG, COMMITTED, CANCELLED -> false
- }
- }
}
/**
@@ -209,50 +190,43 @@
* runnable is not called if the animation is cancelled
*/
inner class DelayedOnAnimationEndListener internal constructor(
- private val handler: Handler,
- private val runnable: Runnable,
- private val minDuration: Long
+ private val handler: Handler,
+ private val runnableDelay: Long,
+ val runnable: Runnable,
) : DynamicAnimation.OnAnimationEndListener {
+
override fun onAnimationEnd(
- animation: DynamicAnimation<*>,
- canceled: Boolean,
- value: Float,
- velocity: Float
+ animation: DynamicAnimation<*>,
+ canceled: Boolean,
+ value: Float,
+ velocity: Float
) {
animation.removeEndListener(this)
+
if (!canceled) {
- // Total elapsed time of the gesture and the animation
- val totalElapsedTime = SystemClock.uptimeMillis() - gestureStartTime
+
// The delay between finishing this animation and starting the runnable
- val delay = max(0, minDuration - totalElapsedTime)
+ val delay = max(0, runnableDelay - elapsedTimeSinceEntry)
+
handler.postDelayed(runnable, delay)
}
}
- internal fun runNow() {
- runnable.run()
- }
+ internal fun run() = runnable.run()
}
- private val setCommittedEndListener =
- DelayedOnAnimationEndListener(
- mainHandler,
- { updateArrowState(GestureState.COMMITTED) },
- minDuration = FLING_MIN_APPEARANCE_DURATION
- )
+ private val onEndSetCommittedStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) {
+ updateArrowState(GestureState.COMMITTED)
+ }
- private val setGoneEndListener =
- DelayedOnAnimationEndListener(
- mainHandler,
- {
+
+ private val onEndSetGoneStateListener =
+ DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
cancelFailsafe()
updateArrowState(GestureState.GONE)
- },
- minDuration = 0
- )
+ }
- // Vibration
- private var vibrationTime: Long = 0
+ private val playAnimationThenSetGoneOnAlphaEnd = Runnable { playAnimationThenSetGoneEnd() }
// Minimum of the screen's width or the predefined threshold
private var fullyStretchedThreshold = 0f
@@ -279,7 +253,7 @@
updateConfiguration()
updateArrowDirection(configurationController.isLayoutRtl)
updateArrowState(GestureState.GONE, force = true)
- updateRestingArrowDimens(animated = false, currentState)
+ updateRestingArrowDimens()
configurationController.addCallback(configurationListener)
}
@@ -296,22 +270,57 @@
velocityTracker!!.addMovement(event)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
- resetOnDown()
+ gestureSinceActionDown = SystemClock.uptimeMillis()
+ cancelAllPendingAnimations()
startX = event.x
startY = event.y
- gestureStartTime = SystemClock.uptimeMillis()
+
+ updateArrowState(GestureState.GONE)
+ updateYStartPosition(startY)
+
+ // reset animation properties
+ startIsLeft = mView.isLeftPanel
+ hasPassedDragSlop = false
+ mView.resetStretch()
}
MotionEvent.ACTION_MOVE -> {
- // only go to the ENTRY state after some minimum motion has occurred
if (dragSlopExceeded(event.x, startX)) {
handleMoveEvent(event)
}
}
MotionEvent.ACTION_UP -> {
- if (currentState == GestureState.ACTIVE) {
- updateArrowState(if (isFlung()) GestureState.FLUNG else GestureState.COMMITTED)
- } else if (currentState != GestureState.GONE) { // if invisible, skip animation
- updateArrowState(GestureState.CANCELLED)
+ when (currentState) {
+ GestureState.ENTRY -> {
+ if (isFlungAwayFromEdge(endX = event.x)) {
+ updateArrowState(GestureState.ACTIVE)
+ updateArrowState(GestureState.FLUNG)
+ } else {
+ updateArrowState(GestureState.CANCELLED)
+ }
+ }
+ GestureState.INACTIVE -> {
+ if (isFlungAwayFromEdge(endX = event.x)) {
+ mainHandler.postDelayed(MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION) {
+ updateArrowState(GestureState.ACTIVE)
+ updateArrowState(GestureState.FLUNG)
+ }
+ } else {
+ updateArrowState(GestureState.CANCELLED)
+ }
+ }
+ GestureState.ACTIVE -> {
+ if (elapsedTimeSinceEntry < MIN_DURATION_CONSIDERED_AS_FLING) {
+ updateArrowState(GestureState.FLUNG)
+ } else {
+ updateArrowState(GestureState.COMMITTED)
+ }
+ }
+ GestureState.GONE,
+ GestureState.FLUNG,
+ GestureState.COMMITTED,
+ GestureState.CANCELLED -> {
+ updateArrowState(GestureState.CANCELLED)
+ }
}
velocityTracker = null
}
@@ -325,6 +334,14 @@
}
}
+ private fun cancelAllPendingAnimations() {
+ cancelFailsafe()
+ mView.cancelAnimations()
+ mainHandler.removeCallbacks(onEndSetCommittedStateListener.runnable)
+ mainHandler.removeCallbacks(onEndSetGoneStateListener.runnable)
+ mainHandler.removeCallbacks(playAnimationThenSetGoneOnAlphaEnd)
+ }
+
/**
* Returns false until the current gesture exceeds the touch slop threshold,
* and returns true thereafter (we reset on the subsequent back gesture).
@@ -335,7 +352,7 @@
private fun dragSlopExceeded(curX: Float, startX: Float): Boolean {
if (hasPassedDragSlop) return true
- if (abs(curX - startX) > viewConfiguration.scaledTouchSlop) {
+ if (abs(curX - startX) > viewConfiguration.scaledEdgeSlop) {
// Reset the arrow to the side
updateArrowState(GestureState.ENTRY)
@@ -348,39 +365,46 @@
}
private fun updateArrowStateOnMove(yTranslation: Float, xTranslation: Float) {
- if (!currentState.isInteractive())
- return
+
+ val isWithinYActivationThreshold = xTranslation * 2 >= yTranslation
when (currentState) {
- // Check if we should transition from ENTRY to ACTIVE
- GestureState.ENTRY ->
- if (xTranslation > params.swipeTriggerThreshold) {
+ GestureState.ENTRY -> {
+ if (xTranslation > params.staticTriggerThreshold) {
updateArrowState(GestureState.ACTIVE)
}
+ }
+ GestureState.ACTIVE -> {
+ val isPastDynamicDeactivationThreshold =
+ totalTouchDelta <= params.deactivationSwipeTriggerThreshold
+ val isMinDurationElapsed =
+ elapsedTimeSinceActionDown > MIN_DURATION_ACTIVE_ANIMATION
- // Abort if we had continuous motion toward the edge for a while, OR the direction
- // in Y is bigger than X * 2
- GestureState.ACTIVE ->
- if ((totalTouchDelta < 0 && -totalTouchDelta > params.minDeltaForSwitch) ||
- (yTranslation > xTranslation * 2)
+ if (isMinDurationElapsed && (!isWithinYActivationThreshold ||
+ isPastDynamicDeactivationThreshold)
) {
updateArrowState(GestureState.INACTIVE)
}
+ }
+ GestureState.INACTIVE -> {
+ val isPastStaticThreshold =
+ xTranslation > params.staticTriggerThreshold
+ val isPastDynamicReactivationThreshold = totalTouchDelta > 0 &&
+ abs(totalTouchDelta) >=
+ params.reactivationTriggerThreshold
- // Re-activate if we had continuous motion away from the edge for a while
- GestureState.INACTIVE ->
- if (totalTouchDelta > 0 && totalTouchDelta > params.minDeltaForSwitch) {
+ if (isPastStaticThreshold &&
+ isPastDynamicReactivationThreshold &&
+ isWithinYActivationThreshold
+ ) {
updateArrowState(GestureState.ACTIVE)
}
-
- // By default assume the current direction is kept
+ }
else -> {}
}
}
private fun handleMoveEvent(event: MotionEvent) {
- if (!currentState.isInteractive())
- return
val x = event.x
val y = event.y
@@ -400,23 +424,44 @@
previousXTranslation = xTranslation
if (abs(xDelta) > 0) {
- if (sign(xDelta) == sign(totalTouchDelta)) {
+ val range =
+ params.run { deactivationSwipeTriggerThreshold..reactivationTriggerThreshold }
+ val isTouchInContinuousDirection =
+ sign(xDelta) == sign(totalTouchDelta) || totalTouchDelta in range
+
+ if (isTouchInContinuousDirection) {
// Direction has NOT changed, so keep counting the delta
totalTouchDelta += xDelta
} else {
// Direction has changed, so reset the delta
totalTouchDelta = xDelta
+ touchDeltaStartX = x
}
}
updateArrowStateOnMove(yTranslation, xTranslation)
+
when (currentState) {
- GestureState.ACTIVE ->
- stretchActiveBackIndicator(fullScreenStretchProgress(xTranslation))
- GestureState.ENTRY ->
- stretchEntryBackIndicator(preThresholdStretchProgress(xTranslation))
- GestureState.INACTIVE ->
- mView.resetStretch()
+ GestureState.ACTIVE -> {
+ stretchActiveBackIndicator(fullScreenProgress(xTranslation))
+ }
+ GestureState.ENTRY -> {
+ val progress = staticThresholdProgress(xTranslation)
+ stretchEntryBackIndicator(progress)
+
+ params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let {
+ mView.popArrowAlpha(0f, it.value)
+ }
+ }
+ GestureState.INACTIVE -> {
+ val progress = reactivationThresholdProgress(totalTouchDelta)
+ stretchInactiveBackIndicator(progress)
+
+ params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let {
+ gestureArrowStrokeVisibleTime = SystemClock.uptimeMillis()
+ mView.popArrowAlpha(0f, it.value)
+ }
+ }
else -> {}
}
@@ -427,21 +472,22 @@
private fun setVerticalTranslation(yOffset: Float) {
val yTranslation = abs(yOffset)
val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f
- val yProgress = saturate(yTranslation / (maxYOffset * RUBBER_BAND_AMOUNT))
- mView.animateVertically(
- RUBBER_BAND_INTERPOLATOR.getInterpolation(yProgress) * maxYOffset *
+ val rubberbandAmount = 15f
+ val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount))
+ val yPosition = params.translationInterpolator.getInterpolation(yProgress) *
+ maxYOffset *
sign(yOffset)
- )
+ mView.animateVertically(yPosition)
}
/**
- * @return the relative position of the drag from the time after the arrow is activated until
+ * Tracks the relative position of the drag from the time after the arrow is activated until
* the arrow is fully stretched (between 0.0 - 1.0f)
*/
- private fun fullScreenStretchProgress(xTranslation: Float): Float {
- return saturate(
- (xTranslation - params.swipeTriggerThreshold) /
- (fullyStretchedThreshold - params.swipeTriggerThreshold)
+ private fun fullScreenProgress(xTranslation: Float): Float {
+ return MathUtils.saturate(
+ (xTranslation - previousXTranslationOnActiveOffset) /
+ (fullyStretchedThreshold - previousXTranslationOnActiveOffset)
)
}
@@ -449,26 +495,74 @@
* Tracks the relative position of the drag from the entry until the threshold where the arrow
* activates (between 0.0 - 1.0f)
*/
- private fun preThresholdStretchProgress(xTranslation: Float): Float {
- return saturate(xTranslation / params.swipeTriggerThreshold)
+ private fun staticThresholdProgress(xTranslation: Float): Float {
+ return MathUtils.saturate(xTranslation / params.staticTriggerThreshold)
+ }
+
+ private fun reactivationThresholdProgress(totalTouchDelta: Float): Float {
+ return MathUtils.saturate(totalTouchDelta / params.reactivationTriggerThreshold)
}
private fun stretchActiveBackIndicator(progress: Float) {
- val rubberBandIterpolation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
mView.setStretch(
- horizontalTranslationStretchAmount = rubberBandIterpolation,
- arrowStretchAmount = rubberBandIterpolation,
- backgroundWidthStretchAmount = DECELERATE_INTERPOLATOR_SLOW.getInterpolation(progress),
- params.fullyStretchedIndicator
+ horizontalTranslationStretchAmount = params.translationInterpolator
+ .getInterpolation(progress),
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount = params.activeWidthInterpolator
+ .getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ backgroundHeightStretchAmount = 1f,
+ arrowAlphaStretchAmount = 1f,
+ edgeCornerStretchAmount = 1f,
+ farCornerStretchAmount = 1f,
+ fullyStretchedDimens = params.fullyStretchedIndicator
)
}
private fun stretchEntryBackIndicator(progress: Float) {
mView.setStretch(
- horizontalTranslationStretchAmount = 0f,
- arrowStretchAmount = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress),
- backgroundWidthStretchAmount = DECELERATE_INTERPOLATOR.getInterpolation(progress),
- params.preThresholdIndicator
+ horizontalTranslationStretchAmount = 0f,
+ arrowStretchAmount = params.arrowAngleInterpolator
+ .getInterpolation(progress),
+ backgroundWidthStretchAmount = params.entryWidthInterpolator
+ .getInterpolation(progress),
+ backgroundHeightStretchAmount = params.heightInterpolator
+ .getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ arrowAlphaStretchAmount = params.arrowStrokeAlphaInterpolator.get(progress).value,
+ edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+ farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+ fullyStretchedDimens = params.preThresholdIndicator
+ )
+ }
+
+ private var previousPreThresholdWidthInterpolator = params.entryWidthTowardsEdgeInterpolator
+ fun preThresholdWidthStretchAmount(progress: Float): Float {
+ val interpolator = run {
+ val isPastSlop = abs(totalTouchDelta) > ViewConfiguration.get(context).scaledTouchSlop
+ if (isPastSlop) {
+ if (totalTouchDelta > 0) {
+ params.entryWidthInterpolator
+ } else params.entryWidthTowardsEdgeInterpolator
+ } else {
+ previousPreThresholdWidthInterpolator
+ }.also { previousPreThresholdWidthInterpolator = it }
+ }
+ return interpolator.getInterpolation(progress).coerceAtLeast(0f)
+ }
+
+ private fun stretchInactiveBackIndicator(progress: Float) {
+ mView.setStretch(
+ horizontalTranslationStretchAmount = 0f,
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress),
+ backgroundHeightStretchAmount = params.heightInterpolator
+ .getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ arrowAlphaStretchAmount = params.arrowStrokeAlphaInterpolator.get(progress).value,
+ edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+ farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+ fullyStretchedDimens = params.preThresholdIndicator
)
}
@@ -486,8 +580,7 @@
}
}
- override fun setInsets(insetLeft: Int, insetRight: Int) {
- }
+ override fun setInsets(insetLeft: Int, insetRight: Int) = Unit
override fun setBackCallback(callback: NavigationEdgeBackPlugin.BackCallback) {
backCallback = callback
@@ -498,62 +591,54 @@
windowManager.addView(mView, layoutParams)
}
- private fun isFlung() = velocityTracker!!.run {
- computeCurrentVelocity(1000)
- abs(xVelocity) > MIN_FLING_VELOCITY
+ private fun isDragAwayFromEdge(velocityPxPerSecThreshold: Int = 0) = velocityTracker!!.run {
+ computeCurrentVelocity(PX_PER_SEC)
+ val velocity = xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
+ velocity > velocityPxPerSecThreshold
}
- private fun playFlingBackAnimation() {
- playAnimation(setCommittedEndListener)
+ private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean {
+ val minDistanceConsideredForFling = ViewConfiguration.get(context).scaledTouchSlop
+ val flingDistance = abs(endX - startX)
+ val isPastFlingVelocity = isDragAwayFromEdge(
+ velocityPxPerSecThreshold =
+ ViewConfiguration.get(context).scaledMinimumFlingVelocity)
+ return flingDistance > minDistanceConsideredForFling && isPastFlingVelocity
}
- private fun playCommitBackAnimation() {
- // Check if we should vibrate again
- if (previousState != GestureState.FLUNG) {
- velocityTracker!!.computeCurrentVelocity(1000)
- val isSlow = abs(velocityTracker!!.xVelocity) < 500
- val hasNotVibratedRecently =
- SystemClock.uptimeMillis() - vibrationTime >= GESTURE_DURATION_FOR_CLICK_MS
- if (isSlow || hasNotVibratedRecently) {
- vibratorHelper.vibrate(VibrationEffect.EFFECT_CLICK)
- }
- }
- // Dispatch the actual back trigger
- if (DEBUG) Log.d(TAG, "playCommitBackAnimation() invoked triggerBack() on backCallback")
- backCallback.triggerBack()
-
- playAnimation(setGoneEndListener)
- }
-
- private fun playCancelBackAnimation() {
- backCallback.cancelBack()
- playAnimation(setGoneEndListener)
- }
-
- /**
- * @return true if the animation is running, false otherwise. Some transitions don't animate
- */
- private fun playAnimation(endListener: DelayedOnAnimationEndListener) {
- updateRestingArrowDimens(animated = true, currentState)
-
- if (!mView.addEndListener(endListener)) {
+ private fun playHorizontalAnimationThen(onEnd: DelayedOnAnimationEndListener) {
+ updateRestingArrowDimens()
+ if (!mView.addAnimationEndListener(mView.horizontalTranslation, onEnd)) {
scheduleFailsafe()
}
}
- private fun resetOnDown() {
- hasPassedDragSlop = false
- hasHapticPlayed = false
- totalTouchDelta = 0f
- vibrationTime = 0
- cancelFailsafe()
+ private fun playAnimationThenSetGoneEnd() {
+ updateRestingArrowDimens()
+ if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
+ scheduleFailsafe()
+ }
}
- private fun updateYPosition(touchY: Float) {
+ private fun playWithBackgroundWidthAnimation(
+ onEnd: DelayedOnAnimationEndListener,
+ delay: Long = 0L
+ ) {
+ if (delay == 0L) {
+ updateRestingArrowDimens()
+ if (!mView.addAnimationEndListener(mView.backgroundWidth, onEnd)) {
+ scheduleFailsafe()
+ }
+ } else {
+ mainHandler.postDelayed(delay) { playWithBackgroundWidthAnimation(onEnd, delay = 0L) }
+ }
+ }
+
+ private fun updateYStartPosition(touchY: Float) {
var yPosition = touchY - params.fingerOffset
yPosition = max(yPosition, params.minArrowYPosition.toFloat())
yPosition -= layoutParams.height / 2.0f
- layoutParams.y = constrain(yPosition.toInt(), 0, displaySize.y)
+ layoutParams.y = MathUtils.constrain(yPosition.toInt(), 0, displaySize.y)
}
override fun setDisplaySize(displaySize: Point) {
@@ -564,53 +649,135 @@
/**
* Updates resting arrow and background size not accounting for stretch
*/
- private fun updateRestingArrowDimens(animated: Boolean, currentState: GestureState) {
- if (animated) {
- when (currentState) {
- GestureState.ENTRY, GestureState.ACTIVE, GestureState.FLUNG ->
- mView.setArrowStiffness(ARROW_APPEAR_STIFFNESS, ARROW_APPEAR_DAMPING_RATIO)
- GestureState.CANCELLED -> mView.fadeOut()
- else ->
- mView.setArrowStiffness(
- ARROW_DISAPPEAR_STIFFNESS,
- ARROW_DISAPPEAR_DAMPING_RATIO
- )
+ private fun updateRestingArrowDimens() {
+ when (currentState) {
+ GestureState.GONE,
+ GestureState.ENTRY -> {
+ mView.setSpring(
+ arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
+ arrowAlpha = params.entryIndicator.arrowDimens.alphaSpring,
+ scale = params.entryIndicator.scaleSpring,
+ verticalTranslation = params.entryIndicator.verticalTranslationSpring,
+ horizontalTranslation = params.entryIndicator.horizontalTranslationSpring,
+ backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring,
+ backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius = params.entryIndicator.backgroundDimens
+ .edgeCornerRadiusSpring,
+ backgroundFarCornerRadius = params.entryIndicator.backgroundDimens
+ .farCornerRadiusSpring,
+ )
}
+ GestureState.INACTIVE -> {
+ mView.setSpring(
+ arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring,
+ horizontalTranslation = params.preThresholdIndicator
+ .horizontalTranslationSpring,
+ scale = params.preThresholdIndicator.scaleSpring,
+ backgroundWidth = params.preThresholdIndicator.backgroundDimens
+ .widthSpring,
+ backgroundHeight = params.preThresholdIndicator.backgroundDimens
+ .heightSpring,
+ backgroundEdgeCornerRadius = params.preThresholdIndicator.backgroundDimens
+ .edgeCornerRadiusSpring,
+ backgroundFarCornerRadius = params.preThresholdIndicator.backgroundDimens
+ .farCornerRadiusSpring,
+ )
+ }
+ GestureState.ACTIVE -> {
+ mView.setSpring(
+ arrowLength = params.activeIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.activeIndicator.arrowDimens.heightSpring,
+ scale = params.activeIndicator.scaleSpring,
+ horizontalTranslation = params.activeIndicator.horizontalTranslationSpring,
+ backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius = params.activeIndicator.backgroundDimens
+ .edgeCornerRadiusSpring,
+ backgroundFarCornerRadius = params.activeIndicator.backgroundDimens
+ .farCornerRadiusSpring,
+ )
+ }
+ GestureState.FLUNG -> {
+ mView.setSpring(
+ arrowLength = params.flungIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.flungIndicator.arrowDimens.heightSpring,
+ backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius = params.flungIndicator.backgroundDimens
+ .edgeCornerRadiusSpring,
+ backgroundFarCornerRadius = params.flungIndicator.backgroundDimens
+ .farCornerRadiusSpring,
+ )
+ }
+ GestureState.COMMITTED -> {
+ mView.setSpring(
+ arrowLength = params.committedIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.committedIndicator.arrowDimens.heightSpring,
+ scale = params.committedIndicator.scaleSpring,
+ backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius = params.committedIndicator.backgroundDimens
+ .edgeCornerRadiusSpring,
+ backgroundFarCornerRadius = params.committedIndicator.backgroundDimens
+ .farCornerRadiusSpring,
+ )
+ }
+ else -> {}
}
+
mView.setRestingDimens(
- restingParams = EdgePanelParams.BackIndicatorDimens(
- horizontalTranslation = when (currentState) {
- GestureState.GONE -> -params.activeIndicator.backgroundDimens.width
- // Position the committed arrow slightly further off the screen so we do not
- // see part of it bouncing
- GestureState.COMMITTED ->
- -params.activeIndicator.backgroundDimens.width * 1.5f
- GestureState.FLUNG -> params.fullyStretchedIndicator.horizontalTranslation
- GestureState.ACTIVE -> params.activeIndicator.horizontalTranslation
- GestureState.ENTRY, GestureState.INACTIVE, GestureState.CANCELLED ->
- params.entryIndicator.horizontalTranslation
- },
- arrowDimens = when (currentState) {
- GestureState.ACTIVE, GestureState.INACTIVE,
- GestureState.COMMITTED, GestureState.FLUNG -> params.activeIndicator.arrowDimens
- GestureState.CANCELLED -> params.cancelledArrowDimens
- GestureState.GONE, GestureState.ENTRY -> params.entryIndicator.arrowDimens
- },
- backgroundDimens = when (currentState) {
- GestureState.GONE, GestureState.ENTRY -> params.entryIndicator.backgroundDimens
- else ->
- params.activeIndicator.backgroundDimens.copy(
- edgeCornerRadius =
- if (currentState == GestureState.INACTIVE ||
- currentState == GestureState.CANCELLED
- )
- params.cancelledEdgeCornerRadius
- else
- params.activeIndicator.backgroundDimens.edgeCornerRadius
- )
- }
- ),
- animate = animated
+ animate = !(currentState == GestureState.FLUNG ||
+ currentState == GestureState.COMMITTED),
+ restingParams = EdgePanelParams.BackIndicatorDimens(
+ scale = when (currentState) {
+ GestureState.ACTIVE,
+ GestureState.FLUNG,
+ -> params.activeIndicator.scale
+ GestureState.COMMITTED -> params.committedIndicator.scale
+ else -> params.preThresholdIndicator.scale
+ },
+ scalePivotX = when (currentState) {
+ GestureState.GONE,
+ GestureState.ENTRY,
+ GestureState.INACTIVE,
+ GestureState.CANCELLED -> params.preThresholdIndicator.scalePivotX
+ else -> params.committedIndicator.scalePivotX
+ },
+ horizontalTranslation = when (currentState) {
+ GestureState.GONE -> {
+ params.activeIndicator.backgroundDimens.width?.times(-1)
+ }
+ GestureState.ENTRY,
+ GestureState.INACTIVE -> params.entryIndicator.horizontalTranslation
+ GestureState.FLUNG -> params.activeIndicator.horizontalTranslation
+ GestureState.ACTIVE -> params.activeIndicator.horizontalTranslation
+ GestureState.CANCELLED -> {
+ params.cancelledIndicator.horizontalTranslation
+ }
+ else -> null
+ },
+ arrowDimens = when (currentState) {
+ GestureState.GONE,
+ GestureState.ENTRY,
+ GestureState.INACTIVE -> params.entryIndicator.arrowDimens
+ GestureState.ACTIVE -> params.activeIndicator.arrowDimens
+ GestureState.FLUNG,
+ GestureState.COMMITTED -> params.committedIndicator.arrowDimens
+ GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens
+ },
+ backgroundDimens = when (currentState) {
+ GestureState.GONE,
+ GestureState.ENTRY,
+ GestureState.INACTIVE -> params.entryIndicator.backgroundDimens
+ GestureState.ACTIVE -> params.activeIndicator.backgroundDimens
+ GestureState.FLUNG -> params.activeIndicator.backgroundDimens
+ GestureState.COMMITTED -> params.committedIndicator.backgroundDimens
+ GestureState.CANCELLED -> params.cancelledIndicator.backgroundDimens
+ }
+ )
)
}
@@ -623,42 +790,123 @@
private fun updateArrowState(newState: GestureState, force: Boolean = false) {
if (!force && currentState == newState) return
- if (DEBUG) Log.d(TAG, "updateArrowState $currentState -> $newState")
previousState = currentState
currentState = newState
- if (currentState == GestureState.GONE) {
- mView.cancelAlphaAnimations()
- mView.visibility = View.GONE
- } else {
- mView.visibility = View.VISIBLE
+
+ when (currentState) {
+ GestureState.CANCELLED -> {
+ backCallback.cancelBack()
+ }
+ GestureState.FLUNG,
+ GestureState.COMMITTED -> {
+ // When flung, trigger back immediately but don't fire again
+ // once state resolves to committed.
+ if (previousState != GestureState.FLUNG) backCallback.triggerBack()
+ }
+ GestureState.ENTRY,
+ GestureState.INACTIVE -> {
+ backCallback.setTriggerBack(false)
+ }
+ GestureState.ACTIVE -> {
+ backCallback.setTriggerBack(true)
+ }
+ GestureState.GONE -> { }
}
when (currentState) {
// Transitioning to GONE never animates since the arrow is (presumably) already off the
// screen
- GestureState.GONE -> updateRestingArrowDimens(animated = false, currentState)
+ GestureState.GONE -> {
+ updateRestingArrowDimens()
+ mView.isVisible = false
+ }
GestureState.ENTRY -> {
- updateYPosition(startY)
- updateRestingArrowDimens(animated = true, currentState)
+ mView.isVisible = true
+
+ updateRestingArrowDimens()
+ gestureEntryTime = SystemClock.uptimeMillis()
+ gestureInactiveOrEntryTime = SystemClock.uptimeMillis()
}
GestureState.ACTIVE -> {
- updateRestingArrowDimens(animated = true, currentState)
- // Vibrate the first time we transition to ACTIVE
- if (!hasHapticPlayed) {
- hasHapticPlayed = true
- vibrationTime = SystemClock.uptimeMillis()
- vibratorHelper.vibrate(VibrationEffect.EFFECT_TICK)
+ previousXTranslationOnActiveOffset = previousXTranslation
+ gestureActiveTime = SystemClock.uptimeMillis()
+
+ updateRestingArrowDimens()
+
+ vibratorHelper.cancel()
+ mainHandler.postDelayed(10L) {
+ vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
+ }
+
+ val startingVelocity = convertVelocityToSpringStartingVelocity(
+ valueOnFastVelocity = 0f,
+ valueOnSlowVelocity = if (previousState == GestureState.ENTRY) 2f else 4.5f
+ )
+
+ when (previousState) {
+ GestureState.ENTRY,
+ GestureState.INACTIVE -> {
+ mView.popOffEdge(startingVelocity)
+ }
+ GestureState.COMMITTED -> {
+ // if previous state was committed then this activation
+ // was due to a quick second swipe. Don't pop the arrow this time
+ }
+ else -> { }
}
}
+
GestureState.INACTIVE -> {
- updateRestingArrowDimens(animated = true, currentState)
+ gestureInactiveOrEntryTime = SystemClock.uptimeMillis()
+
+ val startingVelocity = convertVelocityToSpringStartingVelocity(
+ valueOnFastVelocity = -1.05f,
+ valueOnSlowVelocity = -1.50f
+ )
+ mView.popOffEdge(startingVelocity)
+
+ vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT)
+ updateRestingArrowDimens()
}
- GestureState.FLUNG -> playFlingBackAnimation()
- GestureState.COMMITTED -> playCommitBackAnimation()
- GestureState.CANCELLED -> playCancelBackAnimation()
+ GestureState.FLUNG -> {
+ mainHandler.postDelayed(POP_ON_FLING_DELAY) { mView.popScale(1.9f) }
+ playHorizontalAnimationThen(onEndSetCommittedStateListener)
+ }
+ GestureState.COMMITTED -> {
+ if (previousState == GestureState.FLUNG) {
+ playAnimationThenSetGoneEnd()
+ } else {
+ mView.popScale(3f)
+ mainHandler.postDelayed(
+ playAnimationThenSetGoneOnAlphaEnd,
+ MIN_DURATION_COMMITTED_ANIMATION
+ )
+ }
+ }
+ GestureState.CANCELLED -> {
+ val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry)
+ playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay)
+
+ params.arrowStrokeAlphaSpring.get(0f).takeIf { it.isNewState }?.let {
+ mView.popArrowAlpha(0f, it.value)
+ }
+ mainHandler.postDelayed(10L) { vibratorHelper.cancel() }
+ }
}
}
+ private fun convertVelocityToSpringStartingVelocity(
+ valueOnFastVelocity: Float,
+ valueOnSlowVelocity: Float,
+ ): Float {
+ val factor = velocityTracker?.run {
+ computeCurrentVelocity(PX_PER_MS)
+ MathUtils.smoothStep(0f, 3f, abs(xVelocity))
+ } ?: valueOnFastVelocity
+
+ return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor)
+ }
+
private fun scheduleFailsafe() {
if (!ENABLE_FAILSAFE) return
cancelFailsafe()
@@ -685,24 +933,24 @@
init {
if (DEBUG) mView.drawDebugInfo = { canvas ->
val debugStrings = listOf(
- "$currentState",
- "startX=$startX",
- "startY=$startY",
- "xDelta=${"%.1f".format(totalTouchDelta)}",
- "xTranslation=${"%.1f".format(previousXTranslation)}",
- "pre=${"%.0f".format(preThresholdStretchProgress(previousXTranslation) * 100)}%",
- "post=${"%.0f".format(fullScreenStretchProgress(previousXTranslation) * 100)}%"
+ "$currentState",
+ "startX=$startX",
+ "startY=$startY",
+ "xDelta=${"%.1f".format(totalTouchDelta)}",
+ "xTranslation=${"%.1f".format(previousXTranslation)}",
+ "pre=${"%.0f".format(staticThresholdProgress(previousXTranslation) * 100)}%",
+ "post=${"%.0f".format(fullScreenProgress(previousXTranslation) * 100)}%"
)
val debugPaint = Paint().apply {
color = Color.WHITE
}
val debugInfoBottom = debugStrings.size * 32f + 4f
canvas.drawRect(
- 4f,
- 4f,
- canvas.width.toFloat(),
- debugStrings.size * 32f + 4f,
- debugPaint
+ 4f,
+ 4f,
+ canvas.width.toFloat(),
+ debugStrings.size * 32f + 4f,
+ debugPaint
)
debugPaint.apply {
color = Color.BLACK
@@ -728,9 +976,71 @@
canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint)
}
- drawVerticalLine(x = params.swipeTriggerThreshold, color = Color.BLUE)
+ drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
+ drawVerticalLine(x = params.deactivationSwipeTriggerThreshold, color = Color.BLUE)
drawVerticalLine(x = startX, color = Color.GREEN)
drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
}
}
}
+
+/**
+ * In addition to a typical step function which returns one or two
+ * values based on a threshold, `Step` also gracefully handles quick
+ * changes in input near the threshold value that would typically
+ * result in the output rapidly changing.
+ *
+ * In the context of Back arrow, the arrow's stroke opacity should
+ * always appear transparent or opaque. Using a typical Step function,
+ * this would resulting in a flickering appearance as the output would
+ * change rapidly. `Step` addresses this by moving the threshold after
+ * it is crossed so it cannot be easily crossed again with small changes
+ * in touch events.
+ */
+class Step<T>(
+ private val threshold: Float,
+ private val factor: Float = 1.1f,
+ private val postThreshold: T,
+ private val preThreshold: T
+) {
+
+ data class Value<T>(val value: T, val isNewState: Boolean)
+
+ private val lowerFactor = 2 - factor
+
+ private lateinit var startValue: Value<T>
+ private lateinit var previousValue: Value<T>
+ private var hasCrossedUpperBoundAtLeastOnce = false
+ private var progress: Float = 0f
+
+ init {
+ reset()
+ }
+
+ fun reset() {
+ hasCrossedUpperBoundAtLeastOnce = false
+ progress = 0f
+ startValue = Value(preThreshold, false)
+ previousValue = startValue
+ }
+
+ fun get(progress: Float): Value<T> {
+ this.progress = progress
+
+ val hasCrossedUpperBound = progress > threshold * factor
+ val hasCrossedLowerBound = progress > threshold * lowerFactor
+
+ return when {
+ hasCrossedUpperBound && !hasCrossedUpperBoundAtLeastOnce -> {
+ hasCrossedUpperBoundAtLeastOnce = true
+ Value(postThreshold, true)
+ }
+ hasCrossedLowerBound -> previousValue.copy(isNewState = false)
+ hasCrossedUpperBoundAtLeastOnce -> {
+ hasCrossedUpperBoundAtLeastOnce = false
+ Value(preThreshold, true)
+ }
+ else -> startValue
+ }.also { previousValue = it }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
index d56537b..0c00022 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
@@ -1,52 +1,82 @@
package com.android.systemui.navigationbar.gestural
import android.content.res.Resources
+import android.util.TypedValue
+import androidx.core.animation.PathInterpolator
+import androidx.dynamicanimation.animation.SpringForce
import com.android.systemui.R
data class EdgePanelParams(private var resources: Resources) {
data class ArrowDimens(
- val length: Float = 0f,
- val height: Float = 0f
+ val length: Float = 0f,
+ val height: Float = 0f,
+ val alpha: Float = 0f,
+ var alphaSpring: SpringForce? = null,
+ val heightSpring: SpringForce? = null,
+ val lengthSpring: SpringForce? = null,
)
data class BackgroundDimens(
- val width: Float = 0f,
- val height: Float = 0f,
- val edgeCornerRadius: Float = 0f,
- val farCornerRadius: Float = 0f
+ val width: Float? = 0f,
+ val height: Float = 0f,
+ val edgeCornerRadius: Float = 0f,
+ val farCornerRadius: Float = 0f,
+ val alpha: Float = 0f,
+ val widthSpring: SpringForce? = null,
+ val heightSpring: SpringForce? = null,
+ val farCornerRadiusSpring: SpringForce? = null,
+ val edgeCornerRadiusSpring: SpringForce? = null,
+ val alphaSpring: SpringForce? = null,
)
data class BackIndicatorDimens(
- val horizontalTranslation: Float = 0f,
- val arrowDimens: ArrowDimens = ArrowDimens(),
- val backgroundDimens: BackgroundDimens = BackgroundDimens()
+ val horizontalTranslation: Float? = 0f,
+ val scale: Float = 0f,
+ val scalePivotX: Float = 0f,
+ val arrowDimens: ArrowDimens,
+ val backgroundDimens: BackgroundDimens,
+ val verticalTranslationSpring: SpringForce? = null,
+ val horizontalTranslationSpring: SpringForce? = null,
+ val scaleSpring: SpringForce? = null,
)
- var arrowThickness: Float = 0f
+ lateinit var entryIndicator: BackIndicatorDimens
private set
- var entryIndicator = BackIndicatorDimens()
+ lateinit var activeIndicator: BackIndicatorDimens
private set
- var activeIndicator = BackIndicatorDimens()
+ lateinit var cancelledIndicator: BackIndicatorDimens
private set
- var preThresholdIndicator = BackIndicatorDimens()
+ lateinit var flungIndicator: BackIndicatorDimens
private set
- var fullyStretchedIndicator = BackIndicatorDimens()
+ lateinit var committedIndicator: BackIndicatorDimens
private set
- var cancelledEdgeCornerRadius: Float = 0f
+ lateinit var preThresholdIndicator: BackIndicatorDimens
private set
- var cancelledArrowDimens = ArrowDimens()
+ lateinit var fullyStretchedIndicator: BackIndicatorDimens
+ private set
// navigation bar edge constants
var arrowPaddingEnd: Int = 0
private set
+ var arrowThickness: Float = 0f
+ private set
+ lateinit var arrowStrokeAlphaSpring: Step<SpringForce>
+ private set
+ lateinit var arrowStrokeAlphaInterpolator: Step<Float>
+ private set
// The closest to y
var minArrowYPosition: Int = 0
private set
var fingerOffset: Int = 0
private set
- var swipeTriggerThreshold: Float = 0f
+ var staticTriggerThreshold: Float = 0f
+ private set
+ var reactivationTriggerThreshold: Float = 0f
+ private set
+ var deactivationSwipeTriggerThreshold: Float = 0f
+ get() = -field
private set
var swipeProgressThreshold: Float = 0f
private set
@@ -55,6 +85,26 @@
var minDeltaForSwitch: Int = 0
private set
+ var minDragToStartAnimation: Float = 0f
+ private set
+
+ lateinit var entryWidthInterpolator: PathInterpolator
+ private set
+ lateinit var entryWidthTowardsEdgeInterpolator: PathInterpolator
+ private set
+ lateinit var activeWidthInterpolator: PathInterpolator
+ private set
+ lateinit var arrowAngleInterpolator: PathInterpolator
+ private set
+ lateinit var translationInterpolator: PathInterpolator
+ private set
+ lateinit var farCornerInterpolator: PathInterpolator
+ private set
+ lateinit var edgeCornerInterpolator: PathInterpolator
+ private set
+ lateinit var heightInterpolator: PathInterpolator
+ private set
+
init {
update(resources)
}
@@ -63,6 +113,10 @@
return resources.getDimension(id)
}
+ private fun getDimenFloat(id: Int): Float {
+ return TypedValue().run { resources.getValue(id, this, true); float }
+ }
+
private fun getPx(id: Int): Int {
return resources.getDimensionPixelSize(id)
}
@@ -73,72 +127,200 @@
arrowPaddingEnd = getPx(R.dimen.navigation_edge_panel_padding)
minArrowYPosition = getPx(R.dimen.navigation_edge_arrow_min_y)
fingerOffset = getPx(R.dimen.navigation_edge_finger_offset)
- swipeTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold)
+ staticTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold)
+ reactivationTriggerThreshold =
+ getDimen(R.dimen.navigation_edge_action_reactivation_drag_threshold)
+ deactivationSwipeTriggerThreshold =
+ getDimen(R.dimen.navigation_edge_action_deactivation_drag_threshold)
swipeProgressThreshold = getDimen(R.dimen.navigation_edge_action_progress_threshold)
minDeltaForSwitch = getPx(R.dimen.navigation_edge_minimum_x_delta_for_switch)
+ minDragToStartAnimation =
+ getDimen(R.dimen.navigation_edge_action_min_distance_to_start_animation)
+
+ entryWidthInterpolator = PathInterpolator(.19f, 1.27f, .71f, .86f)
+ entryWidthTowardsEdgeInterpolator = PathInterpolator(1f, -3f, 1f, 1.2f)
+ activeWidthInterpolator = PathInterpolator(.15f, .48f, .46f, .89f)
+ arrowAngleInterpolator = entryWidthInterpolator
+ translationInterpolator = PathInterpolator(0.2f, 1.0f, 1.0f, 1.0f)
+ farCornerInterpolator = PathInterpolator(.03f, .19f, .14f, 1.09f)
+ edgeCornerInterpolator = PathInterpolator(0f, 1.11f, .85f, .84f)
+ heightInterpolator = PathInterpolator(1f, .05f, .9f, -0.29f)
+
+ val showArrowOnProgressValue = .2f
+ val showArrowOnProgressValueFactor = 1.05f
+
+ val entryActiveHorizontalTranslationSpring = createSpring(675f, 0.8f)
+ val activeCommittedArrowLengthSpring = createSpring(1500f, 0.29f)
+ val activeCommittedArrowHeightSpring = createSpring(1500f, 0.29f)
+ val flungCommittedEdgeCornerSpring = createSpring(10000f, 1f)
+ val flungCommittedFarCornerSpring = createSpring(10000f, 1f)
+ val flungCommittedWidthSpring = createSpring(10000f, 1f)
+ val flungCommittedHeightSpring = createSpring(10000f, 1f)
entryIndicator = BackIndicatorDimens(
- horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin),
- arrowDimens = ArrowDimens(
- length = getDimen(R.dimen.navigation_edge_entry_arrow_length),
- height = getDimen(R.dimen.navigation_edge_entry_arrow_height),
- ),
- backgroundDimens = BackgroundDimens(
- width = getDimen(R.dimen.navigation_edge_entry_background_width),
- height = getDimen(R.dimen.navigation_edge_entry_background_height),
- edgeCornerRadius = getDimen(R.dimen.navigation_edge_entry_edge_corners),
- farCornerRadius = getDimen(R.dimen.navigation_edge_entry_far_corners)
- )
+ horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin),
+ scale = getDimenFloat(R.dimen.navigation_edge_entry_scale),
+ scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
+ horizontalTranslationSpring = entryActiveHorizontalTranslationSpring,
+ verticalTranslationSpring = createSpring(10000f, 0.9f),
+ scaleSpring = createSpring(120f, 0.8f),
+ arrowDimens = ArrowDimens(
+ length = getDimen(R.dimen.navigation_edge_entry_arrow_length),
+ height = getDimen(R.dimen.navigation_edge_entry_arrow_height),
+ alpha = 0f,
+ alphaSpring = createSpring(200f, 1f),
+ lengthSpring = createSpring(600f, 0.4f),
+ heightSpring = createSpring(600f, 0.4f),
+ ),
+ backgroundDimens = BackgroundDimens(
+ alpha = 1f,
+ width = getDimen(R.dimen.navigation_edge_entry_background_width),
+ height = getDimen(R.dimen.navigation_edge_entry_background_height),
+ edgeCornerRadius = getDimen(R.dimen.navigation_edge_entry_edge_corners),
+ farCornerRadius = getDimen(R.dimen.navigation_edge_entry_far_corners),
+ alphaSpring = createSpring(900f, 1f),
+ widthSpring = createSpring(450f, 0.65f),
+ heightSpring = createSpring(1500f, 0.45f),
+ farCornerRadiusSpring = createSpring(300f, 0.5f),
+ edgeCornerRadiusSpring = createSpring(150f, 0.5f),
+ )
)
activeIndicator = BackIndicatorDimens(
- horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin),
- arrowDimens = ArrowDimens(
- length = getDimen(R.dimen.navigation_edge_active_arrow_length),
- height = getDimen(R.dimen.navigation_edge_active_arrow_height),
- ),
- backgroundDimens = BackgroundDimens(
- width = getDimen(R.dimen.navigation_edge_active_background_width),
- height = getDimen(R.dimen.navigation_edge_active_background_height),
- edgeCornerRadius = getDimen(R.dimen.navigation_edge_active_edge_corners),
- farCornerRadius = getDimen(R.dimen.navigation_edge_active_far_corners)
-
- )
+ horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin),
+ scale = getDimenFloat(R.dimen.navigation_edge_active_scale),
+ horizontalTranslationSpring = entryActiveHorizontalTranslationSpring,
+ scaleSpring = createSpring(450f, 0.415f),
+ arrowDimens = ArrowDimens(
+ length = getDimen(R.dimen.navigation_edge_active_arrow_length),
+ height = getDimen(R.dimen.navigation_edge_active_arrow_height),
+ alpha = 1f,
+ lengthSpring = activeCommittedArrowLengthSpring,
+ heightSpring = activeCommittedArrowHeightSpring,
+ ),
+ backgroundDimens = BackgroundDimens(
+ alpha = 1f,
+ width = getDimen(R.dimen.navigation_edge_active_background_width),
+ height = getDimen(R.dimen.navigation_edge_active_background_height),
+ edgeCornerRadius = getDimen(R.dimen.navigation_edge_active_edge_corners),
+ farCornerRadius = getDimen(R.dimen.navigation_edge_active_far_corners),
+ widthSpring = createSpring(375f, 0.675f),
+ heightSpring = createSpring(10000f, 1f),
+ edgeCornerRadiusSpring = createSpring(600f, 0.36f),
+ farCornerRadiusSpring = createSpring(2500f, 0.855f),
+ )
)
preThresholdIndicator = BackIndicatorDimens(
- horizontalTranslation = getDimen(R.dimen.navigation_edge_pre_threshold_margin),
- arrowDimens = ArrowDimens(
- length = entryIndicator.arrowDimens.length,
- height = entryIndicator.arrowDimens.height,
- ),
- backgroundDimens = BackgroundDimens(
- width = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
- height = getDimen(R.dimen.navigation_edge_pre_threshold_background_height),
- edgeCornerRadius = getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners),
- farCornerRadius = getDimen(R.dimen.navigation_edge_pre_threshold_far_corners)
- )
+ horizontalTranslation = getDimen(R.dimen.navigation_edge_pre_threshold_margin),
+ scale = getDimenFloat(R.dimen.navigation_edge_pre_threshold_scale),
+ scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
+ scaleSpring = createSpring(120f, 0.8f),
+ horizontalTranslationSpring = createSpring(6000f, 1f),
+ arrowDimens = ArrowDimens(
+ length = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_length),
+ height = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_height),
+ alpha = 1f,
+ lengthSpring = createSpring(100f, 0.6f),
+ heightSpring = createSpring(100f, 0.6f),
+ ),
+ backgroundDimens = BackgroundDimens(
+ alpha = 1f,
+ width = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
+ height = getDimen(R.dimen.navigation_edge_pre_threshold_background_height),
+ edgeCornerRadius =
+ getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners),
+ farCornerRadius =
+ getDimen(R.dimen.navigation_edge_pre_threshold_far_corners),
+ widthSpring = createSpring(200f, 0.65f),
+ heightSpring = createSpring(1500f, 0.45f),
+ farCornerRadiusSpring = createSpring(200f, 1f),
+ edgeCornerRadiusSpring = createSpring(150f, 0.5f),
+ )
+ )
+
+ committedIndicator = activeIndicator.copy(
+ horizontalTranslation = null,
+ arrowDimens = activeIndicator.arrowDimens.copy(
+ lengthSpring = activeCommittedArrowLengthSpring,
+ heightSpring = activeCommittedArrowHeightSpring,
+ ),
+ backgroundDimens = activeIndicator.backgroundDimens.copy(
+ alpha = 0f,
+ // explicitly set to null to preserve previous width upon state change
+ width = null,
+ widthSpring = flungCommittedWidthSpring,
+ heightSpring = flungCommittedHeightSpring,
+ edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring,
+ farCornerRadiusSpring = flungCommittedFarCornerSpring,
+ ),
+ scale = 0.85f,
+ scaleSpring = createSpring(650f, 1f),
+ )
+
+ flungIndicator = committedIndicator.copy(
+ arrowDimens = committedIndicator.arrowDimens.copy(
+ lengthSpring = createSpring(850f, 0.46f),
+ heightSpring = createSpring(850f, 0.46f),
+ ),
+ backgroundDimens = committedIndicator.backgroundDimens.copy(
+ widthSpring = flungCommittedWidthSpring,
+ heightSpring = flungCommittedHeightSpring,
+ edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring,
+ farCornerRadiusSpring = flungCommittedFarCornerSpring,
+ )
+ )
+
+ cancelledIndicator = entryIndicator.copy(
+ backgroundDimens = entryIndicator.backgroundDimens.copy(width = 0f)
)
fullyStretchedIndicator = BackIndicatorDimens(
- horizontalTranslation = getDimen(R.dimen.navigation_edge_stretch_margin),
- arrowDimens = ArrowDimens(
- length = getDimen(R.dimen.navigation_edge_stretched_arrow_length),
- height = getDimen(R.dimen.navigation_edge_stretched_arrow_height),
- ),
- backgroundDimens = BackgroundDimens(
- width = getDimen(R.dimen.navigation_edge_stretch_background_width),
- height = getDimen(R.dimen.navigation_edge_stretch_background_height),
- edgeCornerRadius = getDimen(R.dimen.navigation_edge_stretch_edge_corners),
- farCornerRadius = getDimen(R.dimen.navigation_edge_stretch_far_corners)
+ horizontalTranslation = getDimen(R.dimen.navigation_edge_stretch_margin),
+ scale = getDimenFloat(R.dimen.navigation_edge_stretch_scale),
+ horizontalTranslationSpring = null,
+ verticalTranslationSpring = null,
+ scaleSpring = null,
+ arrowDimens = ArrowDimens(
+ length = getDimen(R.dimen.navigation_edge_stretched_arrow_length),
+ height = getDimen(R.dimen.navigation_edge_stretched_arrow_height),
+ alpha = 1f,
+ alphaSpring = null,
+ heightSpring = null,
+ lengthSpring = null,
+ ),
+ backgroundDimens = BackgroundDimens(
+ alpha = 1f,
+ width = getDimen(R.dimen.navigation_edge_stretch_background_width),
+ height = getDimen(R.dimen.navigation_edge_stretch_background_height),
+ edgeCornerRadius = getDimen(R.dimen.navigation_edge_stretch_edge_corners),
+ farCornerRadius = getDimen(R.dimen.navigation_edge_stretch_far_corners),
+ alphaSpring = null,
+ widthSpring = null,
+ heightSpring = null,
+ edgeCornerRadiusSpring = null,
+ farCornerRadiusSpring = null,
+ )
+ )
+
+ arrowStrokeAlphaInterpolator = Step(
+ threshold = showArrowOnProgressValue,
+ factor = showArrowOnProgressValueFactor,
+ postThreshold = 1f,
+ preThreshold = 0f
+ )
+
+ entryIndicator.arrowDimens.alphaSpring?.let { alphaSpring ->
+ arrowStrokeAlphaSpring = Step(
+ threshold = showArrowOnProgressValue,
+ factor = showArrowOnProgressValueFactor,
+ postThreshold = alphaSpring,
+ preThreshold = SpringForce().setStiffness(2000f).setDampingRatio(1f)
)
- )
-
- cancelledEdgeCornerRadius = getDimen(R.dimen.navigation_edge_cancelled_edge_corners)
-
- cancelledArrowDimens = ArrowDimens(
- length = getDimen(R.dimen.navigation_edge_cancelled_arrow_length),
- height = getDimen(R.dimen.navigation_edge_cancelled_arrow_height)
- )
+ }
}
}
+
+fun createSpring(stiffness: Float, dampingRatio: Float): SpringForce {
+ return SpringForce().setStiffness(stiffness).setDampingRatio(dampingRatio)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 6bfe1a0..be615d6 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -20,9 +20,12 @@
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
import android.os.UserManager
import android.util.Log
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.kotlin.getOrNull
@@ -42,11 +45,12 @@
@Inject
constructor(
private val context: Context,
- private val intentResolver: NoteTaskIntentResolver,
+ private val resolver: NoteTaskInfoResolver,
private val optionalBubbles: Optional<Bubbles>,
private val optionalKeyguardManager: Optional<KeyguardManager>,
private val optionalUserManager: Optional<UserManager>,
@NoteTaskEnabledKey private val isEnabled: Boolean,
+ private val uiEventLogger: UiEventLogger,
) {
/**
@@ -64,7 +68,9 @@
*
* That will let users open other apps in full screen, and take contextual notes.
*/
- fun showNoteTask(isInMultiWindowMode: Boolean = false) {
+ @JvmOverloads
+ fun showNoteTask(isInMultiWindowMode: Boolean = false, uiEvent: ShowNoteTaskUiEvent? = null) {
+
if (!isEnabled) return
val bubbles = optionalBubbles.getOrNull() ?: return
@@ -74,9 +80,12 @@
// TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
if (!userManager.isUserUnlocked) return
- val intent = intentResolver.resolveIntent() ?: return
+ val noteTaskInfo = resolver.resolveInfo() ?: return
+
+ uiEvent?.let { uiEventLogger.log(it, noteTaskInfo.uid, noteTaskInfo.packageName) }
// TODO(b/266686199): We should handle when app not available. For now, we log.
+ val intent = noteTaskInfo.toCreateNoteIntent()
try {
if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) {
context.startActivity(intent)
@@ -84,9 +93,7 @@
bubbles.showOrHideAppBubble(intent)
}
} catch (e: ActivityNotFoundException) {
- val message =
- "Activity not found for action: ${NoteTaskIntentResolver.ACTION_CREATE_NOTE}."
- Log.e(TAG, message, e)
+ Log.e(TAG, "Activity not found for action: $ACTION_CREATE_NOTE.", e)
}
}
@@ -114,10 +121,47 @@
)
}
+ /** IDs of UI events accepted by [showNoteTask]. */
+ enum class ShowNoteTaskUiEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "User opened a note by tapping on the lockscreen shortcut.")
+ NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE(1294),
+
+ /* ktlint-disable max-line-length */
+ @UiEvent(
+ doc =
+ "User opened a note by pressing the stylus tail button while the screen was unlocked."
+ )
+ NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON(1295),
+ @UiEvent(
+ doc =
+ "User opened a note by pressing the stylus tail button while the screen was locked."
+ )
+ NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED(1296),
+ @UiEvent(doc = "User opened a note by tapping on an app shortcut.")
+ NOTE_OPENED_VIA_SHORTCUT(1297);
+
+ override fun getId() = _id
+ }
+
companion object {
private val TAG = NoteTaskController::class.simpleName.orEmpty()
+ private fun NoteTaskInfoResolver.NoteTaskInfo.toCreateNoteIntent(): Intent {
+ return Intent(ACTION_CREATE_NOTE)
+ .setPackage(packageName)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint
+ // was used to start it.
+ .putExtra(INTENT_EXTRA_USE_STYLUS_MODE, true)
+ }
+
// TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead.
const val NOTE_TASK_KEY_EVENT = 311
+
+ // TODO(b/265912743): Use Intent.ACTION_CREATE_NOTE instead.
+ const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
+
+ // TODO(b/265912743): Use Intent.INTENT_EXTRA_USE_STYLUS_MODE instead.
+ const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt
new file mode 100644
index 0000000..bd822d4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import android.app.role.RoleManager
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.util.Log
+import javax.inject.Inject
+
+internal class NoteTaskInfoResolver
+@Inject
+constructor(
+ private val context: Context,
+ private val roleManager: RoleManager,
+ private val packageManager: PackageManager,
+) {
+ fun resolveInfo(): NoteTaskInfo? {
+ // TODO(b/267634412): Select UserHandle depending on where the user initiated note-taking.
+ val user = context.user
+ val packageName = roleManager.getRoleHoldersAsUser(ROLE_NOTES, user).firstOrNull()
+
+ if (packageName.isNullOrEmpty()) return null
+
+ return NoteTaskInfo(packageName, packageManager.getUidOf(packageName, user))
+ }
+
+ /** Package name and kernel user-ID of a note-taking app. */
+ data class NoteTaskInfo(val packageName: String, val uid: Int)
+
+ companion object {
+ private val TAG = NoteTaskInfoResolver::class.simpleName.orEmpty()
+
+ private val EMPTY_APPLICATION_INFO_FLAGS = PackageManager.ApplicationInfoFlags.of(0)!!
+
+ /**
+ * Returns the kernel user-ID of [packageName] for a [user]. Returns zero if the app cannot
+ * be found.
+ */
+ private fun PackageManager.getUidOf(packageName: String, user: UserHandle): Int {
+ val applicationInfo =
+ try {
+ getApplicationInfoAsUser(packageName, EMPTY_APPLICATION_INFO_FLAGS, user)
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.e(TAG, "Couldn't find notes app UID", e)
+ return 0
+ }
+ return applicationInfo.uid
+ }
+
+ // TODO(b/265912743): Use RoleManager.NOTES_ROLE instead.
+ const val ROLE_NOTES = "android.app.role.NOTES"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index d5f4a5a..d40bf2b 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -16,8 +16,10 @@
package com.android.systemui.notetask
+import android.app.KeyguardManager
import androidx.annotation.VisibleForTesting
import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.kotlin.getOrNull
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
import javax.inject.Inject
@@ -30,6 +32,7 @@
private val noteTaskController: NoteTaskController,
private val commandQueue: CommandQueue,
@NoteTaskEnabledKey private val isEnabled: Boolean,
+ private val optionalKeyguardManager: Optional<KeyguardManager>,
) {
@VisibleForTesting
@@ -37,11 +40,21 @@
object : CommandQueue.Callbacks {
override fun handleSystemKey(keyCode: Int) {
if (keyCode == NoteTaskController.NOTE_TASK_KEY_EVENT) {
- noteTaskController.showNoteTask()
+ showNoteTask()
}
}
}
+ private fun showNoteTask() {
+ val uiEvent =
+ if (optionalKeyguardManager.isKeyguardLocked) {
+ NoteTaskController.ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED
+ } else {
+ NoteTaskController.ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON
+ }
+ noteTaskController.showNoteTask(uiEvent = uiEvent)
+ }
+
fun initialize() {
if (isEnabled && optionalBubbles.isPresent) {
commandQueue.addCallback(callbacks)
@@ -49,3 +62,7 @@
noteTaskController.setNoteTaskShortcutEnabled(isEnabled)
}
}
+
+private val Optional<KeyguardManager>.isKeyguardLocked: Boolean
+ // If there's no KeyguardManager, assume that the keyguard is not locked.
+ get() = getOrNull()?.isKeyguardLocked ?: false
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
deleted file mode 100644
index 11dc1d7..0000000
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
+++ /dev/null
@@ -1,54 +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.notetask
-
-import android.app.role.RoleManager
-import android.content.Context
-import android.content.Intent
-import javax.inject.Inject
-
-internal class NoteTaskIntentResolver
-@Inject
-constructor(
- private val context: Context,
- private val roleManager: RoleManager,
-) {
-
- fun resolveIntent(): Intent? {
- val packageName = roleManager.getRoleHoldersAsUser(ROLE_NOTES, context.user).firstOrNull()
-
- if (packageName.isNullOrEmpty()) return null
-
- return Intent(ACTION_CREATE_NOTE)
- .setPackage(packageName)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint was
- // used to start it.
- .putExtra(INTENT_EXTRA_USE_STYLUS_MODE, true)
- }
-
- companion object {
- // TODO(b/265912743): Use Intent.ACTION_CREATE_NOTE instead.
- const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
-
- // TODO(b/265912743): Use RoleManager.NOTES_ROLE instead.
- const val ROLE_NOTES = "android.app.role.NOTES"
-
- // TODO(b/265912743): Use Intent.INTENT_EXTRA_USE_STYLUS_MODE instead.
- const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE"
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index ec6a16a..b8800a2 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -51,7 +51,7 @@
featureFlags: FeatureFlags,
roleManager: RoleManager,
): Boolean {
- val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskIntentResolver.ROLE_NOTES)
+ val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskInfoResolver.ROLE_NOTES)
val isFeatureEnabled = featureFlags.isEnabled(Flags.NOTE_TASKS)
return isRoleAvailable && isFeatureEnabled
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
index cfbaa48..43869cc 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
@@ -27,6 +27,7 @@
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.PickerScreenState
import com.android.systemui.notetask.NoteTaskController
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.android.systemui.notetask.NoteTaskEnabledKey
import javax.inject.Inject
import kotlinx.coroutines.flow.flowOf
@@ -64,7 +65,9 @@
}
override fun onTriggered(expandable: Expandable?): OnTriggeredResult {
- noteTaskController.showNoteTask()
+ noteTaskController.showNoteTask(
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE
+ )
return OnTriggeredResult.Handled
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
index f203e7a..3ac5bfa 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
@@ -21,7 +21,7 @@
import android.os.Bundle
import androidx.activity.ComponentActivity
import com.android.systemui.notetask.NoteTaskController
-import com.android.systemui.notetask.NoteTaskIntentResolver
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import javax.inject.Inject
/** Activity responsible for launching the note experience, and finish. */
@@ -34,7 +34,10 @@
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- noteTaskController.showNoteTask(isInMultiWindowMode)
+ noteTaskController.showNoteTask(
+ isInMultiWindowMode = isInMultiWindowMode,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT,
+ )
finish()
}
@@ -46,7 +49,7 @@
return Intent(context, LaunchNoteTaskActivity::class.java).apply {
// Intent's action must be set in shortcuts, or an exception will be thrown.
// TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead.
- action = NoteTaskIntentResolver.ACTION_CREATE_NOTE
+ action = NoteTaskController.ACTION_CREATE_NOTE
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index 51de522..1c60486 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -255,17 +255,19 @@
Log.d(TAG, "setWifiIndicators: " + indicators);
}
mWifiInfo.mEnabled = indicators.enabled;
- if (indicators.qsIcon == null) {
- return;
- }
- mWifiInfo.mConnected = indicators.qsIcon.visible;
- mWifiInfo.mWifiSignalIconId = indicators.qsIcon.icon;
- mWifiInfo.mWifiSignalContentDescription = indicators.qsIcon.contentDescription;
- mWifiInfo.mEnabled = indicators.enabled;
mWifiInfo.mSsid = indicators.description;
mWifiInfo.mIsTransient = indicators.isTransient;
mWifiInfo.mStatusLabel = indicators.statusLabel;
- refreshState(mWifiInfo);
+ if (indicators.qsIcon != null) {
+ mWifiInfo.mConnected = indicators.qsIcon.visible;
+ mWifiInfo.mWifiSignalIconId = indicators.qsIcon.icon;
+ mWifiInfo.mWifiSignalContentDescription = indicators.qsIcon.contentDescription;
+ refreshState(mWifiInfo);
+ } else {
+ mWifiInfo.mConnected = false;
+ mWifiInfo.mWifiSignalIconId = 0;
+ mWifiInfo.mWifiSignalContentDescription = null;
+ }
}
@Override
@@ -529,6 +531,9 @@
if (DEBUG) {
Log.d(TAG, "handleUpdateEthernetState: " + "EthernetCallbackInfo = " + cb.toString());
}
+ if (!cb.mConnected) {
+ return;
+ }
final Resources r = mContext.getResources();
state.label = r.getString(R.string.quick_settings_internet_label);
state.state = Tile.STATE_ACTIVE;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index 64a8a14..ad00069 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -171,8 +171,9 @@
getHost().collapsePanels();
};
- Dialog dialog = mController.createScreenRecordDialog(mContext, mFlags,
+ final Dialog dialog = mController.createScreenRecordDialog(mContext, mFlags,
mDialogLaunchAnimator, mActivityStarter, onStartRecordingClicked);
+
ActivityStarter.OnDismissAction dismissAction = () -> {
if (shouldAnimateFromView) {
mDialogLaunchAnimator.showFromView(dialog, view, new DialogCuj(
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index b8684ee..db2e62b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -36,6 +36,8 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.settings.UserTracker;
@@ -46,6 +48,8 @@
import javax.inject.Inject;
+import dagger.Lazy;
+
/**
* Helper class to initiate a screen recording
*/
@@ -60,6 +64,8 @@
private CountDownTimer mCountDownTimer = null;
private final Executor mMainExecutor;
private final BroadcastDispatcher mBroadcastDispatcher;
+ private final Context mContext;
+ private final FeatureFlags mFlags;
private final UserContextProvider mUserContextProvider;
private final UserTracker mUserTracker;
@@ -70,6 +76,8 @@
private CopyOnWriteArrayList<RecordingStateChangeCallback> mListeners =
new CopyOnWriteArrayList<>();
+ private final Lazy<ScreenCaptureDevicePolicyResolver> mDevicePolicyResolver;
+
@VisibleForTesting
final UserTracker.Callback mUserChangedCallback =
new UserTracker.Callback() {
@@ -100,22 +108,44 @@
@Inject
public RecordingController(@Main Executor mainExecutor,
BroadcastDispatcher broadcastDispatcher,
+ Context context,
+ FeatureFlags flags,
UserContextProvider userContextProvider,
+ Lazy<ScreenCaptureDevicePolicyResolver> devicePolicyResolver,
UserTracker userTracker) {
mMainExecutor = mainExecutor;
+ mContext = context;
+ mFlags = flags;
+ mDevicePolicyResolver = devicePolicyResolver;
mBroadcastDispatcher = broadcastDispatcher;
mUserContextProvider = userContextProvider;
mUserTracker = userTracker;
}
- /** Create a dialog to show screen recording options to the user. */
+ /**
+ * MediaProjection host is SystemUI for the screen recorder, so return 'my user handle'
+ */
+ private UserHandle getHostUserHandle() {
+ return UserHandle.of(UserHandle.myUserId());
+ }
+
+ /** Create a dialog to show screen recording options to the user.
+ * If screen capturing is currently not allowed it will return a dialog
+ * that warns users about it. */
public Dialog createScreenRecordDialog(Context context, FeatureFlags flags,
DialogLaunchAnimator dialogLaunchAnimator,
ActivityStarter activityStarter,
@Nullable Runnable onStartRecordingClicked) {
+ if (mFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)
+ && mDevicePolicyResolver.get()
+ .isScreenCaptureCompletelyDisabled(getHostUserHandle())) {
+ return new ScreenCaptureDisabledDialog(mContext);
+ }
+
return flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)
- ? new ScreenRecordPermissionDialog(context, this, activityStarter,
- dialogLaunchAnimator, mUserContextProvider, onStartRecordingClicked)
+ ? new ScreenRecordPermissionDialog(context, getHostUserHandle(), this,
+ activityStarter, dialogLaunchAnimator, mUserContextProvider,
+ onStartRecordingClicked)
: new ScreenRecordDialog(context, this, activityStarter,
mUserContextProvider, flags, dialogLaunchAnimator, onStartRecordingClicked);
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
index 68e3dcd..dd21be9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
@@ -42,6 +42,7 @@
/** Dialog to select screen recording options */
class ScreenRecordPermissionDialog(
context: Context?,
+ private val hostUserHandle: UserHandle,
private val controller: RecordingController,
private val activityStarter: ActivityStarter,
private val dialogLaunchAnimator: DialogLaunchAnimator,
@@ -79,11 +80,9 @@
CaptureTargetResultReceiver()
)
- // Send SystemUI's user handle as the host app user handle because SystemUI
- // is the 'host app' (the app that receives screen capture data)
intent.putExtra(
MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
- UserHandle.of(UserHandle.myUserId())
+ hostUserHandle
)
val animationController = dialogLaunchAnimator.createActivityLaunchController(v!!)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
index c891686..7a62bae 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
@@ -46,6 +46,8 @@
SCREENSHOT_SAVED(306),
@UiEvent(doc = "screenshot failed to save")
SCREENSHOT_NOT_SAVED(336),
+ @UiEvent(doc = "failed to capture screenshot")
+ SCREENSHOT_CAPTURE_FAILED(1281),
@UiEvent(doc = "screenshot preview tapped")
SCREENSHOT_PREVIEW_TAPPED(307),
@UiEvent(doc = "screenshot edit button tapped")
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index 4214c8f..8035d19 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -26,6 +26,7 @@
import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
import static com.android.systemui.screenshot.LogConfig.DEBUG_SERVICE;
import static com.android.systemui.screenshot.LogConfig.logTag;
+import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED;
import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER;
import android.annotation.MainThread;
@@ -202,6 +203,7 @@
// animation and error notification.
if (!mUserManager.isUserUnlocked()) {
Log.w(TAG, "Skipping screenshot because storage is locked!");
+ logFailedRequest(request);
mNotificationsController.notifyScreenshotError(
R.string.screenshot_failed_to_save_user_locked_text);
callback.reportError();
@@ -212,6 +214,7 @@
mBgExecutor.execute(() -> {
Log.w(TAG, "Skipping screenshot because an IT admin has disabled "
+ "screenshots on the device");
+ logFailedRequest(request);
String blockedByAdminText = mDevicePolicyManager.getResources().getString(
SCREENSHOT_BLOCKED_BY_ADMIN,
() -> mContext.getString(R.string.screenshot_blocked_by_admin));
@@ -225,38 +228,43 @@
if (mFeatureFlags.isEnabled(Flags.SCREENSHOT_METADATA)) {
Log.d(TAG, "Processing screenshot data");
ScreenshotData screenshotData = ScreenshotData.fromRequest(request);
- mProcessor.processAsync(screenshotData,
- (data) -> dispatchToController(data, onSaved, callback));
+ try {
+ mProcessor.processAsync(screenshotData,
+ (data) -> dispatchToController(data, onSaved, callback));
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to process screenshot request!", e);
+ logFailedRequest(request);
+ mNotificationsController.notifyScreenshotError(
+ R.string.screenshot_failed_to_capture_text);
+ callback.reportError();
+ }
} else {
- mProcessor.processAsync(request,
- (r) -> dispatchToController(r, onSaved, callback));
+ try {
+ mProcessor.processAsync(request,
+ (r) -> dispatchToController(r, onSaved, callback));
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to process screenshot request!", e);
+ logFailedRequest(request);
+ mNotificationsController.notifyScreenshotError(
+ R.string.screenshot_failed_to_capture_text);
+ callback.reportError();
+ }
}
}
private void dispatchToController(ScreenshotData screenshot,
Consumer<Uri> uriConsumer, RequestCallback callback) {
-
mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshot.getSource()), 0,
screenshot.getPackageNameString());
-
- if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
- && screenshot.getBitmap() == null) {
- Log.e(TAG, "Got null bitmap from screenshot message");
- mNotificationsController.notifyScreenshotError(
- R.string.screenshot_failed_to_capture_text);
- callback.reportError();
- return;
- }
-
mScreenshot.handleScreenshot(screenshot, uriConsumer, callback);
}
private void dispatchToController(ScreenshotRequest request,
Consumer<Uri> uriConsumer, RequestCallback callback) {
-
ComponentName topComponent = request.getTopComponent();
- mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(request.getSource()), 0,
- topComponent == null ? "" : topComponent.getPackageName());
+ String packageName = topComponent == null ? "" : topComponent.getPackageName();
+ mUiEventLogger.log(
+ ScreenshotEvent.getScreenshotSource(request.getSource()), 0, packageName);
switch (request.getType()) {
case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
@@ -275,21 +283,22 @@
int taskId = request.getTaskId();
int userId = request.getUserId();
- if (screenshot == null) {
- Log.e(TAG, "Got null bitmap from screenshot message");
- mNotificationsController.notifyScreenshotError(
- R.string.screenshot_failed_to_capture_text);
- callback.reportError();
- } else {
- mScreenshot.handleImageAsScreenshot(screenshot, screenBounds, insets,
- taskId, userId, topComponent, uriConsumer, callback);
- }
+ mScreenshot.handleImageAsScreenshot(screenshot, screenBounds, insets,
+ taskId, userId, topComponent, uriConsumer, callback);
break;
default:
- Log.w(TAG, "Invalid screenshot option: " + request.getType());
+ Log.wtf(TAG, "Invalid screenshot option: " + request.getType());
}
}
+ private void logFailedRequest(ScreenshotRequest request) {
+ ComponentName topComponent = request.getTopComponent();
+ String packageName = topComponent == null ? "" : topComponent.getPackageName();
+ mUiEventLogger.log(
+ ScreenshotEvent.getScreenshotSource(request.getSource()), 0, packageName);
+ mUiEventLogger.log(SCREENSHOT_CAPTURE_FAILED, 0, packageName);
+ }
+
private static void sendComplete(Messenger target) {
try {
if (DEBUG_CALLBACK) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
index 66b7842..1b728b8 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
@@ -67,10 +67,7 @@
}
// If label wasn't loaded, use a default
- val badgedLabel =
- packageManager.getUserBadgedLabel(label ?: defaultFileAppName(), userHandle)
-
- return WorkProfileFirstRunData(badgedLabel, badgedIcon)
+ return WorkProfileFirstRunData(label ?: defaultFileAppName(), badgedIcon)
}
return null
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 469f383..41846f0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -685,6 +685,7 @@
private boolean mInstantExpanding;
private boolean mAnimateAfterExpanding;
private boolean mIsFlinging;
+ private boolean mLastFlingWasExpanding;
private String mViewName;
private float mInitialExpandY;
private float mInitialExpandX;
@@ -2142,6 +2143,7 @@
@VisibleForTesting
void flingToHeight(float vel, boolean expand, float target,
float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
+ mLastFlingWasExpanding = expand;
mHeadsUpTouchHelper.notifyFling(!expand);
mKeyguardStateController.notifyPanelFlingStart(!expand /* flingingToDismiss */);
setClosingWithAlphaFadeout(!expand && !isOnKeyguard() && getFadeoutAlpha() == 1.0f);
@@ -2531,7 +2533,7 @@
}
// defer touches on QQS to shade while shade is collapsing. Added margin for error
// as sometimes the qsExpansionFraction can be a tiny value instead of 0 when in QQS.
- if (!mSplitShadeEnabled
+ if (!mSplitShadeEnabled && !mLastFlingWasExpanding
&& computeQsExpansionFraction() <= 0.01 && getExpandedFraction() < 1.0) {
mShadeLog.logMotionEvent(event,
"handleQsTouch: shade touched while collapsing, QS tracking disabled");
@@ -3151,17 +3153,11 @@
}
// The padding on this area is large enough that we can use a cheaper clipping strategy
mKeyguardStatusViewController.setClipBounds(clipStatusView ? mLastQsClipBounds : null);
- if (!qsVisible && mSplitShadeEnabled) {
- // On the lockscreen when qs isn't visible, we don't want the bounds of the shade to
- // be visible, otherwise you can see the bounds once swiping up to see bouncer
- mScrimController.setNotificationsBounds(0, 0, 0, 0);
- } else {
- // Increase the height of the notifications scrim when not in split shade
- // (e.g. portrait tablet) so the rounded corners are not visible at the bottom,
- // in this case they are rendered off-screen
- final int notificationsScrimBottom = mSplitShadeEnabled ? bottom : bottom + radius;
- mScrimController.setNotificationsBounds(left, top, right, notificationsScrimBottom);
- }
+ // Increase the height of the notifications scrim when not in split shade
+ // (e.g. portrait tablet) so the rounded corners are not visible at the bottom,
+ // in this case they are rendered off-screen
+ final int notificationsScrimBottom = mSplitShadeEnabled ? bottom : bottom + radius;
+ mScrimController.setNotificationsBounds(left, top, right, notificationsScrimBottom);
if (mSplitShadeEnabled) {
mKeyguardStatusBarViewController.setNoTopClipping();
@@ -3222,6 +3218,12 @@
private int calculateQsBottomPosition(float qsExpansionFraction) {
if (mTransitioningToFullShadeProgress > 0.0f) {
return mTransitionToFullShadeQSPosition;
+ } else if (mSplitShadeEnabled) {
+ // in split shade - outside lockscreen transition handled above - we simply jump between
+ // two qs expansion values - either shade is closed and qs expansion is 0 or shade is
+ // open and qs expansion is 1
+ int qsBottomTarget = mQs.getDesiredHeight() + mLargeScreenShadeHeaderHeight;
+ return qsExpansionFraction > 0 ? qsBottomTarget : 0;
} else {
int qsBottomYFrom = (int) getHeaderTranslation() + mQs.getQsMinExpansionHeight();
int expandedTopMargin = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight : 0;
@@ -4125,7 +4127,7 @@
if (didFaceAuthRun) {
mUpdateMonitor.requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
"lockScreenEmptySpaceTap");
} else {
mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT,
@@ -6135,6 +6137,11 @@
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
+ if (mTracking) {
+ // TODO(b/247126247) fix underlying issue. Should be ACTION_POINTER_DOWN.
+ mShadeLog.d("Don't intercept down event while already tracking");
+ return false;
+ }
mCentralSurfaces.userActivity();
mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
mMinExpandHeight = 0.0f;
@@ -6222,6 +6229,11 @@
"onTouch: duplicate down event detected... ignoring");
return true;
}
+ if (mTracking) {
+ // TODO(b/247126247) fix underlying issue. Should be ACTION_POINTER_DOWN.
+ mShadeLog.d("Don't handle down event while already tracking");
+ return true;
+ }
mLastTouchDownTime = event.getDownTime();
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/OWNERS b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
index 49709a8..c8b6a2e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
@@ -7,6 +7,7 @@
per-file NotificationsQuickSettingsContainer.java = kozynski@google.com, asc@google.com
per-file NotificationsQSContainerController.kt = kozynski@google.com, asc@google.com
per-file *ShadeHeader* = kozynski@google.com, asc@google.com
+per-file *Shade* = justinweir@google.com
per-file NotificationShadeWindowViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com
per-file NotificationShadeWindowView.java = pixel@google.com, cinek@google.com, juliacr@google.com
@@ -14,4 +15,4 @@
per-file NotificationPanelUnfoldAnimationController.kt = alexflo@google.com, jeffdq@google.com, juliacr@google.com
per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com
-per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com
\ No newline at end of file
+per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
index 393279b..641131e 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
@@ -44,6 +44,11 @@
const val DREAM_SMARTSPACE_PRECONDITION = "dream_smartspace_precondition"
/**
+ * The BcSmartspaceDataPlugin for the standalone date (+alarm+dnd).
+ */
+ const val DATE_SMARTSPACE_DATA_PLUGIN = "date_smartspace_data_plugin"
+
+ /**
* The BcSmartspaceDataPlugin for the standalone weather.
*/
const val WEATHER_SMARTSPACE_DATA_PLUGIN = "weather_smartspace_data_plugin"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 6481855..4bf84f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -26,6 +26,7 @@
import static android.view.View.VISIBLE;
import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
+import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED;
import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser;
import static com.android.systemui.DejankUtils.whitelistIpcs;
import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.IMPORTANT_MSG_MIN_DURATION;
@@ -539,23 +540,23 @@
.build(),
true
);
- if (!TextUtils.isEmpty(mBiometricMessageFollowUp)) {
- mRotateTextViewController.updateIndication(
- INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
- new KeyguardIndication.Builder()
- .setMessage(mBiometricMessageFollowUp)
- .setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION)
- .setTextColor(mInitialTextColorState)
- .build(),
- true
- );
- } else {
- mRotateTextViewController.hideIndication(
- INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP);
- }
} else {
- mRotateTextViewController.hideIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE);
- mRotateTextViewController.hideIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP);
+ mRotateTextViewController.hideIndication(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE);
+ }
+ if (!TextUtils.isEmpty(mBiometricMessageFollowUp)) {
+ mRotateTextViewController.updateIndication(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+ new KeyguardIndication.Builder()
+ .setMessage(mBiometricMessageFollowUp)
+ .setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION)
+ .setTextColor(mInitialTextColorState)
+ .build(),
+ true
+ );
+ } else {
+ mRotateTextViewController.hideIndication(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP);
}
}
@@ -784,7 +785,8 @@
*/
private void showBiometricMessage(CharSequence biometricMessage,
@Nullable CharSequence biometricMessageFollowUp) {
- if (TextUtils.equals(biometricMessage, mBiometricMessage)) {
+ if (TextUtils.equals(biometricMessage, mBiometricMessage)
+ && TextUtils.equals(biometricMessageFollowUp, mBiometricMessageFollowUp)) {
return;
}
@@ -793,7 +795,8 @@
mHandler.removeMessages(MSG_SHOW_ACTION_TO_UNLOCK);
hideBiometricMessageDelayed(
- mBiometricMessageFollowUp != null
+ !TextUtils.isEmpty(mBiometricMessage)
+ && !TextUtils.isEmpty(mBiometricMessageFollowUp)
? IMPORTANT_MSG_MIN_DURATION * 2
: DEFAULT_HIDE_DELAY_MS
);
@@ -1091,6 +1094,8 @@
&& msgId != BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
final boolean faceAuthFailed = biometricSourceType == FACE
&& msgId == BIOMETRIC_HELP_FACE_NOT_RECOGNIZED; // ran through matcher & failed
+ final boolean fpAuthFailed = biometricSourceType == FINGERPRINT
+ && msgId == BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED; // ran matcher & failed
final boolean isUnlockWithFingerprintPossible = canUnlockWithFingerprint();
final boolean isCoExFaceAcquisitionMessage =
faceAuthSoftError && isUnlockWithFingerprintPossible;
@@ -1113,6 +1118,22 @@
mContext.getString(R.string.keyguard_face_failed),
mContext.getString(R.string.keyguard_suggest_fingerprint)
);
+ } else if (fpAuthFailed
+ && mKeyguardUpdateMonitor.getUserUnlockedWithFace(getCurrentUser())) {
+ // face had already previously unlocked the device, so instead of showing a
+ // fingerprint error, tell them they have already unlocked with face auth
+ // and how to enter their device
+ showBiometricMessage(
+ mContext.getString(R.string.keyguard_face_successful_unlock),
+ mContext.getString(R.string.keyguard_unlock)
+ );
+ } else if (fpAuthFailed
+ && mKeyguardUpdateMonitor.getUserHasTrust(
+ KeyguardUpdateMonitor.getCurrentUser())) {
+ showBiometricMessage(
+ getTrustGrantedIndication(),
+ mContext.getString(R.string.keyguard_unlock)
+ );
} else {
showBiometricMessage(helpString);
}
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 f4ca9cc..5440fcc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -35,6 +35,7 @@
import android.view.ContextThemeWrapper
import android.view.View
import android.view.ViewGroup
+import com.android.keyguard.KeyguardUpdateMonitor
import com.android.settingslib.Utils
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
@@ -52,12 +53,15 @@
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.regionsampling.RegionSampler
import com.android.systemui.shared.regionsampling.UpdateColorCallback
+import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DATE_SMARTSPACE_DATA_PLUGIN
import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.WEATHER_SMARTSPACE_DATA_PLUGIN
+import com.android.systemui.statusbar.Weather
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.util.concurrency.Execution
import com.android.systemui.util.settings.SecureSettings
+import java.time.Instant
import java.util.Optional
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -80,10 +84,13 @@
private val statusBarStateController: StatusBarStateController,
private val deviceProvisionedController: DeviceProvisionedController,
private val bypassController: KeyguardBypassController,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val execution: Execution,
@Main private val uiExecutor: Executor,
@Background private val bgExecutor: Executor,
@Main private val handler: Handler,
+ @Named(DATE_SMARTSPACE_DATA_PLUGIN)
+ optionalDatePlugin: Optional<BcSmartspaceDataPlugin>,
@Named(WEATHER_SMARTSPACE_DATA_PLUGIN)
optionalWeatherPlugin: Optional<BcSmartspaceDataPlugin>,
optionalPlugin: Optional<BcSmartspaceDataPlugin>,
@@ -94,6 +101,7 @@
}
private var session: SmartspaceSession? = null
+ private val datePlugin: BcSmartspaceDataPlugin? = optionalDatePlugin.orElse(null)
private val weatherPlugin: BcSmartspaceDataPlugin? = optionalWeatherPlugin.orElse(null)
private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null)
private val configPlugin: BcSmartspaceConfigPlugin? = optionalConfigPlugin.orElse(null)
@@ -160,6 +168,17 @@
}
isContentUpdatedOnce = true
}
+
+ val now = Instant.now()
+ val weatherTarget = targets.find { t ->
+ t.featureType == SmartspaceTarget.FEATURE_WEATHER &&
+ now.isAfter(Instant.ofEpochMilli(t.creationTimeMillis)) &&
+ now.isBefore(Instant.ofEpochMilli(t.expiryTimeMillis))
+ }
+ if (weatherTarget != null) {
+ val weatherData = Weather.fromBundle(weatherTarget.baseAction.extras)
+ keyguardUpdateMonitor.sendWeatherData(weatherData)
+ }
}
private val userTrackerCallback = object : UserTracker.Callback {
@@ -222,7 +241,7 @@
execution.assertIsMainThread()
return featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED) &&
- weatherPlugin != null
+ datePlugin != null && weatherPlugin != null
}
private fun updateBypassEnabled() {
@@ -231,6 +250,25 @@
}
/**
+ * Constructs the date view and connects it to the smartspace service.
+ */
+ fun buildAndConnectDateView(parent: ViewGroup): View? {
+ execution.assertIsMainThread()
+
+ if (!isEnabled()) {
+ throw RuntimeException("Cannot build view when not enabled")
+ }
+ if (!isDateWeatherDecoupled()) {
+ throw RuntimeException("Cannot build date view when not decoupled")
+ }
+
+ val view = buildView(parent, datePlugin)
+ connectSession()
+
+ return view
+ }
+
+ /**
* Constructs the weather view and connects it to the smartspace service.
*/
fun buildAndConnectWeatherView(parent: ViewGroup): View? {
@@ -308,7 +346,7 @@
}
private fun connectSession() {
- if (weatherPlugin == null && plugin == null) return
+ if (datePlugin == null && weatherPlugin == null && plugin == null) return
if (session != null || smartspaceViews.isEmpty()) {
return
}
@@ -346,6 +384,7 @@
statusBarStateController.addCallback(statusBarStateListener)
bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener)
+ datePlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
plugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
@@ -383,6 +422,8 @@
bypassController.unregisterOnBypassStateChangedListener(bypassStateChangedListener)
session = null
+ datePlugin?.registerSmartspaceEventNotifier(null)
+
weatherPlugin?.registerSmartspaceEventNotifier(null)
weatherPlugin?.onTargetsAvailable(emptyList())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
index 58f59be..a37bbbc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
@@ -62,7 +62,7 @@
* are not.
*/
public class NotificationLogger implements StateListener {
- private static final String TAG = "NotificationLogger";
+ static final String TAG = "NotificationLogger";
private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
/** The minimum delay in ms between reports of notification visibility. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
index ec8501a..cc1103d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
@@ -18,6 +18,7 @@
package com.android.systemui.statusbar.notification.logging
import android.app.StatsManager
+import android.util.Log
import android.util.StatsEvent
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -25,6 +26,7 @@
import com.android.systemui.shared.system.SysUiStatsLog
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.util.traceSection
+import java.lang.Exception
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlin.math.roundToInt
@@ -82,43 +84,56 @@
return StatsManager.PULL_SKIP
}
- // Notifications can only be retrieved on the main thread, so switch to that thread.
- val notifications = getAllNotificationsOnMainThread()
- val notificationMemoryUse =
- NotificationMemoryMeter.notificationMemoryUse(notifications)
- .sortedWith(
- compareBy(
- { it.packageName },
- { it.objectUsage.style },
- { it.notificationKey }
+ try {
+ // Notifications can only be retrieved on the main thread, so switch to that thread.
+ val notifications = getAllNotificationsOnMainThread()
+ val notificationMemoryUse =
+ NotificationMemoryMeter.notificationMemoryUse(notifications)
+ .sortedWith(
+ compareBy(
+ { it.packageName },
+ { it.objectUsage.style },
+ { it.notificationKey }
+ )
+ )
+ val usageData = aggregateMemoryUsageData(notificationMemoryUse)
+ usageData.forEach { (_, use) ->
+ data.add(
+ SysUiStatsLog.buildStatsEvent(
+ SysUiStatsLog.NOTIFICATION_MEMORY_USE,
+ use.uid,
+ use.style,
+ use.count,
+ use.countWithInflatedViews,
+ toKb(use.smallIconObject),
+ use.smallIconBitmapCount,
+ toKb(use.largeIconObject),
+ use.largeIconBitmapCount,
+ toKb(use.bigPictureObject),
+ use.bigPictureBitmapCount,
+ toKb(use.extras),
+ toKb(use.extenders),
+ toKb(use.smallIconViews),
+ toKb(use.largeIconViews),
+ toKb(use.systemIconViews),
+ toKb(use.styleViews),
+ toKb(use.customViews),
+ toKb(use.softwareBitmaps),
+ use.seenCount
)
)
- val usageData = aggregateMemoryUsageData(notificationMemoryUse)
- usageData.forEach { (_, use) ->
- data.add(
- SysUiStatsLog.buildStatsEvent(
- SysUiStatsLog.NOTIFICATION_MEMORY_USE,
- use.uid,
- use.style,
- use.count,
- use.countWithInflatedViews,
- toKb(use.smallIconObject),
- use.smallIconBitmapCount,
- toKb(use.largeIconObject),
- use.largeIconBitmapCount,
- toKb(use.bigPictureObject),
- use.bigPictureBitmapCount,
- toKb(use.extras),
- toKb(use.extenders),
- toKb(use.smallIconViews),
- toKb(use.largeIconViews),
- toKb(use.systemIconViews),
- toKb(use.styleViews),
- toKb(use.customViews),
- toKb(use.softwareBitmaps),
- use.seenCount
- )
- )
+ }
+ } catch (e: InterruptedException) {
+ // This can happen if the device is sleeping or view walking takes too long.
+ // The statsd collector will interrupt the thread and we need to handle it
+ // gracefully.
+ Log.w(NotificationLogger.TAG, "Timed out when measuring notification memory.", e)
+ return@traceSection StatsManager.PULL_SKIP
+ } catch (e: Exception) {
+ // Error while collecting data, this should not crash prod SysUI. Just
+ // log WTF and move on.
+ Log.wtf(NotificationLogger.TAG, "Failed to measure notification memory.", e)
+ return@traceSection StatsManager.PULL_SKIP
}
return StatsManager.PULL_SUCCESS
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
index 2d04211..6491223 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
@@ -184,19 +184,21 @@
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
- }
+ drawable.bitmap?.let {
+ val ref = System.identityHashCode(it)
+ if (seenObjects.contains(ref)) {
+ 0
+ } else {
+ seenObjects.add(ref)
+ it.allocationByteCount
+ }
+ } ?: 0
}
else -> 0
}
private fun isDrawableSoftwareBitmap(drawable: Drawable) =
- drawable is BitmapDrawable && drawable.bitmap.config != Bitmap.Config.HARDWARE
+ drawable is BitmapDrawable && drawable.bitmap?.config != Bitmap.Config.HARDWARE
private fun identifierForView(view: View) =
if (view.id == View.NO_ID) {
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 9275e2b..a6b71dc 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
@@ -591,6 +591,7 @@
}
mShowingPublicInitialized = false;
updateNotificationColor();
+ updateLongClickable();
if (mMenuRow != null) {
mMenuRow.onNotificationUpdated(mEntry.getSbn());
mMenuRow.setAppName(mAppName);
@@ -1196,8 +1197,26 @@
return getShowingLayout().getVisibleWrapper();
}
+ private boolean isNotificationRowLongClickable() {
+ if (mLongPressListener == null) {
+ return false;
+ }
+
+ if (!areGutsExposed()) { // guts is not opened
+ return true;
+ }
+
+ // if it is leave behind, it shouldn't be long clickable.
+ return !isGutsLeaveBehind();
+ }
+
+ private void updateLongClickable() {
+ setLongClickable(isNotificationRowLongClickable());
+ }
+
public void setLongPressListener(LongPressListener longPressListener) {
mLongPressListener = longPressListener;
+ updateLongClickable();
}
public void setDragController(ExpandableNotificationRowDragController dragController) {
@@ -2044,11 +2063,13 @@
void onGutsOpened() {
resetTranslation();
updateContentAccessibilityImportanceForGuts(false /* isEnabled */);
+ updateLongClickable();
}
void onGutsClosed() {
updateContentAccessibilityImportanceForGuts(true /* isEnabled */);
mIsSnoozed = false;
+ updateLongClickable();
}
/**
@@ -2947,6 +2968,10 @@
return (mGuts != null && mGuts.isExposed());
}
+ private boolean isGutsLeaveBehind() {
+ return (mGuts != null && mGuts.isLeavebehind());
+ }
+
@Override
public boolean isContentExpandable() {
if (mIsSummaryWithChildren && !shouldShowPublic()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index 37ff11d..efcbb3c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -586,7 +586,9 @@
}
final ExpandableNotificationRow row = (ExpandableNotificationRow) view;
- view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ if (view.isLongClickable()) {
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
if (row.areGutsExposed()) {
closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
true /* removeControls */, -1 /* x */, -1 /* y */,
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 aab36da..1fb7eb5 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
@@ -147,7 +147,7 @@
private boolean mShadeNeedsToClose = false;
@VisibleForTesting
- static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
+ static final float RUBBER_BAND_FACTOR_NORMAL = 0.1f;
private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
index c248a50..b88531e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -352,7 +352,7 @@
}
private boolean willAnimateFromLockScreenToAod() {
- return getAlwaysOn() && mKeyguardVisible;
+ return shouldControlScreenOff() && mKeyguardVisible;
}
private boolean getBoolean(String propName, int resId) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
index 01af486..c163a89 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -89,14 +89,19 @@
private float mPanelExpansion;
/**
- * Burn-in prevention x translation.
+ * Max burn-in prevention x translation.
*/
- private int mBurnInPreventionOffsetX;
+ private int mMaxBurnInPreventionOffsetX;
/**
- * Burn-in prevention y translation for clock layouts.
+ * Max burn-in prevention y translation for clock layouts.
*/
- private int mBurnInPreventionOffsetYClock;
+ private int mMaxBurnInPreventionOffsetYClock;
+
+ /**
+ * Current burn-in prevention y translation.
+ */
+ private float mCurrentBurnInOffsetY;
/**
* Doze/AOD transition amount.
@@ -155,9 +160,9 @@
mContainerTopPadding =
res.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
- mBurnInPreventionOffsetX = res.getDimensionPixelSize(
+ mMaxBurnInPreventionOffsetX = res.getDimensionPixelSize(
R.dimen.burn_in_prevention_offset_x);
- mBurnInPreventionOffsetYClock = res.getDimensionPixelSize(
+ mMaxBurnInPreventionOffsetYClock = res.getDimensionPixelSize(
R.dimen.burn_in_prevention_offset_y_clock);
}
@@ -215,7 +220,10 @@
if (mBypassEnabled) {
return (int) (mUnlockedStackScrollerPadding + mOverStretchAmount);
} else if (mIsSplitShade) {
- return clockYPosition - mSplitShadeTopNotificationsMargin + mUserSwitchHeight;
+ // mCurrentBurnInOffsetY is subtracted to make notifications not follow clock adjustment
+ // for burn-in. It can make pulsing notification go too high and it will get clipped
+ return clockYPosition - mSplitShadeTopNotificationsMargin + mUserSwitchHeight
+ - (int) mCurrentBurnInOffsetY;
} else {
return clockYPosition + mKeyguardStatusHeight;
}
@@ -255,11 +263,11 @@
// This will keep the clock at the top but out of the cutout area
float shift = 0;
- if (clockY - mBurnInPreventionOffsetYClock < mCutoutTopInset) {
- shift = mCutoutTopInset - (clockY - mBurnInPreventionOffsetYClock);
+ if (clockY - mMaxBurnInPreventionOffsetYClock < mCutoutTopInset) {
+ shift = mCutoutTopInset - (clockY - mMaxBurnInPreventionOffsetYClock);
}
- int burnInPreventionOffsetY = mBurnInPreventionOffsetYClock; // requested offset
+ int burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock; // requested offset
final boolean hasUdfps = mUdfpsTop > -1;
if (hasUdfps && !mIsClockTopAligned) {
// ensure clock doesn't overlap with the udfps icon
@@ -267,8 +275,8 @@
// sometimes the clock textView extends beyond udfps, so let's just use the
// space above the KeyguardStatusView/clock as our burn-in offset
burnInPreventionOffsetY = (int) (clockY - mCutoutTopInset) / 2;
- if (mBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
- burnInPreventionOffsetY = mBurnInPreventionOffsetYClock;
+ if (mMaxBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
+ burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock;
}
shift = -burnInPreventionOffsetY;
} else {
@@ -276,16 +284,18 @@
float lowerSpace = mUdfpsTop - mClockBottom;
// center the burn-in offset within the upper + lower space
burnInPreventionOffsetY = (int) (lowerSpace + upperSpace) / 2;
- if (mBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
- burnInPreventionOffsetY = mBurnInPreventionOffsetYClock;
+ if (mMaxBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
+ burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock;
}
shift = (lowerSpace - upperSpace) / 2;
}
}
+ float fullyDarkBurnInOffset = burnInPreventionOffsetY(burnInPreventionOffsetY);
float clockYDark = clockY
- + burnInPreventionOffsetY(burnInPreventionOffsetY)
+ + fullyDarkBurnInOffset
+ shift;
+ mCurrentBurnInOffsetY = MathUtils.lerp(0, fullyDarkBurnInOffset, darkAmount);
return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mOverStretchAmount);
}
@@ -325,7 +335,7 @@
}
private float burnInPreventionOffsetX() {
- return getBurnInOffset(mBurnInPreventionOffsetX, true /* xAxis */);
+ return getBurnInOffset(mMaxBurnInPreventionOffsetX, true /* xAxis */);
}
public static class Result {
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 4550cb2..8ee2c6f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
@@ -76,7 +76,7 @@
FaceAuthApiRequestReason.PICK_UP_GESTURE_TRIGGERED
)
keyguardUpdateMonitor.requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE,
"KeyguardLiftController")
}
}
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 344d233..c1c6c88 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
@@ -23,6 +23,7 @@
import android.view.View;
import android.view.ViewStub;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.LockIconView;
import com.android.systemui.R;
import com.android.systemui.battery.BatteryMeterView;
@@ -67,6 +68,7 @@
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.util.CarrierConfigTracker;
import com.android.systemui.util.settings.SecureSettings;
@@ -299,7 +301,9 @@
OperatorNameViewController.Factory operatorNameViewControllerFactory,
SecureSettings secureSettings,
@Main Executor mainExecutor,
- DumpManager dumpManager
+ DumpManager dumpManager,
+ StatusBarWindowStateController statusBarWindowStateController,
+ KeyguardUpdateMonitor keyguardUpdateMonitor
) {
return new CollapsedStatusBarFragment(statusBarFragmentComponentFactory,
ongoingCallController,
@@ -320,7 +324,9 @@
operatorNameViewControllerFactory,
secureSettings,
mainExecutor,
- dumpManager);
+ dumpManager,
+ statusBarWindowStateController,
+ keyguardUpdateMonitor);
}
/**
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 9354c5e..00fd4ef 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
@@ -44,6 +44,7 @@
import androidx.annotation.VisibleForTesting;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
@@ -72,6 +73,8 @@
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
import com.android.systemui.util.CarrierConfigTracker;
import com.android.systemui.util.CarrierConfigTracker.CarrierConfigChangedListener;
import com.android.systemui.util.CarrierConfigTracker.DefaultDataSubscriptionChangedListener;
@@ -129,6 +132,8 @@
private final SecureSettings mSecureSettings;
private final Executor mMainExecutor;
private final DumpManager mDumpManager;
+ private final StatusBarWindowStateController mStatusBarWindowStateController;
+ private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private List<String> mBlockedIcons = new ArrayList<>();
private Map<Startable, Startable.State> mStartableStates = new ArrayMap<>();
@@ -164,6 +169,22 @@
}
};
+ /**
+ * Whether we've launched the secure camera over the lockscreen, but haven't yet received a
+ * status bar window state change afterward.
+ *
+ * We wait for this state change (which will tell us whether to show/hide the status bar icons)
+ * so that there is no flickering/jump cutting during the camera launch.
+ */
+ private boolean mWaitingForWindowStateChangeAfterCameraLaunch = false;
+
+ /**
+ * Listener that updates {@link #mWaitingForWindowStateChangeAfterCameraLaunch} when it receives
+ * a new status bar window state.
+ */
+ private final StatusBarWindowStateListener mStatusBarWindowStateListener = state ->
+ mWaitingForWindowStateChangeAfterCameraLaunch = false;
+
@SuppressLint("ValidFragment")
public CollapsedStatusBarFragment(
StatusBarFragmentComponent.Factory statusBarFragmentComponentFactory,
@@ -185,7 +206,9 @@
OperatorNameViewController.Factory operatorNameViewControllerFactory,
SecureSettings secureSettings,
@Main Executor mainExecutor,
- DumpManager dumpManager
+ DumpManager dumpManager,
+ StatusBarWindowStateController statusBarWindowStateController,
+ KeyguardUpdateMonitor keyguardUpdateMonitor
) {
mStatusBarFragmentComponentFactory = statusBarFragmentComponentFactory;
mOngoingCallController = ongoingCallController;
@@ -207,6 +230,20 @@
mSecureSettings = secureSettings;
mMainExecutor = mainExecutor;
mDumpManager = dumpManager;
+ mStatusBarWindowStateController = statusBarWindowStateController;
+ mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mStatusBarWindowStateController.addListener(mStatusBarWindowStateListener);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mStatusBarWindowStateController.removeListener(mStatusBarWindowStateListener);
}
@Override
@@ -254,6 +291,11 @@
mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
}
+ @Override
+ public void onCameraLaunchGestureDetected(int source) {
+ mWaitingForWindowStateChangeAfterCameraLaunch = true;
+ }
+
@VisibleForTesting
void updateBlockedIcons() {
mBlockedIcons.clear();
@@ -466,6 +508,27 @@
&& mNotificationPanelViewController.hideStatusBarIconsWhenExpanded()) {
return true;
}
+
+ // When launching the camera over the lockscreen, the icons become visible momentarily
+ // before animating out, since we're not yet aware that the launching camera activity is
+ // fullscreen. Even once the activity finishes launching, it takes a short time before WM
+ // decides that the top app wants to hide the icons and tells us to hide them. To ensure
+ // that this high-visibility animation is smooth, keep the icons hidden during a camera
+ // launch until we receive a window state change which indicates that the activity is done
+ // launching and WM has decided to show/hide the icons. For extra safety (to ensure the
+ // icons don't remain hidden somehow) we double check that the camera is still showing, the
+ // status bar window isn't hidden, and we're still occluded as well, though these checks
+ // are typically unnecessary.
+ final boolean hideIconsForSecureCamera =
+ (mWaitingForWindowStateChangeAfterCameraLaunch ||
+ !mStatusBarWindowStateController.windowIsShowing()) &&
+ mKeyguardUpdateMonitor.isSecureCameraLaunchedOverKeyguard() &&
+ mKeyguardStateController.isOccluded();
+
+ if (hideIconsForSecureCamera) {
+ return true;
+ }
+
return mStatusBarHideIconsForBouncerManager.getShouldHideStatusBarIconsForBouncer();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
index 5479b92..85729c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
@@ -20,16 +20,21 @@
import android.telephony.TelephonyManager.DATA_CONNECTING
import android.telephony.TelephonyManager.DATA_DISCONNECTED
import android.telephony.TelephonyManager.DATA_DISCONNECTING
+import android.telephony.TelephonyManager.DATA_HANDOVER_IN_PROGRESS
+import android.telephony.TelephonyManager.DATA_SUSPENDED
import android.telephony.TelephonyManager.DATA_UNKNOWN
import android.telephony.TelephonyManager.DataState
/** Internal enum representation of the telephony data connection states */
-enum class DataConnectionState(@DataState val dataState: Int) {
- Connected(DATA_CONNECTED),
- Connecting(DATA_CONNECTING),
- Disconnected(DATA_DISCONNECTED),
- Disconnecting(DATA_DISCONNECTING),
- Unknown(DATA_UNKNOWN),
+enum class DataConnectionState {
+ Connected,
+ Connecting,
+ Disconnected,
+ Disconnecting,
+ Suspended,
+ HandoverInProgress,
+ Unknown,
+ Invalid,
}
fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState =
@@ -38,6 +43,8 @@
DATA_CONNECTING -> DataConnectionState.Connecting
DATA_DISCONNECTED -> DataConnectionState.Disconnected
DATA_DISCONNECTING -> DataConnectionState.Disconnecting
+ DATA_SUSPENDED -> DataConnectionState.Suspended
+ DATA_HANDOVER_IN_PROGRESS -> DataConnectionState.HandoverInProgress
DATA_UNKNOWN -> DataConnectionState.Unknown
- else -> throw IllegalArgumentException("unknown data state received $this")
+ else -> DataConnectionState.Invalid
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
index 012b9ec..ed7f60b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
@@ -26,6 +26,7 @@
import android.telephony.TelephonyCallback.SignalStrengthsListener
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyManager
+import androidx.annotation.VisibleForTesting
import com.android.systemui.log.table.Diffable
import com.android.systemui.log.table.TableRowLogger
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected
@@ -94,7 +95,7 @@
) : Diffable<MobileConnectionModel> {
override fun logDiffs(prevVal: MobileConnectionModel, row: TableRowLogger) {
if (prevVal.dataConnectionState != dataConnectionState) {
- row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString())
+ row.logChange(COL_CONNECTION_STATE, dataConnectionState.name)
}
if (prevVal.isEmergencyOnly != isEmergencyOnly) {
@@ -125,8 +126,12 @@
row.logChange(COL_PRIMARY_LEVEL, primaryLevel)
}
- if (prevVal.dataActivityDirection != dataActivityDirection) {
- row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString())
+ if (prevVal.dataActivityDirection.hasActivityIn != dataActivityDirection.hasActivityIn) {
+ row.logChange(COL_ACTIVITY_DIRECTION_IN, dataActivityDirection.hasActivityIn)
+ }
+
+ if (prevVal.dataActivityDirection.hasActivityOut != dataActivityDirection.hasActivityOut) {
+ row.logChange(COL_ACTIVITY_DIRECTION_OUT, dataActivityDirection.hasActivityOut)
}
if (prevVal.carrierNetworkChangeActive != carrierNetworkChangeActive) {
@@ -139,7 +144,7 @@
}
override fun logFull(row: TableRowLogger) {
- row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString())
+ row.logChange(COL_CONNECTION_STATE, dataConnectionState.name)
row.logChange(COL_EMERGENCY, isEmergencyOnly)
row.logChange(COL_ROAMING, isRoaming)
row.logChange(COL_OPERATOR, operatorAlphaShort)
@@ -147,11 +152,13 @@
row.logChange(COL_IS_GSM, isGsm)
row.logChange(COL_CDMA_LEVEL, cdmaLevel)
row.logChange(COL_PRIMARY_LEVEL, primaryLevel)
- row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString())
+ row.logChange(COL_ACTIVITY_DIRECTION_IN, dataActivityDirection.hasActivityIn)
+ row.logChange(COL_ACTIVITY_DIRECTION_OUT, dataActivityDirection.hasActivityOut)
row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive)
row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString())
}
+ @VisibleForTesting
companion object {
const val COL_EMERGENCY = "EmergencyOnly"
const val COL_ROAMING = "Roaming"
@@ -161,7 +168,8 @@
const val COL_CDMA_LEVEL = "CdmaLevel"
const val COL_PRIMARY_LEVEL = "PrimaryLevel"
const val COL_CONNECTION_STATE = "ConnectionState"
- const val COL_ACTIVITY_DIRECTION = "DataActivity"
+ const val COL_ACTIVITY_DIRECTION_IN = "DataActivity.In"
+ const val COL_ACTIVITY_DIRECTION_OUT = "DataActivity.Out"
const val COL_CARRIER_NETWORK_CHANGE = "CarrierNetworkChangeActive"
const val COL_RESOLVED_NETWORK_TYPE = "NetworkType"
}
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 9ae38e9..0e4a432 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
@@ -28,6 +28,7 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
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.shared.ConnectivityPipelineLogger
import com.android.systemui.util.CarrierConfigTracker
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -37,10 +38,12 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
@@ -100,6 +103,7 @@
constructor(
private val mobileConnectionsRepo: MobileConnectionsRepository,
private val carrierConfigTracker: CarrierConfigTracker,
+ private val logger: ConnectivityPipelineLogger,
userSetupRepo: UserSetupRepository,
@Application private val scope: CoroutineScope,
) : MobileIconsInteractor {
@@ -168,6 +172,8 @@
}
}
}
+ .distinctUntilChanged()
+ .onEach { logger.logFilteredSubscriptionsChanged(it) }
override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
index 829a5ca..ef75713 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -23,6 +23,8 @@
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -30,7 +32,9 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -51,13 +55,17 @@
interactor: MobileIconsInteractor,
private val iconController: StatusBarIconController,
private val iconsViewModelFactory: MobileIconsViewModel.Factory,
+ private val logger: ConnectivityPipelineLogger,
@Application private val scope: CoroutineScope,
private val statusBarPipelineFlags: StatusBarPipelineFlags,
) : CoreStartable {
private val mobileSubIds: Flow<List<Int>> =
- interactor.filteredSubscriptions.mapLatest { subscriptions ->
- subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId }
- }
+ interactor.filteredSubscriptions
+ .mapLatest { subscriptions ->
+ subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId }
+ }
+ .distinctUntilChanged()
+ .onEach { logger.logUiAdapterSubIdsUpdated(it) }
/**
* We expose the list of tracked subscriptions as a flow of a list of ints, where each int is
@@ -72,6 +80,9 @@
/** In order to keep the logs tame, we will reuse the same top-level mobile icons view model */
val mobileIconsViewModel = iconsViewModelFactory.create(mobileSubIdsState)
+ private var isCollecting: Boolean = false
+ private var lastValue: List<Int>? = null
+
override fun start() {
// Only notify the icon controller if we want to *render* the new icons.
// Note that this flow may still run if
@@ -79,8 +90,18 @@
// get the logging data without rendering.
if (statusBarPipelineFlags.useNewMobileIcons()) {
scope.launch {
- mobileSubIds.collectLatest { iconController.setNewMobileIconSubIds(it) }
+ isCollecting = true
+ mobileSubIds.collectLatest {
+ logger.logUiAdapterSubIdsSentToIconController(it)
+ lastValue = it
+ iconController.setNewMobileIconSubIds(it)
+ }
}
}
}
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.println("isCollecting=$isCollecting")
+ pw.println("Last values sent to icon controller: $lastValue")
+ }
}
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 3e81c7c..a4b2abc 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
@@ -29,6 +29,7 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.settingslib.graph.SignalDrawable
import com.android.systemui.R
+import com.android.systemui.common.ui.binder.ContentDescriptionViewBinder
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.StatusBarIconView
@@ -97,6 +98,12 @@
}
}
+ launch {
+ viewModel.contentDescription.distinctUntilChanged().collect {
+ ContentDescriptionViewBinder.bind(it, view)
+ }
+ }
+
// Set the network type icon
launch {
viewModel.networkTypeIcon.distinctUntilChanged().collect { dataTypeId ->
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 5e935616..9e2024a 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
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH
+import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH_NONE
import com.android.settingslib.graph.SignalDrawable
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
@@ -43,6 +45,7 @@
val subscriptionId: Int
/** An int consumable by [SignalDrawable] for display */
val iconId: Flow<Int>
+ val contentDescription: Flow<ContentDescription>
val roaming: Flow<Boolean>
/** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
val networkTypeIcon: Flow<Icon?>
@@ -102,6 +105,23 @@
.stateIn(scope, SharingStarted.WhileSubscribed(), initial)
}
+ override val contentDescription: Flow<ContentDescription> = run {
+ val initial = ContentDescription.Resource(PHONE_SIGNAL_STRENGTH_NONE)
+ combine(
+ iconInteractor.level,
+ iconInteractor.isInService,
+ ) { level, isInService ->
+ val resId =
+ when {
+ isInService -> PHONE_SIGNAL_STRENGTH[level]
+ else -> PHONE_SIGNAL_STRENGTH_NONE
+ }
+ ContentDescription.Resource(resId)
+ }
+ .distinctUntilChanged()
+ .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+ }
+
private val showNetworkTypeIcon: Flow<Boolean> =
combine(
iconInteractor.isDataConnected,
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 491f3a5..7c7ffaf 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
@@ -25,6 +25,7 @@
import com.android.systemui.log.dagger.StatusBarConnectivityLog
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -201,6 +202,35 @@
)
}
+ // TODO(b/238425913): We should split this class into mobile-specific and wifi-specific loggers.
+
+ fun logFilteredSubscriptionsChanged(subs: List<SubscriptionModel>) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ { str1 = subs.toString() },
+ { "Filtered subscriptions updated: $str1" },
+ )
+ }
+
+ fun logUiAdapterSubIdsUpdated(subs: List<Int>) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ { str1 = subs.toString() },
+ { "Sub IDs in MobileUiAdapter updated internally: $str1" },
+ )
+ }
+
+ fun logUiAdapterSubIdsSentToIconController(subs: List<Int>) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ { str1 = subs.toString() },
+ { "Sub IDs in MobileUiAdapter being sent to icon controller: $str1" },
+ )
+ }
+
companion object {
const val SB_LOGGING_TAG = "SbConnectivity"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
index cc0ec54..b1e2812 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
@@ -77,6 +77,17 @@
return binding.getShouldIconBeVisible()
}
+ /** See [StatusBarIconView.getDrawingRect]. */
+ override fun getDrawingRect(outRect: Rect) {
+ super.getDrawingRect(outRect)
+ val translationX = translationX.toInt()
+ val translationY = translationY.toInt()
+ outRect.left += translationX
+ outRect.right += translationX
+ outRect.top += translationY
+ outRect.bottom += translationY
+ }
+
/**
* Initializes this view.
*
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index 8669047..c45b420 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -95,7 +95,7 @@
.logDiffsForTable(
wifiTableLogBuffer,
columnPrefix = "",
- columnName = "isWifiEnabled",
+ columnName = "isEnabled",
initialValue = wifiManager.isWifiEnabled,
)
.stateIn(
@@ -141,7 +141,7 @@
.logDiffsForTable(
wifiTableLogBuffer,
columnPrefix = "",
- columnName = "isWifiDefault",
+ columnName = "isDefault",
initialValue = false,
)
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
@@ -212,7 +212,7 @@
.distinctUntilChanged()
.logDiffsForTable(
wifiTableLogBuffer,
- columnPrefix = "wifiNetwork",
+ columnPrefix = "",
initialValue = WIFI_NETWORK_DEFAULT,
)
// There will be multiple wifi icons in different places that will frequently
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
index e491d2b..094bcf9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
@@ -53,4 +53,4 @@
}
}
-private const val COL_ICON = "wifiIcon"
+private const val COL_ICON = "icon"
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 cc6fdcc..9ad36fd5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -438,6 +438,11 @@
}
@Override
+ public void onLockedOutStateChanged(BiometricSourceType biometricSourceType) {
+ update(false /* updateAlways */);
+ }
+
+ @Override
public void onKeyguardVisibilityChanged(boolean visible) {
update(false /* updateAlways */);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
index 60f6df6..8f424b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
@@ -61,6 +61,10 @@
listeners.add(listener)
}
+ fun removeListener(listener: StatusBarWindowStateListener) {
+ listeners.remove(listener)
+ }
+
/** Returns true if the window is currently showing. */
fun windowIsShowing() = windowState == WINDOW_STATE_SHOWING
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
index ec6965a..899b0c2 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -80,6 +80,26 @@
)
}
+ /** Logs that there was a failure to animate the view in. */
+ fun logAnimateInFailure() {
+ buffer.log(
+ tag,
+ LogLevel.WARNING,
+ {},
+ { "View's appearance animation failed. Forcing view display manually." },
+ )
+ }
+
+ /** Logs that there was a failure to animate the view out. */
+ fun logAnimateOutFailure() {
+ buffer.log(
+ tag,
+ LogLevel.WARNING,
+ {},
+ { "View's disappearance animation failed." },
+ )
+ }
+
fun logViewHidden(info: T) {
buffer.log(
tag,
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarAnimator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarAnimator.kt
new file mode 100644
index 0000000..01a81de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarAnimator.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.temporarydisplay.chipbar
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.animation.ViewHierarchyAnimator
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.children
+import javax.inject.Inject
+
+/**
+ * A class controlling chipbar animations. Typically delegates to [ViewHierarchyAnimator].
+ *
+ * Used so that animations can be mocked out in tests.
+ */
+@SysUISingleton
+open class ChipbarAnimator @Inject constructor() {
+ /**
+ * Animates [innerView] and its children into view.
+ *
+ * @return true if the animation was successfully started and false if the animation can't be
+ * run for any reason.
+ *
+ * See [ViewHierarchyAnimator.animateAddition].
+ */
+ open fun animateViewIn(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+ return ViewHierarchyAnimator.animateAddition(
+ innerView,
+ ViewHierarchyAnimator.Hotspot.TOP,
+ Interpolators.EMPHASIZED_DECELERATE,
+ duration = ANIMATION_IN_DURATION,
+ includeMargins = true,
+ includeFadeIn = true,
+ onAnimationEnd = onAnimationEnd,
+ )
+ }
+
+ /**
+ * Animates [innerView] and its children out of view.
+ *
+ * @return true if the animation was successfully started and false if the animation can't be
+ * run for any reason.
+ *
+ * See [ViewHierarchyAnimator.animateRemoval].
+ */
+ open fun animateViewOut(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+ return ViewHierarchyAnimator.animateRemoval(
+ innerView,
+ ViewHierarchyAnimator.Hotspot.TOP,
+ Interpolators.EMPHASIZED_ACCELERATE,
+ ANIMATION_OUT_DURATION,
+ includeMargins = true,
+ onAnimationEnd,
+ )
+ }
+
+ /** Force shows this view and all child views. Should be used in case [animateViewIn] fails. */
+ fun forceDisplayView(innerView: View) {
+ innerView.alpha = 1f
+ if (innerView is ViewGroup) {
+ innerView.children.forEach { forceDisplayView(it) }
+ }
+ }
+}
+
+private const val ANIMATION_IN_DURATION = 500L
+private const val ANIMATION_OUT_DURATION = 250L
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index 46f13cc..696134c 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -32,8 +32,6 @@
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
@@ -78,6 +76,7 @@
configurationController: ConfigurationController,
dumpManager: DumpManager,
powerManager: PowerManager,
+ private val chipbarAnimator: ChipbarAnimator,
private val falsingManager: FalsingManager,
private val falsingCollector: FalsingCollector,
private val swipeChipbarAwayGestureHandler: SwipeChipbarAwayGestureHandler?,
@@ -206,23 +205,17 @@
}
override fun animateViewIn(view: ViewGroup) {
+ // We can only request focus once the animation finishes.
val onAnimationEnd = Runnable {
maybeGetAccessibilityFocus(view.getTag(INFO_TAG) as ChipbarInfo?, view)
}
- val added =
- ViewHierarchyAnimator.animateAddition(
- view.getInnerView(),
- ViewHierarchyAnimator.Hotspot.TOP,
- Interpolators.EMPHASIZED_DECELERATE,
- duration = ANIMATION_IN_DURATION,
- includeMargins = true,
- includeFadeIn = true,
- // We can only request focus once the animation finishes.
- onAnimationEnd = onAnimationEnd,
- )
- // If the view doesn't get animated, the [onAnimationEnd] runnable won't get run. So, just
- // run it immediately.
- if (!added) {
+ val animatedIn = chipbarAnimator.animateViewIn(view.getInnerView(), onAnimationEnd)
+
+ // If the view doesn't get animated, the [onAnimationEnd] runnable won't get run and the
+ // views would remain un-displayed. So, just force-set/run those items immediately.
+ if (!animatedIn) {
+ logger.logAnimateInFailure()
+ chipbarAnimator.forceDisplayView(view.getInnerView())
onAnimationEnd.run()
}
}
@@ -230,18 +223,11 @@
override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val innerView = view.getInnerView()
innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
- val removed =
- ViewHierarchyAnimator.animateRemoval(
- innerView,
- ViewHierarchyAnimator.Hotspot.TOP,
- Interpolators.EMPHASIZED_ACCELERATE,
- ANIMATION_OUT_DURATION,
- includeMargins = true,
- onAnimationEnd,
- )
+ val removed = chipbarAnimator.animateViewOut(innerView, onAnimationEnd)
// If the view doesn't get animated, the [onAnimationEnd] runnable won't get run. So, just
// run it immediately.
if (!removed) {
+ logger.logAnimateOutFailure()
onAnimationEnd.run()
}
@@ -299,8 +285,6 @@
}
}
-private const val ANIMATION_IN_DURATION = 500L
-private const val ANIMATION_OUT_DURATION = 250L
@IdRes private val INFO_TAG = R.id.tag_chipbar_info
private const val SWIPE_UP_GESTURE_REASON = "SWIPE_UP_GESTURE_DETECTED"
private const val TAG = "ChipbarCoordinator"
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index f29ca4d..2b7ea2a 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -357,15 +357,24 @@
};
@Inject
- public ThemeOverlayController(Context context, BroadcastDispatcher broadcastDispatcher,
- @Background Handler bgHandler, @Main Executor mainExecutor,
- @Background Executor bgExecutor, ThemeOverlayApplier themeOverlayApplier,
- SecureSettings secureSettings, WallpaperManager wallpaperManager,
- UserManager userManager, DeviceProvisionedController deviceProvisionedController,
- UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags,
- @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) {
+ public ThemeOverlayController(
+ Context context,
+ BroadcastDispatcher broadcastDispatcher,
+ @Background Handler bgHandler,
+ @Main Executor mainExecutor,
+ @Background Executor bgExecutor,
+ ThemeOverlayApplier themeOverlayApplier,
+ SecureSettings secureSettings,
+ WallpaperManager wallpaperManager,
+ UserManager userManager,
+ DeviceProvisionedController deviceProvisionedController,
+ UserTracker userTracker,
+ DumpManager dumpManager,
+ FeatureFlags featureFlags,
+ @Main Resources resources,
+ WakefulnessLifecycle wakefulnessLifecycle) {
mContext = context;
- mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEMES);
+ mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME);
mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
mDeviceProvisionedController = deviceProvisionedController;
mBroadcastDispatcher = broadcastDispatcher;
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 8cb4deb..e5ab473 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
@@ -17,8 +17,11 @@
package com.android.systemui.user.data.repository
+import android.app.IActivityManager
+import android.app.UserSwitchObserver
import android.content.Context
import android.content.pm.UserInfo
+import android.os.IRemoteCallback
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
@@ -30,6 +33,8 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
import com.android.systemui.settings.UserTracker
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.util.settings.GlobalSettings
@@ -68,6 +73,9 @@
/** [UserInfo] of the currently-selected user. */
val selectedUserInfo: Flow<UserInfo>
+ /** Whether user switching is currently in progress. */
+ val userSwitchingInProgress: Flow<Boolean>
+
/** User ID of the last non-guest selected user. */
val lastSelectedNonGuestUserId: Int
@@ -108,6 +116,8 @@
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val globalSettings: GlobalSettings,
private val tracker: UserTracker,
+ private val activityManager: IActivityManager,
+ featureFlags: FeatureFlags,
) : UserRepository {
private val _userSwitcherSettings = MutableStateFlow(runBlocking { getSettings() })
@@ -129,6 +139,10 @@
private var _isGuestUserResetting: Boolean = false
override var isGuestUserResetting: Boolean = _isGuestUserResetting
+ private val _isUserSwitchingInProgress = MutableStateFlow(false)
+ override val userSwitchingInProgress: Flow<Boolean>
+ get() = _isUserSwitchingInProgress
+
override val isGuestUserCreationScheduled = AtomicBoolean()
override val isStatusBarUserChipEnabled: Boolean =
@@ -141,6 +155,9 @@
init {
observeSelectedUser()
observeUserSettings()
+ if (featureFlags.isEnabled(FACE_AUTH_REFACTOR)) {
+ observeUserSwitching()
+ }
}
override fun refreshUsers() {
@@ -166,6 +183,28 @@
return _userSwitcherSettings.value.isSimpleUserSwitcher
}
+ private fun observeUserSwitching() {
+ conflatedCallbackFlow {
+ val callback =
+ object : UserSwitchObserver() {
+ override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback) {
+ trySendWithFailureLogging(true, TAG, "userSwitching started")
+ }
+
+ override fun onUserSwitchComplete(newUserId: Int) {
+ trySendWithFailureLogging(false, TAG, "userSwitching completed")
+ }
+ }
+ activityManager.registerUserSwitchObserver(callback, TAG)
+ trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
+ awaitClose { activityManager.unregisterUserSwitchObserver(callback) }
+ }
+ .onEach { _isUserSwitchingInProgress.value = it }
+ // TODO (b/262838215), Make this stateIn and initialize directly in field declaration
+ // once the flag is launched
+ .launchIn(applicationScope)
+ }
+
private fun observeSelectedUser() {
conflatedCallbackFlow {
fun send() {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
index 39cc34b..e8d50ca 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
@@ -21,6 +21,8 @@
import android.hardware.biometrics.BiometricFaceConstants
import android.net.Uri
import android.os.Handler
+import android.os.PowerManager
+import android.os.PowerManager.WAKE_REASON_BIOMETRIC
import android.os.UserHandle
import android.provider.Settings
import androidx.test.filters.SmallTest
@@ -48,6 +50,8 @@
private val fakeFaceErrorsUri = Uri.Builder().appendPath("face-errors").build()
private val fakeFaceAcquiredUri = Uri.Builder().appendPath("face-acquired").build()
private val fakeUnlockIntentBioEnroll = Uri.Builder().appendPath("unlock-intent-bio").build()
+ private val fakeWakeupsConsideredUnlockIntents =
+ Uri.Builder().appendPath("wakeups-considered-unlock-intent").build()
@Mock
private lateinit var secureSettings: SecureSettings
@@ -82,6 +86,9 @@
`when`(secureSettings.getUriFor(
Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED))
.thenReturn(fakeUnlockIntentBioEnroll)
+ `when`(secureSettings.getUriFor(
+ Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS))
+ .thenReturn(fakeWakeupsConsideredUnlockIntents)
activeUnlockConfig = ActiveUnlockConfig(
handler,
@@ -92,18 +99,18 @@
}
@Test
- fun testRegsitersForSettingsChanges() {
+ fun registersForSettingsChanges() {
verifyRegisterSettingObserver()
}
@Test
- fun testOnWakeupSettingChanged() {
+ fun onWakeupSettingChanged() {
verifyRegisterSettingObserver()
// GIVEN no active unlock settings enabled
assertFalse(
activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE)
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE)
)
// WHEN unlock on wake is allowed
@@ -114,26 +121,26 @@
// THEN active unlock triggers allowed on: wake, unlock-intent, and biometric failure
assertTrue(
activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE)
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE)
)
assertTrue(
activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT)
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT)
)
assertTrue(
activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL)
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL)
)
}
@Test
- fun testOnUnlockIntentSettingChanged() {
+ fun onUnlockIntentSettingChanged() {
verifyRegisterSettingObserver()
// GIVEN no active unlock settings enabled
assertFalse(
activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT)
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT)
)
// WHEN unlock on biometric failed is allowed
@@ -143,15 +150,15 @@
// THEN active unlock triggers allowed on: biometric failure ONLY
assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE))
assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
}
@Test
- fun testOnBioFailSettingChanged() {
+ fun onBioFailSettingChanged() {
verifyRegisterSettingObserver()
// GIVEN no active unlock settings enabled and triggering unlock intent on biometric
@@ -161,7 +168,7 @@
0)).thenReturn("")
updateSetting(fakeUnlockIntentBioEnroll)
assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
// WHEN unlock on biometric failed is allowed
`when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
@@ -170,15 +177,15 @@
// THEN active unlock triggers allowed on: biometric failure ONLY
assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE))
assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
}
@Test
- fun testFaceErrorSettingsChanged() {
+ fun faceErrorSettingsChanged() {
verifyRegisterSettingObserver()
// GIVEN unlock on biometric fail
@@ -200,7 +207,7 @@
}
@Test
- fun testFaceAcquiredSettingsChanged() {
+ fun faceAcquiredSettingsChanged() {
verifyRegisterSettingObserver()
// GIVEN unlock on biometric fail
@@ -228,7 +235,7 @@
}
@Test
- fun testTriggerOnUnlockIntentWhenBiometricEnrolledNone() {
+ fun triggerOnUnlockIntentWhenBiometricEnrolledNone() {
verifyRegisterSettingObserver()
// GIVEN unlock on biometric fail
@@ -244,16 +251,16 @@
// WHEN unlock intent is allowed when NO biometrics are enrolled (0)
`when`(secureSettings.getStringForUser(
Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
- 0)).thenReturn("${ActiveUnlockConfig.BIOMETRIC_TYPE_NONE}")
+ 0)).thenReturn("${ActiveUnlockConfig.BiometricType.NONE.intValue}")
updateSetting(fakeUnlockIntentBioEnroll)
// THEN active unlock triggers allowed on unlock intent
assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
}
@Test
- fun testTriggerOnUnlockIntentWhenBiometricEnrolledFingerprintOrFaceOnly() {
+ fun triggerOnUnlockIntentWhenBiometricEnrolledFingerprintOrFaceOnly() {
verifyRegisterSettingObserver()
// GIVEN unlock on biometric fail
@@ -263,7 +270,7 @@
// GIVEN fingerprint and face are both enrolled
activeUnlockConfig.keyguardUpdateMonitor = keyguardUpdateMonitor
- `when`(keyguardUpdateMonitor.isFaceEnrolled()).thenReturn(true)
+ `when`(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(true)
`when`(keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(0)).thenReturn(true)
// WHEN unlock intent is allowed when ONLY fingerprint is enrolled or NO biometircs
@@ -271,29 +278,99 @@
`when`(secureSettings.getStringForUser(
Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
0)).thenReturn(
- "${ActiveUnlockConfig.BIOMETRIC_TYPE_ANY_FACE}" +
- "|${ActiveUnlockConfig.BIOMETRIC_TYPE_ANY_FINGERPRINT}")
+ "${ActiveUnlockConfig.BiometricType.ANY_FACE.intValue}" +
+ "|${ActiveUnlockConfig.BiometricType.ANY_FINGERPRINT.intValue}")
updateSetting(fakeUnlockIntentBioEnroll)
// THEN active unlock triggers NOT allowed on unlock intent
assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
// WHEN fingerprint ONLY enrolled
- `when`(keyguardUpdateMonitor.isFaceEnrolled()).thenReturn(false)
+ `when`(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(false)
`when`(keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(0)).thenReturn(true)
// THEN active unlock triggers allowed on unlock intent
assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
// WHEN face ONLY enrolled
- `when`(keyguardUpdateMonitor.isFaceEnrolled()).thenReturn(true)
+ `when`(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(true)
`when`(keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(0)).thenReturn(false)
// THEN active unlock triggers allowed on unlock intent
assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
+ }
+
+ @Test
+ fun isWakeupConsideredUnlockIntent_singleValue() {
+ verifyRegisterSettingObserver()
+
+ // GIVEN lift is considered an unlock intent
+ `when`(secureSettings.getStringForUser(
+ Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+ 0)).thenReturn(PowerManager.WAKE_REASON_LIFT.toString())
+ updateSetting(fakeWakeupsConsideredUnlockIntents)
+
+ // THEN only WAKE_REASON_LIFT is considered an unlock intent
+ for (wakeReason in 0..WAKE_REASON_BIOMETRIC) {
+ if (wakeReason == PowerManager.WAKE_REASON_LIFT) {
+ assertTrue(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+ } else {
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+ }
+ }
+ }
+
+ @Test
+ fun isWakeupConsideredUnlockIntent_multiValue() {
+ verifyRegisterSettingObserver()
+
+ // GIVEN lift and tap are considered an unlock intent
+ `when`(secureSettings.getStringForUser(
+ Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+ 0)).thenReturn(
+ PowerManager.WAKE_REASON_LIFT.toString() +
+ "|" +
+ PowerManager.WAKE_REASON_TAP.toString()
+ )
+ updateSetting(fakeWakeupsConsideredUnlockIntents)
+
+ // THEN WAKE_REASON_LIFT and WAKE_REASON TAP are considered an unlock intent
+ for (wakeReason in 0..WAKE_REASON_BIOMETRIC) {
+ if (wakeReason == PowerManager.WAKE_REASON_LIFT ||
+ wakeReason == PowerManager.WAKE_REASON_TAP) {
+ assertTrue(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+ } else {
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+ }
+ }
+ assertTrue(activeUnlockConfig.isWakeupConsideredUnlockIntent(PowerManager.WAKE_REASON_LIFT))
+ assertTrue(activeUnlockConfig.isWakeupConsideredUnlockIntent(PowerManager.WAKE_REASON_TAP))
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(
+ PowerManager.WAKE_REASON_UNFOLD_DEVICE))
+ }
+
+ @Test
+ fun isWakeupConsideredUnlockIntent_emptyValues() {
+ verifyRegisterSettingObserver()
+
+ // GIVEN lift and tap are considered an unlock intent
+ `when`(secureSettings.getStringForUser(
+ Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+ 0)).thenReturn(" ")
+ updateSetting(fakeWakeupsConsideredUnlockIntents)
+
+ // THEN no wake up gestures are considered an unlock intent
+ for (wakeReason in 0..WAKE_REASON_BIOMETRIC) {
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+ }
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(
+ PowerManager.WAKE_REASON_LIFT))
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(PowerManager.WAKE_REASON_TAP))
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(
+ PowerManager.WAKE_REASON_UNFOLD_DEVICE))
}
private fun updateSetting(uri: Uri) {
@@ -312,6 +389,7 @@
verifyRegisterSettingObserver(fakeFaceErrorsUri)
verifyRegisterSettingObserver(fakeFaceAcquiredUri)
verifyRegisterSettingObserver(fakeUnlockIntentBioEnroll)
+ verifyRegisterSettingObserver(fakeWakeupsConsideredUnlockIntents)
}
private fun verifyRegisterSettingObserver(uri: Uri) {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index b9c23d4..43a2017 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -32,10 +32,12 @@
import com.android.systemui.plugins.ClockEvents
import com.android.systemui.plugins.ClockFaceController
import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.ClockTickRate
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
@@ -72,7 +74,7 @@
@Mock private lateinit var animations: ClockAnimations
@Mock private lateinit var events: ClockEvents
@Mock private lateinit var clock: ClockController
- @Mock private lateinit var mainExecutor: Executor
+ @Mock private lateinit var mainExecutor: DelayableExecutor
@Mock private lateinit var bgExecutor: Executor
@Mock private lateinit var featureFlags: FeatureFlags
@Mock private lateinit var smallClockController: ClockFaceController
@@ -97,6 +99,8 @@
whenever(largeClockController.events).thenReturn(largeClockEvents)
whenever(clock.events).thenReturn(events)
whenever(clock.animations).thenReturn(animations)
+ whenever(smallClockEvents.tickRate).thenReturn(ClockTickRate.PER_MINUTE)
+ whenever(largeClockEvents.tickRate).thenReturn(ClockTickRate.PER_MINUTE)
repository = FakeKeyguardRepository()
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index a4180fd..36b3f89 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -33,6 +33,7 @@
import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
@@ -47,6 +48,7 @@
import com.android.systemui.plugins.ClockController;
import com.android.systemui.plugins.ClockEvents;
import com.android.systemui.plugins.ClockFaceController;
+import com.android.systemui.plugins.ClockFaceEvents;
import com.android.systemui.plugins.log.LogBuffer;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shared.clocks.AnimatableClockView;
@@ -99,6 +101,8 @@
@Mock
private ClockEvents mClockEvents;
@Mock
+ private ClockFaceEvents mClockFaceEvents;
+ @Mock
DumpManager mDumpManager;
@Mock
ClockEventController mClockEventController;
@@ -118,6 +122,11 @@
@Mock
private LogBuffer mLogBuffer;
+ private final View mFakeDateView = (View) (new ViewGroup(mContext) {
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {}
+ });
+ private final View mFakeWeatherView = new View(mContext);
private final View mFakeSmartspaceView = new View(mContext);
private KeyguardClockSwitchController mController;
@@ -145,6 +154,8 @@
when(mLargeClockView.getContext()).thenReturn(getContext());
when(mView.isAttachedToWindow()).thenReturn(true);
+ when(mSmartspaceController.buildAndConnectDateView(any())).thenReturn(mFakeDateView);
+ when(mSmartspaceController.buildAndConnectWeatherView(any())).thenReturn(mFakeWeatherView);
when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
mExecutor = new FakeExecutor(new FakeSystemClock());
mController = new KeyguardClockSwitchController(
@@ -168,6 +179,8 @@
when(mClockController.getLargeClock()).thenReturn(mLargeClockController);
when(mClockController.getSmallClock()).thenReturn(mSmallClockController);
when(mClockController.getEvents()).thenReturn(mClockEvents);
+ when(mSmallClockController.getEvents()).thenReturn(mClockFaceEvents);
+ when(mLargeClockController.getEvents()).thenReturn(mClockFaceEvents);
when(mClockController.getAnimations()).thenReturn(mClockAnimations);
when(mClockRegistry.createCurrentClock()).thenReturn(mClockController);
when(mClockEventController.getClock()).thenReturn(mClockController);
@@ -252,6 +265,19 @@
}
@Test
+ public void onLocaleListChanged_rebuildsSmartspaceViews_whenDecouplingEnabled() {
+ when(mSmartspaceController.isEnabled()).thenReturn(true);
+ when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true);
+ mController.init();
+
+ mController.onLocaleListChanged();
+ // Should be called once on initial setup, then once again for locale change
+ verify(mSmartspaceController, times(2)).buildAndConnectDateView(mView);
+ verify(mSmartspaceController, times(2)).buildAndConnectWeatherView(mView);
+ verify(mSmartspaceController, times(2)).buildAndConnectView(mView);
+ }
+
+ @Test
public void testSmartspaceDisabledShowsKeyguardStatusArea() {
when(mSmartspaceController.isEnabled()).thenReturn(false);
mController.init();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index d1650b7..cd8857a 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -685,12 +685,36 @@
// WHEN fingerprint is locked out
fingerprintErrorLockedOut();
- // THEN unlocking with fingeprint is not allowed
+ // THEN unlocking with fingerprint is not allowed
Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
BiometricSourceType.FINGERPRINT));
}
@Test
+ public void trustAgentHasTrust() {
+ // WHEN user has trust
+ mKeyguardUpdateMonitor.onTrustChanged(true, true, getCurrentUser(), 0, null);
+
+ // THEN user is considered as "having trust" and bouncer can be skipped
+ Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser()));
+ Assert.assertTrue(mKeyguardUpdateMonitor.getUserCanSkipBouncer(getCurrentUser()));
+ }
+
+ @Test
+ public void trustAgentHasTrust_fingerprintLockout() {
+ // GIVEN user has trust
+ mKeyguardUpdateMonitor.onTrustChanged(true, true, getCurrentUser(), 0, null);
+ Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser()));
+
+ // WHEN fingerprint is locked out
+ fingerprintErrorLockedOut();
+
+ // THEN user is NOT considered as "having trust" and bouncer cannot be skipped
+ Assert.assertFalse(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser()));
+ Assert.assertFalse(mKeyguardUpdateMonitor.getUserCanSkipBouncer(getCurrentUser()));
+ }
+
+ @Test
public void testTriesToAuthenticate_whenBouncer() {
setKeyguardBouncerVisibility(true);
@@ -2183,7 +2207,7 @@
// GIVEN active unlock triggers on biometric failures
when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
.thenReturn(true);
// WHEN fingerprint fails
@@ -2206,7 +2230,7 @@
// GIVEN active unlock triggers on biometric failures
when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
.thenReturn(true);
// WHEN face fails & bypass is not allowed
@@ -2230,7 +2254,7 @@
// GIVEN active unlock triggers on biometric failures
when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
.thenReturn(true);
// WHEN face fails & bypass is not allowed
@@ -2252,7 +2276,7 @@
// GIVEN active unlock triggers on biometric failures
when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
.thenReturn(true);
// WHEN face fails & on the bouncer
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
index 32edf8f..babbe45 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
@@ -11,6 +11,7 @@
import com.android.systemui.flags.Flag
import com.android.systemui.flags.FlagListenable
import com.android.systemui.flags.Flags
+import com.android.systemui.flags.ReleasedFlag
import com.android.systemui.flags.UnreleasedFlag
import com.android.systemui.settings.UserTracker
import com.android.systemui.util.mockito.any
@@ -102,7 +103,7 @@
@Test
fun initialize_enablesUnbundledChooser_whenFlagEnabled() {
// Arrange
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+ setFlagMock(true)
// Act
chooserSelector.start()
@@ -118,7 +119,7 @@
@Test
fun initialize_disablesUnbundledChooser_whenFlagDisabled() {
// Arrange
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+ setFlagMock(false)
// Act
chooserSelector.start()
@@ -134,7 +135,7 @@
@Test
fun enablesUnbundledChooser_whenFlagBecomesEnabled() {
// Arrange
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+ setFlagMock(false)
chooserSelector.start()
verify(mockFeatureFlags).addListener(
eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
@@ -147,7 +148,7 @@
)
// Act
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+ setFlagMock(true)
flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.name))
// Assert
@@ -161,7 +162,7 @@
@Test
fun disablesUnbundledChooser_whenFlagBecomesDisabled() {
// Arrange
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+ setFlagMock(true)
chooserSelector.start()
verify(mockFeatureFlags).addListener(
eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
@@ -174,7 +175,7 @@
)
// Act
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+ setFlagMock(false)
flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.name))
// Assert
@@ -188,7 +189,7 @@
@Test
fun doesNothing_whenAnotherFlagChanges() {
// Arrange
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+ setFlagMock(false)
chooserSelector.start()
verify(mockFeatureFlags).addListener(
eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
@@ -197,13 +198,17 @@
clearInvocations(mockPackageManager)
// Act
- whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
flagListener.value.onFlagChanged(TestFlagEvent("other flag"))
// Assert
verifyZeroInteractions(mockPackageManager)
}
+ private fun setFlagMock(enabled: Boolean) {
+ whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(enabled)
+ whenever(mockFeatureFlags.isEnabled(any<ReleasedFlag>())).thenReturn(enabled)
+ }
+
private class TestFlagEvent(override val flagName: String) : FlagListenable.FlagEvent {
override fun requestNoRestart() {}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index e918c1c..4cf5a4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -21,6 +21,7 @@
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.systemui.dump.LogBufferHelperKt.logcatLogBuffer;
import static com.google.common.truth.Truth.assertThat;
@@ -33,8 +34,10 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
@@ -88,6 +91,7 @@
import com.android.systemui.decor.PrivacyDotCornerDecorProviderImpl;
import com.android.systemui.decor.PrivacyDotDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerResDelegate;
+import com.android.systemui.log.ScreenDecorationsLogger;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.settings.UserTracker;
@@ -102,8 +106,12 @@
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.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.List;
@@ -117,7 +125,7 @@
private WindowManager mWindowManager;
private DisplayManager mDisplayManager;
private SecureSettings mSecureSettings;
- private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+ private FakeExecutor mExecutor;
private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
private FakeThreadFactory mThreadFactory;
private ArrayList<DecorProvider> mPrivacyDecorProviders;
@@ -159,6 +167,8 @@
private PrivacyDotViewController.ShowingListener mPrivacyDotShowingListener;
@Mock
private CutoutDecorProviderFactory mCutoutFactory;
+ @Captor
+ private ArgumentCaptor<AuthController.Callback> mAuthControllerCallback;
private List<DecorProvider> mMockCutoutList;
@Before
@@ -167,6 +177,7 @@
Handler mainHandler = new Handler(TestableLooper.get(this).getLooper());
mSecureSettings = new FakeSettings();
+ mExecutor = new FakeExecutor(new FakeSystemClock());
mThreadFactory = new FakeThreadFactory(mExecutor);
mThreadFactory.setHandler(mainHandler);
@@ -219,11 +230,14 @@
mAuthController,
mStatusBarStateController,
mKeyguardUpdateMonitor,
- mExecutor));
+ mExecutor,
+ new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer"))));
mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings,
mTunerService, mUserTracker, mDisplayTracker, mDotViewController, mThreadFactory,
- mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory) {
+ mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
+ new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
+ mAuthController) {
@Override
public void start() {
super.start();
@@ -1161,6 +1175,44 @@
}
@Test
+ public void faceSensorLocationChangesReloadsFaceScanningOverlay() {
+ mFaceScanningProviders = new ArrayList<>();
+ mFaceScanningProviders.add(mFaceScanningDecorProvider);
+ when(mFaceScanningProviderFactory.getProviders()).thenReturn(mFaceScanningProviders);
+ when(mFaceScanningProviderFactory.getHasProviders()).thenReturn(true);
+ ScreenDecorations screenDecorations = new ScreenDecorations(mContext, mExecutor,
+ mSecureSettings, mTunerService, mUserTracker, mDisplayTracker, mDotViewController,
+ mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
+ new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mAuthController);
+ screenDecorations.start();
+ verify(mAuthController).addCallback(mAuthControllerCallback.capture());
+ when(mContext.getDisplay()).thenReturn(mDisplay);
+ when(mDisplay.getDisplayInfo(any())).thenAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ DisplayInfo displayInfo = invocation.getArgument(0);
+ int modeId = 1;
+ displayInfo.modeId = modeId;
+ displayInfo.supportedModes = new Display.Mode[]{new Display.Mode(modeId, 1024, 1024,
+ 90)};
+ return false;
+ }
+ });
+ mExecutor.runAllReady();
+ clearInvocations(mFaceScanningDecorProvider);
+
+ AuthController.Callback callback = mAuthControllerCallback.getValue();
+ callback.onFaceSensorLocationChanged();
+ mExecutor.runAllReady();
+
+ verify(mFaceScanningDecorProvider).onReloadResAndMeasure(any(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ any());
+ }
+
+ @Test
public void testPrivacyDotShowingListenerWorkWellWithNullParameter() {
mPrivacyDotShowingListener.onPrivacyDotShown(null);
mPrivacyDotShowingListener.onPrivacyDotHidden(null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index fd931b0..41beada 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -52,18 +52,16 @@
import androidx.test.filters.SmallTest
import com.airbnb.lottie.LottieAnimationView
import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.keyguard.ViewMediatorCallback
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestableContext
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags.MODERN_ALTERNATE_BOUNCER
-import com.android.systemui.keyguard.data.repository.FakeBiometricRepository
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
-import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.recents.OverviewProxyService
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
@@ -111,7 +109,7 @@
@Captor lateinit var overlayCaptor: ArgumentCaptor<View>
@Captor lateinit var overlayViewParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
- private lateinit var keyguardBouncerRepository: KeyguardBouncerRepository
+ private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository
private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
private val featureFlags = FakeFeatureFlags()
private val executor = FakeExecutor(FakeSystemClock())
@@ -135,17 +133,11 @@
@Before
fun setup() {
featureFlags.set(MODERN_ALTERNATE_BOUNCER, true)
- keyguardBouncerRepository =
- KeyguardBouncerRepository(
- mock(ViewMediatorCallback::class.java),
- FakeSystemClock(),
- TestCoroutineScope(),
- mock(TableLogBuffer::class.java),
- )
+ keyguardBouncerRepository = FakeKeyguardBouncerRepository()
alternateBouncerInteractor =
AlternateBouncerInteractor(
keyguardBouncerRepository,
- FakeBiometricRepository(),
+ FakeBiometricSettingsRepository(),
FakeDeviceEntryFingerprintAuthRepository(),
FakeSystemClock(),
mock(KeyguardUpdateMonitor::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
index 81a6bc2..c73ff1d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
@@ -26,9 +26,10 @@
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.BouncerView
-import com.android.systemui.keyguard.data.repository.BiometricRepository
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepositoryImpl
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
@@ -65,7 +66,7 @@
allowTestableLooperAsMainThread() // repeatWhenAttached requires the main thread
MockitoAnnotations.initMocks(this)
keyguardBouncerRepository =
- KeyguardBouncerRepository(
+ KeyguardBouncerRepositoryImpl(
mock(com.android.keyguard.ViewMediatorCallback::class.java),
FakeSystemClock(),
TestCoroutineScope(),
@@ -91,7 +92,7 @@
mAlternateBouncerInteractor =
AlternateBouncerInteractor(
keyguardBouncerRepository,
- mock(BiometricRepository::class.java),
+ mock(BiometricSettingsRepository::class.java),
mock(DeviceEntryFingerprintAuthRepository::class.java),
mock(SystemClock::class.java),
mock(KeyguardUpdateMonitor::class.java),
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 71c335e..7177919 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
@@ -16,6 +16,8 @@
package com.android.systemui.clipboardoverlay;
+import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
+
import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
import static org.junit.Assert.assertEquals;
@@ -38,6 +40,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlags;
import org.junit.Before;
import org.junit.Test;
@@ -60,6 +63,7 @@
private ClipboardOverlayController mOverlayController;
@Mock
private ClipboardToast mClipboardToast;
+ private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock
private UiEventLogger mUiEventLogger;
@@ -93,8 +97,10 @@
when(mClipboardManager.getPrimaryClip()).thenReturn(mSampleClipData);
when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource);
+ mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, true);
+
mClipboardListener = new ClipboardListener(getContext(), mOverlayControllerProvider,
- mClipboardToast, mClipboardManager, mUiEventLogger);
+ mClipboardToast, mClipboardManager, mFeatureFlags, mUiEventLogger);
}
@@ -187,4 +193,34 @@
verify(mClipboardToast, times(1)).showCopiedToast();
verifyZeroInteractions(mOverlayControllerProvider);
}
+
+ @Test
+ public void test_minimizedLayoutFlagOff_usesLegacy() {
+ mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+
+ mClipboardListener.start();
+ mClipboardListener.onPrimaryClipChanged();
+
+ verify(mOverlayControllerProvider).get();
+
+ verify(mOverlayController).setClipDataLegacy(
+ mClipDataCaptor.capture(), mStringCaptor.capture());
+
+ assertEquals(mSampleClipData, mClipDataCaptor.getValue());
+ assertEquals(mSampleSource, mStringCaptor.getValue());
+ }
+
+ @Test
+ public void test_minimizedLayoutFlagOn_usesNew() {
+ mClipboardListener.start();
+ mClipboardListener.onPrimaryClipChanged();
+
+ verify(mOverlayControllerProvider).get();
+
+ verify(mOverlayController).setClipData(
+ mClipDataCaptor.capture(), mStringCaptor.capture());
+
+ assertEquals(mSampleClipData, mClipDataCaptor.getValue());
+ assertEquals(mSampleSource, mStringCaptor.getValue());
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
new file mode 100644
index 0000000..faef35e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.content.ClipData
+import android.content.ClipDescription
+import android.content.ContentResolver
+import android.content.Context
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.PersistableBundle
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.whenever
+import java.io.IOException
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ClipboardModelTest : SysuiTestCase() {
+ @Mock private lateinit var mClipboardUtils: ClipboardOverlayUtils
+ @Mock private lateinit var mMockContext: Context
+ @Mock private lateinit var mMockContentResolver: ContentResolver
+ private lateinit var mSampleClipData: ClipData
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ mSampleClipData = ClipData("Test", arrayOf("text/plain"), ClipData.Item("Test Item"))
+ }
+
+ @Test
+ fun test_nullClipData() {
+ val model = ClipboardModel.fromClipData(mContext, mClipboardUtils, null, "test source")
+ assertNull(model.clipData)
+ assertEquals("test source", model.source)
+ assertEquals(ClipboardModel.Type.OTHER, model.type)
+ assertNull(model.item)
+ assertFalse(model.isSensitive)
+ assertFalse(model.isRemote)
+ assertNull(model.loadThumbnail(mContext))
+ }
+
+ @Test
+ fun test_textClipData() {
+ val source = "test source"
+ val model = ClipboardModel.fromClipData(mContext, mClipboardUtils, mSampleClipData, source)
+ assertEquals(mSampleClipData, model.clipData)
+ assertEquals(source, model.source)
+ assertEquals(ClipboardModel.Type.TEXT, model.type)
+ assertEquals(mSampleClipData.getItemAt(0), model.item)
+ assertFalse(model.isSensitive)
+ assertFalse(model.isRemote)
+ assertNull(model.loadThumbnail(mContext))
+ }
+
+ @Test
+ fun test_sensitiveExtra() {
+ val description = mSampleClipData.description
+ val b = PersistableBundle()
+ b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
+ description.extras = b
+ val data = ClipData(description, mSampleClipData.getItemAt(0))
+ val (_, _, _, _, sensitive) =
+ ClipboardModel.fromClipData(mContext, mClipboardUtils, data, "")
+ assertTrue(sensitive)
+ }
+
+ @Test
+ fun test_remoteExtra() {
+ whenever(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true)
+ val model = ClipboardModel.fromClipData(mContext, mClipboardUtils, mSampleClipData, "")
+ assertTrue(model.isRemote)
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun test_imageClipData() {
+ val testBitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888)
+ whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver)
+ whenever(mMockContext.resources).thenReturn(mContext.resources)
+ whenever(mMockContentResolver.loadThumbnail(any(), any(), any())).thenReturn(testBitmap)
+ whenever(mMockContentResolver.getType(any())).thenReturn("image")
+ val imageClipData =
+ ClipData("Test", arrayOf("text/plain"), ClipData.Item(Uri.parse("test")))
+ val model = ClipboardModel.fromClipData(mMockContext, mClipboardUtils, imageClipData, "")
+ assertEquals(ClipboardModel.Type.IMAGE, model.type)
+ assertEquals(testBitmap, model.loadThumbnail(mMockContext))
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun test_imageClipData_loadFailure() {
+ whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver)
+ whenever(mMockContext.resources).thenReturn(mContext.resources)
+ whenever(mMockContentResolver.loadThumbnail(any(), any(), any())).thenThrow(IOException())
+ whenever(mMockContentResolver.getType(any())).thenReturn("image")
+ val imageClipData =
+ ClipData("Test", arrayOf("text/plain"), ClipData.Item(Uri.parse("test")))
+ val model = ClipboardModel.fromClipData(mMockContext, mClipboardUtils, imageClipData, "")
+ assertEquals(ClipboardModel.Type.IMAGE, model.type)
+ assertNull(model.loadThumbnail(mMockContext))
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index ca5b7af..0ac2667 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -16,13 +16,17 @@
package com.android.systemui.clipboardoverlay;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN;
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 com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
import static com.android.systemui.flags.Flags.CLIPBOARD_REMOTE_BEHAVIOR;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -35,8 +39,11 @@
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Context;
+import android.graphics.Insets;
+import android.graphics.Rect;
import android.net.Uri;
import android.os.PersistableBundle;
+import android.view.WindowInsets;
import android.view.textclassifier.TextLinks;
import androidx.test.filters.SmallTest;
@@ -102,11 +109,14 @@
when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator);
when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator);
+ when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
+ getImeInsets(new Rect(0, 0, 0, 0)));
mSampleClipData = new ClipData("Test", new String[]{"text/plain"},
new ClipData.Item("Test Item"));
mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, false);
+ mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, true); // turned off for legacy tests
mOverlayController = new ClipboardOverlayController(
mContext,
@@ -118,8 +128,7 @@
mFeatureFlags,
mClipboardUtils,
mExecutor,
- mUiEventLogger,
- mDisplayTracker);
+ mUiEventLogger);
verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture());
mCallbacks = mOverlayCallbacksCaptor.getValue();
}
@@ -130,6 +139,159 @@
}
@Test
+ public void test_setClipData_nullData_legacy() {
+ ClipData clipData = null;
+ mOverlayController.setClipDataLegacy(clipData, "");
+
+ verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
+ verify(mClipboardOverlayView, times(0)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_invalidImageData_legacy() {
+ ClipData clipData = new ClipData("", new String[]{"image/png"},
+ new ClipData.Item(Uri.parse("")));
+
+ mOverlayController.setClipDataLegacy(clipData, "");
+
+ verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
+ verify(mClipboardOverlayView, times(0)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_textData_legacy() {
+ mOverlayController.setClipDataLegacy(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_legacy() {
+ 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.setClipDataLegacy(data, "");
+
+ verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true);
+ verify(mClipboardOverlayView, times(1)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_repeatedCalls_legacy() {
+ when(mAnimator.isRunning()).thenReturn(true);
+
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_viewCallbacks_onShareTapped_legacy() {
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ mCallbacks.onShareButtonTapped();
+
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "");
+ verify(mClipboardOverlayView, times(1)).getExitAnimation();
+ }
+
+ @Test
+ public void test_viewCallbacks_onDismissTapped_legacy() {
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ mCallbacks.onDismissButtonTapped();
+
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "");
+ verify(mClipboardOverlayView, times(1)).getExitAnimation();
+ }
+
+ @Test
+ public void test_multipleDismissals_dismissesOnce_legacy() {
+ mCallbacks.onSwipeDismissInitiated(mAnimator);
+ mCallbacks.onDismissButtonTapped();
+ mCallbacks.onSwipeDismissInitiated(mAnimator);
+ mCallbacks.onDismissButtonTapped();
+
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SWIPE_DISMISSED, 0, null);
+ verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
+ }
+
+ @Test
+ public void test_remoteCopy_withFlagOn_legacy() {
+ mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
+ when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true);
+
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ verify(mTimeoutHandler, never()).resetTimeout();
+ }
+
+ @Test
+ public void test_remoteCopy_withFlagOff_legacy() {
+ when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true);
+
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ verify(mTimeoutHandler).resetTimeout();
+ }
+
+ @Test
+ public void test_nonRemoteCopy_legacy() {
+ mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
+ when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(false);
+
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ verify(mTimeoutHandler).resetTimeout();
+ }
+
+ @Test
+ public void test_logsUseLastClipSource_legacy() {
+ mOverlayController.setClipDataLegacy(mSampleClipData, "first.package");
+ mCallbacks.onDismissButtonTapped();
+ mOverlayController.setClipDataLegacy(mSampleClipData, "second.package");
+ mCallbacks.onDismissButtonTapped();
+
+ verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "first.package");
+ verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "second.package");
+ verifyNoMoreInteractions(mUiEventLogger);
+ }
+
+ @Test
+ public void test_logOnClipboardActionsShown_legacy() {
+ ClipData.Item item = mSampleClipData.getItemAt(0);
+ item.setTextLinks(Mockito.mock(TextLinks.class));
+ mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
+ when(mClipboardUtils.isRemoteCopy(any(Context.class), any(ClipData.class), anyString()))
+ .thenReturn(true);
+ when(mClipboardUtils.getAction(any(ClipData.Item.class), anyString()))
+ .thenReturn(Optional.of(Mockito.mock(RemoteAction.class)));
+ when(mClipboardOverlayView.post(any(Runnable.class))).thenAnswer(new Answer<Object>() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ ((Runnable) invocation.getArgument(0)).run();
+ return null;
+ }
+ });
+
+ mOverlayController.setClipDataLegacy(
+ new ClipData(mSampleClipData.getDescription(), item), "actionShownSource");
+ mExecutor.runAllReady();
+
+ verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_ACTION_SHOWN, 0, "actionShownSource");
+ verifyNoMoreInteractions(mUiEventLogger);
+ }
+
+ // start of refactored setClipData tests
+ @Test
public void test_setClipData_nullData() {
ClipData clipData = null;
mOverlayController.setClipData(clipData, "");
@@ -280,4 +442,43 @@
verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_ACTION_SHOWN, 0, "actionShownSource");
verifyNoMoreInteractions(mUiEventLogger);
}
+
+ @Test
+ public void test_noInsets_showsExpanded() {
+ mOverlayController.setClipData(mSampleClipData, "");
+
+ verify(mClipboardOverlayView, never()).setMinimized(true);
+ verify(mClipboardOverlayView).setMinimized(false);
+ verify(mClipboardOverlayView).showTextPreview("Test Item", false);
+ }
+
+ @Test
+ public void test_insets_showsMinimized() {
+ when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
+ getImeInsets(new Rect(0, 0, 0, 1)));
+ mOverlayController.setClipData(mSampleClipData, "");
+
+ verify(mClipboardOverlayView).setMinimized(true);
+ verify(mClipboardOverlayView, never()).setMinimized(false);
+ verify(mClipboardOverlayView, never()).showTextPreview(any(), anyBoolean());
+
+ mCallbacks.onMinimizedViewTapped();
+
+ verify(mClipboardOverlayView).setMinimized(false);
+ verify(mClipboardOverlayView).showTextPreview("Test Item", false);
+ }
+
+ @Test
+ public void test_insetsChanged_minimizes() {
+ mOverlayController.setClipData(mSampleClipData, "");
+ verify(mClipboardOverlayView, never()).setMinimized(true);
+
+ WindowInsets insetsWithKeyboard = getImeInsets(new Rect(0, 0, 0, 1));
+ mOverlayController.onInsetsChanged(insetsWithKeyboard, ORIENTATION_PORTRAIT);
+ verify(mClipboardOverlayView).setMinimized(true);
+ }
+
+ private static WindowInsets getImeInsets(Rect r) {
+ return new WindowInsets.Builder().setInsets(WindowInsets.Type.ime(), Insets.of(r)).build();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index d54babf..e35b2a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -104,8 +104,6 @@
ArgumentCaptor<ControlsBindingController.LoadCallback>
@Captor
- private lateinit var userTrackerCallbackCaptor: ArgumentCaptor<UserTracker.Callback>
- @Captor
private lateinit var listingCallbackCaptor:
ArgumentCaptor<ControlsListingController.ControlsListingCallback>
@@ -178,10 +176,6 @@
)
controller.auxiliaryPersistenceWrapper = auxiliaryPersistenceWrapper
- verify(userTracker).addCallback(
- capture(userTrackerCallbackCaptor), any()
- )
-
verify(listingController).addCallback(capture(listingCallbackCaptor))
}
@@ -539,7 +533,7 @@
reset(persistenceWrapper)
- userTrackerCallbackCaptor.value.onUserChanged(otherUser, mContext)
+ controller.changeUser(UserHandle.of(otherUser))
verify(persistenceWrapper).changeFileAndBackupManager(any(), any())
verify(persistenceWrapper).readFavorites()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt
new file mode 100644
index 0000000..7ecaca6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.controls.start
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.ui.SelectedItem
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.FakeSystemClock
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ControlsStartableTest : SysuiTestCase() {
+
+ @Mock private lateinit var controlsController: ControlsController
+ @Mock private lateinit var controlsListingController: ControlsListingController
+ @Mock private lateinit var userTracker: UserTracker
+
+ private lateinit var fakeExecutor: FakeExecutor
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf<String>()
+ )
+
+ fakeExecutor = FakeExecutor(FakeSystemClock())
+ }
+
+ @Test
+ fun testDisabledNothingIsCalled() {
+ createStartable(enabled = false).start()
+
+ verifyZeroInteractions(controlsController, controlsListingController, userTracker)
+ }
+
+ @Test
+ fun testNoPreferredPackagesNoDefaultSelected_noNewSelection() {
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController, never()).setPreferredSelection(any())
+ }
+
+ @Test
+ fun testPreferredPackagesNotInstalled_noNewSelection() {
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE_PANEL)
+ )
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+ `when`(controlsListingController.getCurrentServices()).thenReturn(emptyList())
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController, never()).setPreferredSelection(any())
+ }
+
+ @Test
+ fun testPreferredPackageNotPanel_noNewSelection() {
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE_PANEL)
+ )
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT, "not panel", hasPanel = false))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController, never()).setPreferredSelection(any())
+ }
+
+ @Test
+ fun testExistingSelection_noNewSelection() {
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE_PANEL)
+ )
+ `when`(controlsController.getPreferredSelection())
+ .thenReturn(mock<SelectedItem.PanelItem>())
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController, never()).setPreferredSelection(any())
+ }
+
+ @Test
+ fun testPanelAdded() {
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE_PANEL)
+ )
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController).setPreferredSelection(listings[0].toPanelItem())
+ }
+
+ @Test
+ fun testMultiplePreferredOnlyOnePanel_panelAdded() {
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf("other_package", TEST_PACKAGE_PANEL)
+ )
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+ val listings =
+ listOf(
+ ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true),
+ ControlsServiceInfo(ComponentName("other_package", "cls"), "non panel", false)
+ )
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController).setPreferredSelection(listings[0].toPanelItem())
+ }
+
+ @Test
+ fun testMultiplePreferredMultiplePanels_firstPreferredAdded() {
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE_PANEL, "other_package")
+ )
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+ val listings =
+ listOf(
+ ControlsServiceInfo(ComponentName("other_package", "cls"), "panel", true),
+ ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)
+ )
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController).setPreferredSelection(listings[1].toPanelItem())
+ }
+
+ @Test
+ fun testPreferredSelectionIsPanel_bindOnStart() {
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+ `when`(controlsController.getPreferredSelection()).thenReturn(listings[0].toPanelItem())
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController).bindComponentForPanel(TEST_COMPONENT_PANEL)
+ }
+
+ @Test
+ fun testPreferredSelectionPanel_listingNoPanel_notBind() {
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = false))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+ `when`(controlsController.getPreferredSelection())
+ .thenReturn(SelectedItem.PanelItem("panel", TEST_COMPONENT_PANEL))
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController, never()).bindComponentForPanel(any())
+ }
+
+ @Test
+ fun testNotPanelSelection_noBind() {
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = false))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController, never()).bindComponentForPanel(any())
+ }
+
+ private fun createStartable(enabled: Boolean): ControlsStartable {
+ val component: ControlsComponent =
+ mock() {
+ `when`(isEnabled()).thenReturn(enabled)
+ if (enabled) {
+ `when`(getControlsController()).thenReturn(Optional.of(controlsController))
+ `when`(getControlsListingController())
+ .thenReturn(Optional.of(controlsListingController))
+ } else {
+ `when`(getControlsController()).thenReturn(Optional.empty())
+ `when`(getControlsListingController()).thenReturn(Optional.empty())
+ }
+ }
+ return ControlsStartable(context.resources, fakeExecutor, component, userTracker)
+ }
+
+ private fun ControlsServiceInfo(
+ componentName: ComponentName,
+ label: CharSequence,
+ hasPanel: Boolean
+ ): ControlsServiceInfo {
+ val serviceInfo =
+ ServiceInfo().apply {
+ applicationInfo = ApplicationInfo()
+ packageName = componentName.packageName
+ name = componentName.className
+ }
+ return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel)
+ }
+
+ private class FakeControlsServiceInfo(
+ context: Context,
+ serviceInfo: ServiceInfo,
+ private val label: CharSequence,
+ hasPanel: Boolean
+ ) : ControlsServiceInfo(context, serviceInfo) {
+
+ init {
+ if (hasPanel) {
+ panelActivity = serviceInfo.componentName
+ }
+ }
+
+ override fun loadLabel(): CharSequence {
+ return label
+ }
+ }
+
+ companion object {
+ private fun ControlsServiceInfo.toPanelItem(): SelectedItem.PanelItem {
+ if (panelActivity == null) {
+ throw IllegalArgumentException("$this is not a panel")
+ }
+ return SelectedItem.PanelItem(loadLabel(), componentName)
+ }
+
+ private const val TEST_PACKAGE = "pkg"
+ private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service")
+ private const val TEST_PACKAGE_PANEL = "pkg.panel"
+ private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt
index a3740d8..925c06f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt
@@ -19,7 +19,6 @@
import android.content.Context
import android.media.AudioManager
-import androidx.lifecycle.LiveData
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.settings.UserFileManager
@@ -27,10 +26,12 @@
import com.android.systemui.util.RingerModeTracker
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 kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -57,13 +58,15 @@
@Mock
private lateinit var userFileManager: UserFileManager
+ private lateinit var testDispatcher: TestDispatcher
private lateinit var testScope: TestScope
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- testScope = TestScope()
+ testDispatcher = StandardTestDispatcher()
+ testScope = TestScope(testDispatcher)
whenever(userTracker.userContext).thenReturn(context)
whenever(userFileManager.getSharedPreferences(any(), any(), any()))
@@ -74,7 +77,10 @@
userTracker,
userFileManager,
ringerModeTracker,
- audioManager
+ audioManager,
+ testScope.backgroundScope,
+ testDispatcher,
+ testDispatcher,
)
}
@@ -103,17 +109,16 @@
}
@Test
- fun `triggered - state was previously NORMAL - currently SILENT - move to previous state`() {
+ fun `triggered - state was previously NORMAL - currently SILENT - move to previous state`() = testScope.runTest {
//given
val ringerModeCapture = argumentCaptor<Int>()
- val ringerModeInternal = mock<LiveData<Int>>()
- whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
- whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+ whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL)
underTest.onTriggered(null)
- whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_SILENT)
+ whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT)
//when
val result = underTest.onTriggered(null)
+ runCurrent()
verify(audioManager, times(2)).ringerModeInternal = ringerModeCapture.capture()
//then
@@ -122,15 +127,14 @@
}
@Test
- fun `triggered - state is not SILENT - move to SILENT ringer`() {
+ fun `triggered - state is not SILENT - move to SILENT ringer`() = testScope.runTest {
//given
val ringerModeCapture = argumentCaptor<Int>()
- val ringerModeInternal = mock<LiveData<Int>>()
- whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
- whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+ whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL)
//when
val result = underTest.onTriggered(null)
+ runCurrent()
verify(audioManager).ringerModeInternal = ringerModeCapture.capture()
//then
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
index 26601b6..34f3ed8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
@@ -37,6 +37,8 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -67,6 +69,7 @@
@Mock
private lateinit var keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository
+ private lateinit var testDispatcher: TestDispatcher
private lateinit var testScope: TestScope
private lateinit var underTest: MuteQuickAffordanceCoreStartable
@@ -83,7 +86,8 @@
val emission = MutableStateFlow(mapOf("testQuickAffordanceKey" to listOf(config)))
whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
- testScope = TestScope()
+ testDispatcher = StandardTestDispatcher()
+ testScope = TestScope(testDispatcher)
underTest = MuteQuickAffordanceCoreStartable(
featureFlags,
@@ -91,7 +95,8 @@
ringerModeTracker,
userFileManager,
keyguardQuickAffordanceRepository,
- testScope,
+ testScope.backgroundScope,
+ testDispatcher,
)
}
@@ -158,6 +163,7 @@
runCurrent()
verify(ringerModeInternal).observeForever(observerCaptor.capture())
observerCaptor.value.onChanged(newRingerMode)
+ runCurrent()
val result = sharedPrefs.getInt("key_last_non_silent_ringer_mode", -1)
//then
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
similarity index 94%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
index a92dd3b..ddd1049 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
@@ -50,8 +50,8 @@
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidTestingRunner::class)
-class BiometricRepositoryTest : SysuiTestCase() {
- private lateinit var underTest: BiometricRepository
+class BiometricSettingsRepositoryTest : SysuiTestCase() {
+ private lateinit var underTest: BiometricSettingsRepository
@Mock private lateinit var authController: AuthController
@Mock private lateinit var lockPatternUtils: LockPatternUtils
@@ -71,11 +71,11 @@
userRepository = FakeUserRepository()
}
- private suspend fun createBiometricRepository() {
+ private suspend fun createBiometricSettingsRepository() {
userRepository.setUserInfos(listOf(PRIMARY_USER))
userRepository.setSelectedUserInfo(PRIMARY_USER)
underTest =
- BiometricRepositoryImpl(
+ BiometricSettingsRepositoryImpl(
context = context,
lockPatternUtils = lockPatternUtils,
broadcastDispatcher = fakeBroadcastDispatcher,
@@ -91,7 +91,7 @@
@Test
fun fingerprintEnrollmentChange() =
testScope.runTest {
- createBiometricRepository()
+ createBiometricSettingsRepository()
val fingerprintEnabledByDevicePolicy = collectLastValue(underTest.isFingerprintEnrolled)
runCurrent()
@@ -117,7 +117,7 @@
@Test
fun strongBiometricAllowedChange() =
testScope.runTest {
- createBiometricRepository()
+ createBiometricSettingsRepository()
val strongBiometricAllowed = collectLastValue(underTest.isStrongBiometricAllowed)
runCurrent()
@@ -140,7 +140,7 @@
@Test
fun fingerprintDisabledByDpmChange() =
testScope.runTest {
- createBiometricRepository()
+ createBiometricSettingsRepository()
val fingerprintEnabledByDevicePolicy =
collectLastValue(underTest.isFingerprintEnabledByDevicePolicy)
runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt
index 969537d2..444a2a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt
@@ -45,7 +45,7 @@
MockitoAnnotations.initMocks(this)
val testCoroutineScope = TestCoroutineScope()
underTest =
- KeyguardBouncerRepository(
+ KeyguardBouncerRepositoryImpl(
viewMediatorCallback,
systemClock,
testCoroutineScope,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
index 68fff26..8caf60f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
@@ -22,9 +22,10 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeBiometricRepository
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepositoryImpl
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.util.time.SystemClock
@@ -46,7 +47,7 @@
class AlternateBouncerInteractorTest : SysuiTestCase() {
private lateinit var underTest: AlternateBouncerInteractor
private lateinit var bouncerRepository: KeyguardBouncerRepository
- private lateinit var biometricRepository: FakeBiometricRepository
+ private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
private lateinit var deviceEntryFingerprintAuthRepository:
FakeDeviceEntryFingerprintAuthRepository
@Mock private lateinit var systemClock: SystemClock
@@ -58,19 +59,19 @@
fun setup() {
MockitoAnnotations.initMocks(this)
bouncerRepository =
- KeyguardBouncerRepository(
+ KeyguardBouncerRepositoryImpl(
mock(ViewMediatorCallback::class.java),
FakeSystemClock(),
TestCoroutineScope(),
bouncerLogger,
)
- biometricRepository = FakeBiometricRepository()
+ biometricSettingsRepository = FakeBiometricSettingsRepository()
deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
featureFlags = FakeFeatureFlags().apply { this.set(Flags.MODERN_ALTERNATE_BOUNCER, true) }
underTest =
AlternateBouncerInteractor(
bouncerRepository,
- biometricRepository,
+ biometricSettingsRepository,
deviceEntryFingerprintAuthRepository,
systemClock,
keyguardUpdateMonitor,
@@ -95,7 +96,7 @@
@Test
fun canShowAlternateBouncerForFingerprint_noFingerprintsEnrolled() {
givenCanShowAlternateBouncer()
- biometricRepository.setFingerprintEnrolled(false)
+ biometricSettingsRepository.setFingerprintEnrolled(false)
assertFalse(underTest.canShowAlternateBouncerForFingerprint())
}
@@ -103,7 +104,7 @@
@Test
fun canShowAlternateBouncerForFingerprint_strongBiometricNotAllowed() {
givenCanShowAlternateBouncer()
- biometricRepository.setStrongBiometricAllowed(false)
+ biometricSettingsRepository.setStrongBiometricAllowed(false)
assertFalse(underTest.canShowAlternateBouncerForFingerprint())
}
@@ -111,7 +112,7 @@
@Test
fun canShowAlternateBouncerForFingerprint_devicePolicyDoesNotAllowFingerprint() {
givenCanShowAlternateBouncer()
- biometricRepository.setFingerprintEnabledByDevicePolicy(false)
+ biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(false)
assertFalse(underTest.canShowAlternateBouncerForFingerprint())
}
@@ -158,13 +159,13 @@
private fun givenCanShowAlternateBouncer() {
bouncerRepository.setAlternateBouncerUIAvailable(true)
- biometricRepository.setFingerprintEnrolled(true)
- biometricRepository.setStrongBiometricAllowed(true)
- biometricRepository.setFingerprintEnabledByDevicePolicy(true)
+ biometricSettingsRepository.setFingerprintEnrolled(true)
+ biometricSettingsRepository.setStrongBiometricAllowed(true)
+ biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true)
deviceEntryFingerprintAuthRepository.setLockedOut(false)
}
private fun givenCannotShowAlternateBouncer() {
- biometricRepository.setFingerprintEnrolled(false)
+ biometricSettingsRepository.setFingerprintEnrolled(false)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 3a871b4..702f3763 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -24,6 +24,7 @@
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -437,6 +438,43 @@
}
@Test
+ fun `DOZING to GONE`() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to DOZING
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DOZING,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN biometrics succeeds with wake and unlock mode
+ keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DOZING should occur
+ assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.DOZING)
+ assertThat(info.to).isEqualTo(KeyguardState.GONE)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
fun `GONE to DOZING`() =
testScope.runTest {
// GIVEN a device with AOD not available
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
index 7f48ea1..c5e0252 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -68,13 +68,13 @@
@Mock private lateinit var keyguardBypassController: KeyguardBypassController
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
private val mainHandler = FakeHandler(Looper.getMainLooper())
- private lateinit var mPrimaryBouncerInteractor: PrimaryBouncerInteractor
+ private lateinit var underTest: PrimaryBouncerInteractor
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
DejankUtils.setImmediate(true)
- mPrimaryBouncerInteractor =
+ underTest =
PrimaryBouncerInteractor(
repository,
bouncerView,
@@ -94,7 +94,7 @@
@Test
fun testShow_isScrimmed() {
- mPrimaryBouncerInteractor.show(true)
+ underTest.show(true)
verify(repository).setOnScreenTurnedOff(false)
verify(repository).setKeyguardAuthenticated(null)
verify(repository).setPrimaryHide(false)
@@ -124,7 +124,7 @@
@Test
fun testHide() {
- mPrimaryBouncerInteractor.hide()
+ underTest.hide()
verify(falsingCollector).onBouncerHidden()
verify(keyguardStateController).notifyBouncerShowing(false)
verify(repository).setPrimaryShowingSoon(false)
@@ -137,7 +137,7 @@
@Test
fun testExpansion() {
`when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
- mPrimaryBouncerInteractor.setPanelExpansion(0.6f)
+ underTest.setPanelExpansion(0.6f)
verify(repository).setPanelExpansion(0.6f)
verify(mPrimaryBouncerCallbackInteractor).dispatchExpansionChanged(0.6f)
}
@@ -146,7 +146,7 @@
fun testExpansion_fullyShown() {
`when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
`when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
- mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_VISIBLE)
+ underTest.setPanelExpansion(EXPANSION_VISIBLE)
verify(falsingCollector).onBouncerShown()
verify(mPrimaryBouncerCallbackInteractor).dispatchFullyShown()
}
@@ -155,7 +155,7 @@
fun testExpansion_fullyHidden() {
`when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
`when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
- mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_HIDDEN)
+ underTest.setPanelExpansion(EXPANSION_HIDDEN)
verify(repository).setPrimaryVisible(false)
verify(repository).setPrimaryShow(null)
verify(repository).setPrimaryHide(true)
@@ -167,7 +167,7 @@
@Test
fun testExpansion_startingToHide() {
`when`(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
- mPrimaryBouncerInteractor.setPanelExpansion(0.1f)
+ underTest.setPanelExpansion(0.1f)
verify(repository).setPrimaryStartingToHide(true)
verify(mPrimaryBouncerCallbackInteractor).dispatchStartingToHide()
}
@@ -175,7 +175,7 @@
@Test
fun testShowMessage() {
val argCaptor = ArgumentCaptor.forClass(BouncerShowMessageModel::class.java)
- mPrimaryBouncerInteractor.showMessage("abc", null)
+ underTest.showMessage("abc", null)
verify(repository).setShowMessage(argCaptor.capture())
assertThat(argCaptor.value.message).isEqualTo("abc")
}
@@ -184,62 +184,62 @@
fun testDismissAction() {
val onDismissAction = mock(ActivityStarter.OnDismissAction::class.java)
val cancelAction = mock(Runnable::class.java)
- mPrimaryBouncerInteractor.setDismissAction(onDismissAction, cancelAction)
+ underTest.setDismissAction(onDismissAction, cancelAction)
verify(bouncerViewDelegate).setDismissAction(onDismissAction, cancelAction)
}
@Test
fun testUpdateResources() {
- mPrimaryBouncerInteractor.updateResources()
+ underTest.updateResources()
verify(repository).setResourceUpdateRequests(true)
}
@Test
fun testNotifyKeyguardAuthenticated() {
- mPrimaryBouncerInteractor.notifyKeyguardAuthenticated(true)
+ underTest.notifyKeyguardAuthenticated(true)
verify(repository).setKeyguardAuthenticated(true)
}
@Test
fun testNotifyShowedMessage() {
- mPrimaryBouncerInteractor.onMessageShown()
+ underTest.onMessageShown()
verify(repository).setShowMessage(null)
}
@Test
fun testOnScreenTurnedOff() {
- mPrimaryBouncerInteractor.onScreenTurnedOff()
+ underTest.onScreenTurnedOff()
verify(repository).setOnScreenTurnedOff(true)
}
@Test
fun testSetKeyguardPosition() {
- mPrimaryBouncerInteractor.setKeyguardPosition(0f)
+ underTest.setKeyguardPosition(0f)
verify(repository).setKeyguardPosition(0f)
}
@Test
fun testNotifyKeyguardAuthenticatedHandled() {
- mPrimaryBouncerInteractor.notifyKeyguardAuthenticatedHandled()
+ underTest.notifyKeyguardAuthenticatedHandled()
verify(repository).setKeyguardAuthenticated(null)
}
@Test
fun testNotifyUpdatedResources() {
- mPrimaryBouncerInteractor.notifyUpdatedResources()
+ underTest.notifyUpdatedResources()
verify(repository).setResourceUpdateRequests(false)
}
@Test
fun testSetBackButtonEnabled() {
- mPrimaryBouncerInteractor.setBackButtonEnabled(true)
+ underTest.setBackButtonEnabled(true)
verify(repository).setIsBackButtonEnabled(true)
}
@Test
fun testStartDisappearAnimation() {
val runnable = mock(Runnable::class.java)
- mPrimaryBouncerInteractor.startDisappearAnimation(runnable)
+ underTest.startDisappearAnimation(runnable)
verify(repository).setPrimaryStartDisappearAnimation(any(Runnable::class.java))
}
@@ -248,42 +248,42 @@
`when`(repository.primaryBouncerVisible.value).thenReturn(true)
`when`(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
`when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
- assertThat(mPrimaryBouncerInteractor.isFullyShowing()).isTrue()
+ assertThat(underTest.isFullyShowing()).isTrue()
`when`(repository.primaryBouncerVisible.value).thenReturn(false)
- assertThat(mPrimaryBouncerInteractor.isFullyShowing()).isFalse()
+ assertThat(underTest.isFullyShowing()).isFalse()
}
@Test
fun testIsScrimmed() {
`when`(repository.primaryBouncerScrimmed.value).thenReturn(true)
- assertThat(mPrimaryBouncerInteractor.isScrimmed()).isTrue()
+ assertThat(underTest.isScrimmed()).isTrue()
`when`(repository.primaryBouncerScrimmed.value).thenReturn(false)
- assertThat(mPrimaryBouncerInteractor.isScrimmed()).isFalse()
+ assertThat(underTest.isScrimmed()).isFalse()
}
@Test
fun testIsInTransit() {
`when`(repository.primaryBouncerShowingSoon.value).thenReturn(true)
- assertThat(mPrimaryBouncerInteractor.isInTransit()).isTrue()
+ assertThat(underTest.isInTransit()).isTrue()
`when`(repository.primaryBouncerShowingSoon.value).thenReturn(false)
- assertThat(mPrimaryBouncerInteractor.isInTransit()).isFalse()
+ assertThat(underTest.isInTransit()).isFalse()
`when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
- assertThat(mPrimaryBouncerInteractor.isInTransit()).isTrue()
+ assertThat(underTest.isInTransit()).isTrue()
}
@Test
fun testIsAnimatingAway() {
`when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(Runnable {})
- assertThat(mPrimaryBouncerInteractor.isAnimatingAway()).isTrue()
+ assertThat(underTest.isAnimatingAway()).isTrue()
`when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
- assertThat(mPrimaryBouncerInteractor.isAnimatingAway()).isFalse()
+ assertThat(underTest.isAnimatingAway()).isFalse()
}
@Test
fun testWillDismissWithAction() {
`when`(bouncerViewDelegate.willDismissWithActions()).thenReturn(true)
- assertThat(mPrimaryBouncerInteractor.willDismissWithAction()).isTrue()
+ assertThat(underTest.willDismissWithAction()).isTrue()
`when`(bouncerViewDelegate.willDismissWithActions()).thenReturn(false)
- assertThat(mPrimaryBouncerInteractor.willDismissWithAction()).isFalse()
+ assertThat(underTest.willDismissWithAction()).isFalse()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
new file mode 100644
index 0000000..ea7bc91
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.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.keyguard.domain.interactor
+
+import android.os.Looper
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.BouncerViewDelegate
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class PrimaryBouncerInteractorWithCoroutinesTest : SysuiTestCase() {
+ private lateinit var repository: FakeKeyguardBouncerRepository
+ @Mock private lateinit var bouncerView: BouncerView
+ @Mock private lateinit var bouncerViewDelegate: BouncerViewDelegate
+ @Mock private lateinit var keyguardStateController: KeyguardStateController
+ @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
+ @Mock private lateinit var primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor
+ @Mock private lateinit var falsingCollector: FalsingCollector
+ @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry
+ @Mock private lateinit var keyguardBypassController: KeyguardBypassController
+ @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ private val mainHandler = FakeHandler(Looper.getMainLooper())
+ private lateinit var underTest: PrimaryBouncerInteractor
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ repository = FakeKeyguardBouncerRepository()
+ underTest =
+ PrimaryBouncerInteractor(
+ repository,
+ bouncerView,
+ mainHandler,
+ keyguardStateController,
+ keyguardSecurityModel,
+ primaryBouncerCallbackInteractor,
+ falsingCollector,
+ dismissCallbackRegistry,
+ keyguardBypassController,
+ keyguardUpdateMonitor,
+ )
+ }
+
+ @Test
+ fun notInteractableWhenExpansionIsBelow90Percent() = runTest {
+ val isInteractable = collectLastValue(underTest.isInteractable)
+
+ repository.setPrimaryVisible(true)
+ repository.setPanelExpansion(0.15f)
+
+ assertThat(isInteractable()).isFalse()
+ }
+
+ @Test
+ fun notInteractableWhenExpansionAbove90PercentButNotVisible() = runTest {
+ val isInteractable = collectLastValue(underTest.isInteractable)
+
+ repository.setPrimaryVisible(false)
+ repository.setPanelExpansion(0.05f)
+
+ assertThat(isInteractable()).isFalse()
+ }
+
+ @Test
+ fun isInteractableWhenExpansionAbove90PercentAndVisible() = runTest {
+ var isInteractable = collectLastValue(underTest.isInteractable)
+
+ repository.setPrimaryVisible(true)
+ repository.setPanelExpansion(0.09f)
+
+ assertThat(isInteractable()).isTrue()
+
+ repository.setPanelExpansion(0.12f)
+ assertThat(isInteractable()).isFalse()
+
+ repository.setPanelExpansion(0f)
+ assertThat(isInteractable()).isTrue()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
new file mode 100644
index 0000000..a5b78b74
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardTransitionAnimationFlowTest : SysuiTestCase() {
+ private lateinit var underTest: KeyguardTransitionAnimationFlow
+ private lateinit var repository: FakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ repository = FakeKeyguardTransitionRepository()
+ underTest =
+ KeyguardTransitionAnimationFlow(
+ 1000.milliseconds,
+ repository.transitions,
+ )
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun zeroDurationThrowsException() = runTest {
+ val flow = underTest.createFlow(duration = 0.milliseconds, onStep = { it })
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun startTimePlusDurationGreaterThanTransitionDurationThrowsException() = runTest {
+ val flow =
+ underTest.createFlow(
+ startTime = 300.milliseconds,
+ duration = 800.milliseconds,
+ onStep = { it }
+ )
+ }
+
+ @Test
+ fun onFinishRunsWhenSpecified() = runTest {
+ val flow =
+ underTest.createFlow(
+ duration = 100.milliseconds,
+ onStep = { it },
+ onFinish = { 10f },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+ assertThat(animationValues()).isEqualTo(10f)
+ }
+
+ @Test
+ fun onCancelRunsWhenSpecified() = runTest {
+ val flow =
+ underTest.createFlow(
+ duration = 100.milliseconds,
+ onStep = { it },
+ onCancel = { 100f },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(0.5f, TransitionState.CANCELED))
+ assertThat(animationValues()).isEqualTo(100f)
+ }
+
+ @Test
+ fun usesStartTime() = runTest {
+ val flow =
+ underTest.createFlow(
+ startTime = 500.milliseconds,
+ duration = 500.milliseconds,
+ onStep = { it },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertThat(animationValues()).isEqualTo(0f)
+
+ // Should not emit a value
+ repository.sendTransitionStep(step(0.1f, TransitionState.RUNNING))
+
+ repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 0f)
+ repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 0.2f)
+ repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 0.6f)
+ repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 1f)
+ }
+
+ @Test
+ fun usesInterpolator() = runTest {
+ val flow =
+ underTest.createFlow(
+ duration = 1000.milliseconds,
+ interpolator = EMPHASIZED_ACCELERATE,
+ onStep = { it },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0f))
+ repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.5f))
+ repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.6f))
+ repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.8f))
+ repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(1f))
+ }
+
+ @Test
+ fun usesOnStepToDoubleValue() = runTest {
+ val flow =
+ underTest.createFlow(
+ duration = 1000.milliseconds,
+ onStep = { it * 2 },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertFloat(animationValues(), 0f)
+ repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 0.6f)
+ repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 1.2f)
+ repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 1.6f)
+ repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 2f)
+ }
+
+ private fun assertFloat(actual: Float?, expected: Float) {
+ assertThat(actual!!).isWithin(0.01f).of(expected)
+ }
+
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.DREAMING,
+ value = value,
+ transitionState = state,
+ ownerName = "GoneToDreamingTransitionViewModelTest"
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index 5571663..06e397d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -18,19 +18,13 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
-import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_TRANSLATION_Y
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -46,6 +40,7 @@
class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() {
private lateinit var underTest: DreamingToLockscreenTransitionViewModel
private lateinit var repository: FakeKeyguardTransitionRepository
+ private lateinit var transitionAnimation: KeyguardTransitionAnimationFlow
@Before
fun setUp() {
@@ -63,32 +58,18 @@
val job =
underTest.dreamOverlayTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+ // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(0.6f))
+ // ...up to here
+ repository.sendTransitionStep(step(0.8f))
repository.sendTransitionStep(step(1f))
- // Only 3 values should be present, since the dream overlay runs for a small fraction
- // of the overall animation time
- assertThat(values.size).isEqualTo(3)
- assertThat(values[0])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0f, DREAM_OVERLAY_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.3f, DREAM_OVERLAY_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.5f, DREAM_OVERLAY_TRANSLATION_Y)
- ) * pixels
- )
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
job.cancel()
}
@@ -100,16 +81,18 @@
val job = underTest.dreamOverlayAlpha.onEach { values.add(it) }.launchIn(this)
+ // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.1f))
repository.sendTransitionStep(step(0.5f))
+ // ...up to here
repository.sendTransitionStep(step(1f))
// Only two values should be present, since the dream overlay runs for a small fraction
// of the overall animation time
- assertThat(values.size).isEqualTo(2)
- assertThat(values[0]).isEqualTo(1f - animValue(0f, DREAM_OVERLAY_ALPHA))
- assertThat(values[1]).isEqualTo(1f - animValue(0.1f, DREAM_OVERLAY_ALPHA))
+ assertThat(values.size).isEqualTo(4)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -121,19 +104,15 @@
val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.1f))
- // Should start running here...
repository.sendTransitionStep(step(0.2f))
repository.sendTransitionStep(step(0.3f))
- // ...up to here
repository.sendTransitionStep(step(1f))
- // Only two values should be present, since the dream overlay runs for a small fraction
- // of the overall animation time
- assertThat(values.size).isEqualTo(2)
- assertThat(values[0]).isEqualTo(animValue(0.2f, LOCKSCREEN_ALPHA))
- assertThat(values[1]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+ assertThat(values.size).isEqualTo(4)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -147,58 +126,27 @@
val job =
underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
repository.sendTransitionStep(step(1f))
- assertThat(values.size).isEqualTo(4)
- assertThat(values[0])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[3])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(1f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
job.cancel()
}
- private fun animValue(stepValue: Float, params: AnimationParams): Float {
- val totalDuration = TO_LOCKSCREEN_DURATION
- val startValue = (params.startTime / totalDuration).toFloat()
-
- val multiplier = (totalDuration / params.duration).toFloat()
- return (stepValue - startValue) * multiplier
- }
-
- private fun step(value: Float): TransitionStep {
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
return TransitionStep(
from = KeyguardState.DREAMING,
to = KeyguardState.LOCKSCREEN,
value = value,
- transitionState = TransitionState.RUNNING,
+ transitionState = state,
ownerName = "DreamingToLockscreenTransitionViewModelTest"
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
index 7fa204b..14c3b50 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -59,20 +55,18 @@
val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
// Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.1f))
repository.sendTransitionStep(step(0.2f))
- // ...up to here
repository.sendTransitionStep(step(0.3f))
+ // ...up to here
repository.sendTransitionStep(step(1f))
// Only three values should be present, since the dream overlay runs for a small
- // fraction
- // of the overall animation time
- assertThat(values.size).isEqualTo(3)
- assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
- assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
- assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA))
+ // fraction of the overall animation time
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -87,45 +81,19 @@
underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
// Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
- // ...up to here
- repository.sendTransitionStep(step(1f))
// And a final reset event on CANCEL
repository.sendTransitionStep(step(0.8f, TransitionState.CANCELED))
- assertThat(values.size).isEqualTo(4)
- assertThat(values[0])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[3]).isEqualTo(0f)
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
job.cancel()
}
- private fun animValue(stepValue: Float, params: AnimationParams): Float {
- val totalDuration = TO_DREAMING_DURATION
- val startValue = (params.startTime / totalDuration).toFloat()
-
- val multiplier = (totalDuration / params.duration).toFloat()
- return (stepValue - startValue) * multiplier
- }
-
private fun step(
value: Float,
state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index 539fc2c..ed31dc3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -59,19 +55,18 @@
val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
// Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.1f))
repository.sendTransitionStep(step(0.2f))
- // ...up to here
repository.sendTransitionStep(step(0.3f))
+ // ...up to here
repository.sendTransitionStep(step(1f))
// Only three values should be present, since the dream overlay runs for a small
// fraction of the overall animation time
- assertThat(values.size).isEqualTo(3)
- assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
- assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
- assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA))
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -85,47 +80,22 @@
val job =
underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
- // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
- // ...up to here
repository.sendTransitionStep(step(1f))
// And a final reset event on FINISHED
repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
- assertThat(values.size).isEqualTo(4)
- assertThat(values[0])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[3]).isEqualTo(0f)
+ assertThat(values.size).isEqualTo(6)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+ // Validate finished value
+ assertThat(values[5]).isEqualTo(0f)
job.cancel()
}
- private fun animValue(stepValue: Float, params: AnimationParams): Float {
- val totalDuration = TO_DREAMING_DURATION
- val startValue = (params.startTime / totalDuration).toFloat()
-
- val multiplier = (totalDuration / params.duration).toFloat()
- return (stepValue - startValue) * multiplier
- }
-
private fun step(
value: Float,
state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index 759345f..458b315 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -59,19 +55,18 @@
val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
// Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.1f))
repository.sendTransitionStep(step(0.4f))
- // ...up to here
repository.sendTransitionStep(step(0.7f))
+ // ...up to here
repository.sendTransitionStep(step(1f))
// Only 3 values should be present, since the dream overlay runs for a small fraction
// of the overall animation time
- assertThat(values.size).isEqualTo(3)
- assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
- assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
- assertThat(values[2]).isEqualTo(1f - animValue(0.4f, LOCKSCREEN_ALPHA))
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -86,54 +81,51 @@
underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
// Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
repository.sendTransitionStep(step(1f))
// ...up to here
- assertThat(values.size).isEqualTo(4)
- assertThat(values[0])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[3])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(1f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
job.cancel()
}
- private fun animValue(stepValue: Float, params: AnimationParams): Float {
- val totalDuration = TO_OCCLUDED_DURATION
- val startValue = (params.startTime / totalDuration).toFloat()
+ @Test
+ fun lockscreenTranslationYIsCanceled() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
- val multiplier = (totalDuration / params.duration).toFloat()
- return (stepValue - startValue) * multiplier
- }
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
- private fun step(value: Float): TransitionStep {
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.3f, TransitionState.CANCELED))
+
+ assertThat(values.size).isEqualTo(4)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
+ // Cancel will reset the translation
+ assertThat(values[3]).isEqualTo(0)
+
+ job.cancel()
+ }
+
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING,
+ ): TransitionStep {
return TransitionStep(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.OCCLUDED,
value = value,
- transitionState = TransitionState.RUNNING,
+ transitionState = state,
ownerName = "LockscreenToOccludedTransitionViewModelTest"
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
index 98d292d..a36214e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -58,21 +54,19 @@
val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
- repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ repository.sendTransitionStep(step(0.1f))
// Should start running here...
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.4f))
repository.sendTransitionStep(step(0.5f))
- // ...up to here
repository.sendTransitionStep(step(0.6f))
+ // ...up to here
+ repository.sendTransitionStep(step(0.8f))
repository.sendTransitionStep(step(1f))
- // Only two values should be present, since the dream overlay runs for a small fraction
- // of the overall animation time
- assertThat(values.size).isEqualTo(3)
- assertThat(values[0]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
- assertThat(values[1]).isEqualTo(animValue(0.4f, LOCKSCREEN_ALPHA))
- assertThat(values[2]).isEqualTo(animValue(0.5f, LOCKSCREEN_ALPHA))
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -86,58 +80,27 @@
val job =
underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
repository.sendTransitionStep(step(1f))
- assertThat(values.size).isEqualTo(4)
- assertThat(values[0])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[3])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(1f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
job.cancel()
}
- private fun animValue(stepValue: Float, params: AnimationParams): Float {
- val totalDuration = TO_LOCKSCREEN_DURATION
- val startValue = (params.startTime / totalDuration).toFloat()
-
- val multiplier = (totalDuration / params.duration).toFloat()
- return (stepValue - startValue) * multiplier
- }
-
- private fun step(value: Float): TransitionStep {
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
return TransitionStep(
from = KeyguardState.OCCLUDED,
to = KeyguardState.LOCKSCREEN,
value = value,
- transitionState = TransitionState.RUNNING,
+ transitionState = state,
ownerName = "OccludedToLockscreenTransitionViewModelTest"
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
index 1d6e980..670f117 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
@@ -113,5 +113,6 @@
recommendations = emptyList(),
dismissIntent = null,
headphoneConnectionTimeMillis = 0,
- instanceId = InstanceId.fakeInstanceId(-1)
+ instanceId = InstanceId.fakeInstanceId(-1),
+ expiryTimeMs = 0,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
index c0639f3..0a5b124 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
@@ -79,7 +79,7 @@
USER_ID, true, APP, null, ARTIST, TITLE, null,
new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L,
- InstanceId.fakeInstanceId(-1), -1, false);
+ InstanceId.fakeInstanceId(-1), -1, false, null);
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
index 9d33e6f..eb6235c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
@@ -27,11 +27,13 @@
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.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
@@ -40,11 +42,11 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
private const val KEY = "TEST_KEY"
@@ -72,6 +74,7 @@
@Mock private lateinit var smartspaceData: SmartspaceMediaData
@Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
@Mock private lateinit var logger: MediaUiEventLogger
+ @Mock private lateinit var mediaFlags: MediaFlags
private lateinit var mediaDataFilter: MediaDataFilter
private lateinit var dataMain: MediaData
@@ -82,6 +85,7 @@
fun setup() {
MockitoAnnotations.initMocks(this)
MediaPlayerData.clear()
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
mediaDataFilter =
MediaDataFilter(
context,
@@ -90,7 +94,8 @@
lockscreenUserManager,
executor,
clock,
- logger
+ logger,
+ mediaFlags
)
mediaDataFilter.mediaDataManager = mediaDataManager
mediaDataFilter.addListener(listener)
@@ -108,19 +113,20 @@
)
dataGuest = dataMain.copy(userId = USER_GUEST)
- `when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
- `when`(smartspaceData.isActive).thenReturn(true)
- `when`(smartspaceData.isValid()).thenReturn(true)
- `when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
- `when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem))
- `when`(smartspaceData.headphoneConnectionTimeMillis)
+ whenever(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
+ whenever(smartspaceData.isActive).thenReturn(true)
+ whenever(smartspaceData.isValid()).thenReturn(true)
+ whenever(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
+ whenever(smartspaceData.recommendations)
+ .thenReturn(listOf(smartspaceMediaRecommendationItem))
+ whenever(smartspaceData.headphoneConnectionTimeMillis)
.thenReturn(clock.currentTimeMillis() - 100)
- `when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
+ whenever(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
}
private fun setUser(id: Int) {
- `when`(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
- `when`(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
+ whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
+ whenever(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
mediaDataFilter.handleUserSwitched(id)
}
@@ -277,7 +283,7 @@
@Test
fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() {
- `when`(smartspaceData.isActive).thenReturn(false)
+ whenever(smartspaceData.isActive).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
@@ -285,7 +291,7 @@
@Test
fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() {
- `when`(smartspaceData.isValid()).thenReturn(false)
+ whenever(smartspaceData.isValid()).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
@@ -293,8 +299,8 @@
@Test
fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() {
- `when`(smartspaceData.isActive).thenReturn(true)
- `when`(smartspaceData.isValid()).thenReturn(true)
+ whenever(smartspaceData.isActive).thenReturn(true)
+ whenever(smartspaceData.isValid()).thenReturn(true)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
@@ -349,7 +355,7 @@
@Test
fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() {
- `when`(smartspaceData.isActive).thenReturn(false)
+ whenever(smartspaceData.isActive).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
@@ -379,7 +385,7 @@
@Test
fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() {
- `when`(smartspaceData.isActive).thenReturn(false)
+ whenever(smartspaceData.isActive).thenReturn(false)
val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
@@ -395,7 +401,7 @@
@Test
fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() {
- `when`(smartspaceData.isActive).thenReturn(false)
+ whenever(smartspaceData.isActive).thenReturn(false)
// WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -418,7 +424,7 @@
@Test
fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() {
- `when`(smartspaceData.isValid()).thenReturn(false)
+ whenever(smartspaceData.isValid()).thenReturn(false)
// WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -513,4 +519,59 @@
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
}
+
+ @Test
+ fun testOnSmartspaceLoaded_persistentEnabled_isInactive_notifiesListeners() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ whenever(smartspaceData.isActive).thenReturn(false)
+
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+ assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+ assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
+ }
+
+ @Test
+ fun testOnSmartspaceLoaded_persistentEnabled_inactive_hasRecentMedia_staysInactive() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ whenever(smartspaceData.isActive).thenReturn(false)
+
+ // If there is media that was recently played but inactive
+ 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))
+
+ // And an inactive recommendation is loaded
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ // Smartspace is loaded but the media stays inactive
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+ verify(listener, never())
+ .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+ assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+ assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
+ }
+
+ @Test
+ fun testOnSwipeToDismiss_persistentEnabled_recommendationSetInactive() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+ val data =
+ EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+ targetId = SMARTSPACE_KEY,
+ isActive = true,
+ packageName = SMARTSPACE_PACKAGE,
+ recommendations = listOf(smartspaceMediaRecommendationItem),
+ )
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, data)
+ mediaDataFilter.onSwipeToDismiss()
+
+ verify(mediaDataManager).setRecommendationInactive(eq(SMARTSPACE_KEY))
+ verify(mediaDataManager, never())
+ .dismissSmartspaceRecommendation(eq(SMARTSPACE_KEY), anyLong())
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
index 1ac6695..44e2fbd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
@@ -46,6 +46,8 @@
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.EXTRA_KEY_TRIGGER_SOURCE
+import com.android.systemui.media.controls.models.recommendation.EXTRA_VALUE_TRIGGER_PERIODIC
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
@@ -82,6 +84,8 @@
private const val KEY = "KEY"
private const val KEY_2 = "KEY_2"
private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+private const val SMARTSPACE_CREATION_TIME = 1234L
+private const val SMARTSPACE_EXPIRY_TIME = 5678L
private const val PACKAGE_NAME = "com.example.app"
private const val SYSTEM_PACKAGE_NAME = "com.android.systemui"
private const val APP_NAME = "SystemUI"
@@ -230,10 +234,12 @@
whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE)
whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
- whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
+ whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME)
+ whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true)
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false)
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
}
@@ -644,27 +650,8 @@
build()
}
val currentTime = clock.elapsedRealtime()
- 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)
- )
+ addResumeControlAndLoad(desc)
+
val data = mediaDataCaptor.value
assertThat(data.resumption).isTrue()
assertThat(data.song).isEqualTo(SESSION_TITLE)
@@ -690,27 +677,8 @@
build()
}
val currentTime = clock.elapsedRealtime()
- 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)
- )
+ addResumeControlAndLoad(desc)
+
val data = mediaDataCaptor.value
assertThat(data.resumption).isTrue()
assertThat(data.song).isEqualTo(SESSION_TITLE)
@@ -723,6 +691,84 @@
}
@Test
+ fun testAddResumptionControls_hasPartialProgress() {
+ whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+ // WHEN resumption controls are added with partial progress
+ val progress = 0.5
+ val extras =
+ Bundle().apply {
+ putInt(
+ MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
+ )
+ putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress)
+ }
+ val desc =
+ MediaDescription.Builder().run {
+ setTitle(SESSION_TITLE)
+ setExtras(extras)
+ build()
+ }
+ addResumeControlAndLoad(desc)
+
+ val data = mediaDataCaptor.value
+ assertThat(data.resumption).isTrue()
+ assertThat(data.resumeProgress).isEqualTo(progress)
+ }
+
+ @Test
+ fun testAddResumptionControls_hasNotPlayedProgress() {
+ whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+ // WHEN resumption controls are added that have not been played
+ val extras =
+ Bundle().apply {
+ putInt(
+ MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
+ )
+ }
+ val desc =
+ MediaDescription.Builder().run {
+ setTitle(SESSION_TITLE)
+ setExtras(extras)
+ build()
+ }
+ addResumeControlAndLoad(desc)
+
+ val data = mediaDataCaptor.value
+ assertThat(data.resumption).isTrue()
+ assertThat(data.resumeProgress).isEqualTo(0)
+ }
+
+ @Test
+ fun testAddResumptionControls_hasFullProgress() {
+ whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+ // WHEN resumption controls are added with progress info
+ val extras =
+ Bundle().apply {
+ putInt(
+ MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
+ )
+ }
+ val desc =
+ MediaDescription.Builder().run {
+ setTitle(SESSION_TITLE)
+ setExtras(extras)
+ build()
+ }
+ addResumeControlAndLoad(desc)
+
+ // THEN the media data includes the progress
+ val data = mediaDataCaptor.value
+ assertThat(data.resumption).isTrue()
+ assertThat(data.resumeProgress).isEqualTo(1)
+ }
+
+ @Test
fun testResumptionDisabled_dismissesResumeControls() {
// WHEN there are resume controls and resumption is switched off
val desc =
@@ -730,26 +776,8 @@
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)
- )
+ addResumeControlAndLoad(desc)
+
val data = mediaDataCaptor.value
mediaDataManager.setMediaResumptionEnabled(false)
@@ -825,8 +853,9 @@
cardAction = mediaSmartspaceBaseAction,
recommendations = validRecommendationList,
dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = 1234L,
- instanceId = InstanceId.fakeInstanceId(instanceId)
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
@@ -848,8 +877,9 @@
targetId = KEY_MEDIA_SMARTSPACE,
isActive = true,
dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = 1234L,
- instanceId = InstanceId.fakeInstanceId(instanceId)
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
@@ -879,8 +909,9 @@
targetId = KEY_MEDIA_SMARTSPACE,
isActive = true,
dismissIntent = null,
- headphoneConnectionTimeMillis = 1234L,
- instanceId = InstanceId.fakeInstanceId(instanceId)
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
@@ -909,6 +940,129 @@
}
@Test
+ fun testOnSmartspaceMediaDataLoaded_persistentEnabled_headphoneTrigger_isActive() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+ 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 = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
+ )
+ ),
+ eq(false)
+ )
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataLoaded_persistentEnabled_periodicTrigger_notActive() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ val extras =
+ Bundle().apply {
+ putString("package_name", PACKAGE_NAME)
+ putParcelable("dismiss_intent", DISMISS_INTENT)
+ putString(EXTRA_KEY_TRIGGER_SOURCE, EXTRA_VALUE_TRIGGER_PERIODIC)
+ }
+ whenever(mediaSmartspaceBaseAction.extras).thenReturn(extras)
+
+ smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+ val instanceId = instanceIdSequence.lastInstanceId
+
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(
+ eq(KEY_MEDIA_SMARTSPACE),
+ eq(
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = false,
+ packageName = PACKAGE_NAME,
+ cardAction = mediaSmartspaceBaseAction,
+ recommendations = validRecommendationList,
+ dismissIntent = DISMISS_INTENT,
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
+ )
+ ),
+ eq(false)
+ )
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataLoaded_persistentEnabled_noTargets_inactive() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+ smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+ val instanceId = instanceIdSequence.lastInstanceId
+
+ smartspaceMediaDataProvider.onTargetsAvailable(listOf())
+ uiExecutor.advanceClockToLast()
+ uiExecutor.runAllReady()
+
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(
+ eq(KEY_MEDIA_SMARTSPACE),
+ eq(
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = false,
+ packageName = PACKAGE_NAME,
+ cardAction = mediaSmartspaceBaseAction,
+ recommendations = validRecommendationList,
+ dismissIntent = DISMISS_INTENT,
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
+ )
+ ),
+ eq(false)
+ )
+ verify(listener, never()).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
+ }
+
+ @Test
+ fun testSetRecommendationInactive_notifiesListeners() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+ smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+ val instanceId = instanceIdSequence.lastInstanceId
+
+ mediaDataManager.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
+ uiExecutor.advanceClockToLast()
+ uiExecutor.runAllReady()
+
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(
+ eq(KEY_MEDIA_SMARTSPACE),
+ eq(
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = false,
+ packageName = PACKAGE_NAME,
+ cardAction = mediaSmartspaceBaseAction,
+ recommendations = validRecommendationList,
+ dismissIntent = DISMISS_INTENT,
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
+ )
+ ),
+ eq(false)
+ )
+ }
+
+ @Test
fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
// WHEN media recommendation setting is off
Settings.Secure.putInt(
@@ -1690,4 +1844,29 @@
stateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 1.0f)
whenever(controller.playbackState).thenReturn(stateBuilder.build())
}
+
+ /** Helper function to add a resumption control and capture the resulting MediaData */
+ private fun addResumeControlAndLoad(desc: MediaDescription) {
+ 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)
+ )
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
index 92bf84c..8baa06a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
@@ -25,13 +25,16 @@
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.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.util.concurrency.FakeExecutor
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.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -48,7 +51,6 @@
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"
@@ -56,6 +58,7 @@
private const val SESSION_KEY = "SESSION_KEY"
private const val SESSION_ARTIST = "SESSION_ARTIST"
private const val SESSION_TITLE = "SESSION_TITLE"
+private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
private fun <T> anyObject(): T {
return Mockito.anyObject<T>()
@@ -85,10 +88,13 @@
private lateinit var resumeData: MediaData
private lateinit var mediaTimeoutListener: MediaTimeoutListener
private var clock = FakeSystemClock()
+ @Mock private lateinit var mediaFlags: MediaFlags
+ @Mock private lateinit var smartspaceData: SmartspaceMediaData
@Before
fun setup() {
- `when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
+ whenever(mediaControllerFactory.create(any())).thenReturn(mediaController)
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
executor = FakeExecutor(clock)
mediaTimeoutListener =
MediaTimeoutListener(
@@ -96,7 +102,8 @@
executor,
logger,
statusBarStateController,
- clock
+ clock,
+ mediaFlags,
)
mediaTimeoutListener.timeoutCallback = timeoutCallback
mediaTimeoutListener.stateCallback = stateCallback
@@ -133,9 +140,9 @@
@Test
fun testOnMediaDataLoaded_registersPlaybackListener() {
val playingState = mock(android.media.session.PlaybackState::class.java)
- `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
+ whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
- `when`(mediaController.playbackState).thenReturn(playingState)
+ whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
verify(logger).logPlaybackState(eq(KEY), eq(playingState))
@@ -188,8 +195,8 @@
// To playing
val playingState = mock(android.media.session.PlaybackState::class.java)
- `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
- `when`(mediaController.playbackState).thenReturn(playingState)
+ whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
+ whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
verify(mediaController).unregisterCallback(anyObject())
verify(mediaController).registerCallback(anyObject())
@@ -208,8 +215,8 @@
// Migrate, still not playing
val playingState = mock(android.media.session.PlaybackState::class.java)
- `when`(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
- `when`(mediaController.playbackState).thenReturn(playingState)
+ whenever(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
+ whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
// The number of queued timeout tasks remains the same. The timeout task isn't cancelled nor
@@ -296,8 +303,8 @@
// WHEN we get an update with media playing
val playingState = mock(android.media.session.PlaybackState::class.java)
- `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
- `when`(mediaController.playbackState).thenReturn(playingState)
+ whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
+ whenever(mediaController.playbackState).thenReturn(playingState)
val mediaPlaying = mediaData.copy(isPlaying = true)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPlaying)
@@ -347,7 +354,7 @@
// WHEN regular media is paused
val pausedState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
- `when`(mediaController.playbackState).thenReturn(pausedState)
+ whenever(mediaController.playbackState).thenReturn(pausedState)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
assertThat(executor.numPending()).isEqualTo(1)
@@ -379,7 +386,7 @@
// AND that media is resumed
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
- `when`(mediaController.playbackState).thenReturn(playingState)
+ whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
// THEN the timeout length is changed to a regular media control
@@ -593,8 +600,91 @@
assertThat(executor.numPending()).isEqualTo(1)
}
+ @Test
+ fun testSmartspaceDataLoaded_schedulesTimeout() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ val duration = 60_000
+ val createTime = 1234L
+ val expireTime = createTime + duration
+ whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
+ whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
+
+ mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+ assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(executor.advanceClockToNext()).isEqualTo(duration)
+ }
+
+ @Test
+ fun testSmartspaceMediaData_timesOut_invokesCallback() {
+ // Given a pending timeout
+ testSmartspaceDataLoaded_schedulesTimeout()
+
+ executor.runAllReady()
+ verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true))
+ }
+
+ @Test
+ fun testSmartspaceDataLoaded_alreadyExists_updatesTimeout() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ val duration = 100
+ val createTime = 1234L
+ val expireTime = createTime + duration
+ whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
+ whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
+
+ mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+ assertThat(executor.numPending()).isEqualTo(1)
+
+ val expiryLonger = expireTime + duration
+ whenever(smartspaceData.expiryTimeMs).thenReturn(expiryLonger)
+ mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(executor.advanceClockToNext()).isEqualTo(duration * 2)
+ }
+
+ @Test
+ fun testSmartspaceDataRemoved_cancelTimeout() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+ mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+ assertThat(executor.numPending()).isEqualTo(1)
+
+ mediaTimeoutListener.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+ assertThat(executor.numPending()).isEqualTo(0)
+ }
+
+ @Test
+ fun testSmartspaceData_dozedPastTimeout_invokedOnWakeup() {
+ // Given a pending timeout
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
+ val duration = 60_000
+ val createTime = 1234L
+ val expireTime = createTime + duration
+ whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
+ whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
+
+ mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+ assertThat(executor.numPending()).isEqualTo(1)
+
+ // And we doze past the scheduled timeout
+ val time = clock.currentTimeMillis()
+ clock.setElapsedRealtime(time + duration * 2)
+ assertThat(executor.numPending()).isEqualTo(1)
+
+ // Then when no longer dozing, the timeout runs immediately
+ dozingCallbackCaptor.value.onDozingChanged(false)
+ verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true))
+ verify(logger).logTimeout(eq(SMARTSPACE_KEY))
+
+ // and cancel any later scheduled timeout
+ assertThat(executor.numPending()).isEqualTo(0)
+ }
+
private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
- `when`(mediaController.playbackState).thenReturn(state)
+ whenever(mediaController.playbackState).thenReturn(state)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
}
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
index 5e5dc8b..e201b6b 100644
--- 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
@@ -47,6 +47,7 @@
import com.android.systemui.util.time.FakeSystemClock
import javax.inject.Provider
import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
@@ -126,6 +127,7 @@
whenever(mediaControlPanelFactory.get()).thenReturn(panel)
whenever(panel.mediaViewController).thenReturn(mediaViewController)
whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
MediaPlayerData.clear()
}
@@ -703,4 +705,39 @@
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
)
}
+
+ @Test
+ fun testRecommendation_persistentEnabled_newSmartspaceLoaded_updatesSort() {
+ testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded()
+
+ // When an update to existing smartspace data is loaded
+ listener.value.onSmartspaceMediaDataLoaded(
+ SMARTSPACE_KEY,
+ EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
+ true
+ )
+
+ // Then the carousel is updated
+ assertTrue(MediaPlayerData.playerKeys().elementAt(0).data.active)
+ assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active)
+ }
+
+ @Test
+ fun testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+ // When inactive smartspace data is loaded
+ listener.value.onSmartspaceMediaDataLoaded(
+ SMARTSPACE_KEY,
+ EMPTY_SMARTSPACE_MEDIA_DATA,
+ false
+ )
+
+ // Then it is added to the carousel with correct state
+ assertTrue(MediaPlayerData.playerKeys().elementAt(0).isSsMediaRec)
+ assertFalse(MediaPlayerData.playerKeys().elementAt(0).data.active)
+
+ assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).isSsMediaRec)
+ assertFalse(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index ce22b19..55a33b6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -52,6 +52,7 @@
import androidx.constraintlayout.widget.Barrier
import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.LiveData
+import androidx.media.utils.MediaConstants
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.internal.widget.CachingIconView
@@ -206,6 +207,12 @@
@Mock private lateinit var coverContainer3: ViewGroup
@Mock private lateinit var recAppIconItem: CachingIconView
@Mock private lateinit var recCardTitle: TextView
+ @Mock private lateinit var recProgressBar1: SeekBar
+ @Mock private lateinit var recProgressBar2: SeekBar
+ @Mock private lateinit var recProgressBar3: SeekBar
+ @Mock private lateinit var recSubtitleMock1: TextView
+ @Mock private lateinit var recSubtitleMock2: TextView
+ @Mock private lateinit var recSubtitleMock3: TextView
@Mock private lateinit var coverItem: ImageView
private lateinit var coverItem1: ImageView
private lateinit var coverItem2: ImageView
@@ -906,6 +913,17 @@
}
@Test
+ fun bind_resumeState_withProgress() {
+ val progress = 0.5
+ val state = mediaData.copy(resumption = true, resumeProgress = progress)
+
+ player.attachPlayer(viewHolder)
+ player.bindPlayer(state, PACKAGE)
+
+ verify(seekBarViewModel).updateStaticProgress(progress)
+ }
+
+ @Test
fun bindNotificationActions() {
val icon = context.getDrawable(android.R.drawable.ic_media_play)
val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
@@ -2070,6 +2088,10 @@
whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
whenever(recommendationViewHolder.mediaCoverItems)
.thenReturn(listOf(coverItem, coverItem, coverItem))
+ whenever(recommendationViewHolder.mediaProgressBars)
+ .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
+ whenever(recommendationViewHolder.mediaSubtitles)
+ .thenReturn(listOf(recSubtitleMock1, recSubtitleMock2, recSubtitleMock3))
val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
@@ -2108,6 +2130,65 @@
}
@Test
+ fun bindRecommendationWithProgressBars() {
+ fakeFeatureFlag.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, true)
+ whenever(recommendationViewHolder.mediaAppIcons)
+ .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
+ whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
+ whenever(recommendationViewHolder.mediaCoverItems)
+ .thenReturn(listOf(coverItem, coverItem, coverItem))
+ whenever(recommendationViewHolder.mediaProgressBars)
+ .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
+ whenever(recommendationViewHolder.mediaSubtitles)
+ .thenReturn(listOf(recSubtitleMock1, recSubtitleMock2, recSubtitleMock3))
+
+ val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(bmp)
+ canvas.drawColor(Color.RED)
+ val albumArt = Icon.createWithBitmap(bmp)
+ val bundle =
+ Bundle().apply {
+ putInt(
+ MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
+ )
+ putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.5)
+ }
+ val data =
+ smartspaceData.copy(
+ recommendations =
+ listOf(
+ SmartspaceAction.Builder("id1", "title1")
+ .setSubtitle("subtitle1")
+ .setIcon(albumArt)
+ .setExtras(bundle)
+ .build(),
+ SmartspaceAction.Builder("id2", "title2")
+ .setSubtitle("subtitle1")
+ .setIcon(albumArt)
+ .setExtras(Bundle.EMPTY)
+ .build(),
+ SmartspaceAction.Builder("id3", "title3")
+ .setSubtitle("subtitle1")
+ .setIcon(albumArt)
+ .setExtras(Bundle.EMPTY)
+ .build()
+ )
+ )
+
+ player.attachRecommendation(recommendationViewHolder)
+ player.bindRecommendation(data)
+
+ verify(recProgressBar1).setProgress(50)
+ verify(recProgressBar1).visibility = View.VISIBLE
+ verify(recProgressBar2).visibility = View.GONE
+ verify(recProgressBar3).visibility = View.GONE
+ verify(recSubtitleMock1).visibility = View.GONE
+ verify(recSubtitleMock2).visibility = View.VISIBLE
+ verify(recSubtitleMock3).visibility = View.VISIBLE
+ }
+
+ @Test
fun onButtonClick_touchRippleFlagEnabled_playsTouchRipple() {
fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true)
val semanticActions =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index c63ca3d..db890f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -45,6 +45,7 @@
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
+import com.android.systemui.temporarydisplay.chipbar.ChipbarAnimator
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger
import com.android.systemui.temporarydisplay.chipbar.SwipeChipbarAwayGestureHandler
@@ -145,6 +146,7 @@
configurationController,
dumpManager,
powerManager,
+ ChipbarAnimator(),
falsingManager,
falsingCollector,
swipeHandler,
@@ -622,7 +624,7 @@
}
@Test
- fun commandQueueCallback_receiverSucceededThenReceiverTriggered_invalidTransitionLogged() {
+ fun commandQueueCallback_receiverSucceededThenThisDeviceSucceeded_invalidTransitionLogged() {
displayReceiverTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
@@ -632,7 +634,7 @@
reset(windowManager)
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
routeInfo,
null
)
@@ -642,7 +644,7 @@
}
@Test
- fun commandQueueCallback_thisDeviceSucceededThenThisDeviceTriggered_invalidTransitionLogged() {
+ fun commandQueueCallback_thisDeviceSucceededThenReceiverSucceeded_invalidTransitionLogged() {
displayThisDeviceTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
@@ -652,7 +654,7 @@
reset(windowManager)
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
routeInfo,
null
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
new file mode 100644
index 0000000..f42bfb8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.navigationbar.gestural
+
+import android.os.Handler
+import android.os.Looper
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import android.view.ViewConfiguration
+import android.view.WindowManager
+import androidx.test.filters.SmallTest
+import com.android.internal.util.LatencyTracker
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.NavigationEdgeBackPlugin
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BackPanelControllerTest : SysuiTestCase() {
+ companion object {
+ private const val START_X: Float = 0f
+ }
+ private lateinit var mBackPanelController: BackPanelController
+ private lateinit var testableLooper: TestableLooper
+ private var triggerThreshold: Float = 0.0f
+ private val touchSlop = ViewConfiguration.get(context).scaledEdgeSlop
+ @Mock private lateinit var vibratorHelper: VibratorHelper
+ @Mock private lateinit var windowManager: WindowManager
+ @Mock private lateinit var configurationController: ConfigurationController
+ @Mock private lateinit var latencyTracker: LatencyTracker
+ @Mock private lateinit var layoutParams: WindowManager.LayoutParams
+ @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ mBackPanelController =
+ BackPanelController(
+ context,
+ windowManager,
+ ViewConfiguration.get(context),
+ Handler.createAsync(Looper.myLooper()),
+ vibratorHelper,
+ configurationController,
+ latencyTracker
+ )
+ mBackPanelController.setLayoutParams(layoutParams)
+ mBackPanelController.setBackCallback(backCallback)
+ mBackPanelController.setIsLeftPanel(true)
+ testableLooper = TestableLooper.get(this)
+ triggerThreshold = mBackPanelController.params.staticTriggerThreshold
+ }
+
+ @Test
+ fun handlesActionDown() {
+ startTouch()
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.GONE)
+ }
+
+ @Test
+ fun staysHiddenBeforeSlopCrossed() {
+ startTouch()
+ // Move just enough to not cross the touch slop
+ continueTouch(START_X + touchSlop - 1)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.GONE)
+ }
+
+ @Test
+ fun handlesDragSlopCrossed() {
+ startTouch()
+ continueTouch(START_X + touchSlop + 1)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.ENTRY)
+ }
+
+ @Test
+ fun handlesBackCommitted() {
+ startTouch()
+ // Move once to cross the touch slop
+ continueTouch(START_X + touchSlop.toFloat() + 1)
+ // Move again to cross the back trigger threshold
+ continueTouch(START_X + touchSlop + triggerThreshold + 1)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.ACTIVE)
+ verify(backCallback).setTriggerBack(true)
+ testableLooper.moveTimeForward(100)
+ testableLooper.processAllMessages()
+ verify(vibratorHelper).vibrate(VIBRATE_ACTIVATED_EFFECT)
+
+ finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1)
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.FLUNG)
+ verify(backCallback).triggerBack()
+ }
+
+ @Test
+ fun handlesBackCancelled() {
+ startTouch()
+ continueTouch(START_X + touchSlop.toFloat() + 1)
+ continueTouch(
+ START_X + touchSlop + triggerThreshold -
+ mBackPanelController.params.deactivationSwipeTriggerThreshold
+ )
+ clearInvocations(backCallback)
+ Thread.sleep(MIN_DURATION_ACTIVE_ANIMATION)
+ // Move in the opposite direction to cross the deactivation threshold and cancel back
+ continueTouch(START_X)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.INACTIVE)
+ verify(backCallback).setTriggerBack(false)
+ verify(vibratorHelper).vibrate(VIBRATE_DEACTIVATED_EFFECT)
+
+ finishTouchActionUp(START_X)
+ verify(backCallback).cancelBack()
+ }
+
+ private fun startTouch() {
+ mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f))
+ }
+
+ private fun continueTouch(x: Float) {
+ mBackPanelController.onMotionEvent(createMotionEvent(ACTION_MOVE, x, 0f))
+ }
+
+ private fun finishTouchActionUp(x: Float) {
+ mBackPanelController.onMotionEvent(createMotionEvent(ACTION_UP, x, 0f))
+ }
+
+ private fun createMotionEvent(action: Int, x: Float, y: Float): MotionEvent {
+ return MotionEvent.obtain(0L, 0L, action, x, y, 0)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index 8440455..39c4e06 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -23,10 +23,14 @@
import android.os.UserManager
import android.test.suitebuilder.annotation.SmallTest
import androidx.test.runner.AndroidJUnit4
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
+import com.android.systemui.notetask.NoteTaskController.Companion.INTENT_EXTRA_USE_STYLUS_MODE
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
+import com.android.systemui.notetask.NoteTaskInfoResolver.NoteTaskInfo
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.android.wm.shell.bubbles.Bubbles
@@ -36,8 +40,8 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
/**
@@ -50,24 +54,23 @@
@RunWith(AndroidJUnit4::class)
internal class NoteTaskControllerTest : SysuiTestCase() {
- private val notesIntent = Intent(ACTION_CREATE_NOTE)
-
@Mock lateinit var context: Context
@Mock lateinit var packageManager: PackageManager
- @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver
+ @Mock lateinit var resolver: NoteTaskInfoResolver
@Mock lateinit var bubbles: Bubbles
@Mock lateinit var optionalBubbles: Optional<Bubbles>
@Mock lateinit var keyguardManager: KeyguardManager
@Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager>
@Mock lateinit var optionalUserManager: Optional<UserManager>
@Mock lateinit var userManager: UserManager
+ @Mock lateinit var uiEventLogger: UiEventLogger
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(context.packageManager).thenReturn(packageManager)
- whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent)
+ whenever(resolver.resolveInfo()).thenReturn(NoteTaskInfo(NOTES_PACKAGE_NAME, NOTES_UID))
whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager)
whenever(optionalUserManager.orElse(null)).thenReturn(userManager)
@@ -77,101 +80,182 @@
private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController {
return NoteTaskController(
context = context,
- intentResolver = noteTaskIntentResolver,
+ resolver = resolver,
optionalBubbles = optionalBubbles,
optionalKeyguardManager = optionalKeyguardManager,
optionalUserManager = optionalUserManager,
isEnabled = isEnabled,
+ uiEventLogger = uiEventLogger,
)
}
// region showNoteTask
@Test
- fun showNoteTask_keyguardIsLocked_shouldStartActivity() {
+ fun showNoteTask_keyguardIsLocked_shouldStartActivityAndLogUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE,
+ )
- verify(context).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(context).startActivity(capture(intentCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ verifyZeroInteractions(bubbles)
+ verify(uiEventLogger)
+ .log(
+ ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE,
+ NOTES_UID,
+ NOTES_PACKAGE_NAME
+ )
}
@Test
- fun showNoteTask_keyguardIsUnlocked_shouldStartBubbles() {
+ fun showNoteTask_keyguardIsUnlocked_shouldStartBubblesAndLogUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(bubbles).showOrHideAppBubble(notesIntent)
- verify(context, never()).startActivity(notesIntent)
+ verifyZeroInteractions(context)
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(bubbles).showOrHideAppBubble(capture(intentCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ verify(uiEventLogger)
+ .log(
+ ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ NOTES_UID,
+ NOTES_PACKAGE_NAME
+ )
}
@Test
- fun showNoteTask_isInMultiWindowMode_shouldStartActivity() {
+ fun showNoteTask_keyguardIsUnlocked_uiEventIsNull_shouldStartBubblesWithoutLoggingUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = true)
+ createNoteTaskController().showNoteTask(isInMultiWindowMode = false, uiEvent = null)
- verify(context).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context)
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(bubbles).showOrHideAppBubble(capture(intentCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ verifyZeroInteractions(uiEventLogger)
+ }
+
+ @Test
+ fun showNoteTask_isInMultiWindowMode_shouldStartActivityAndLogUiEvent() {
+ whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
+
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = true,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT,
+ )
+
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(context).startActivity(capture(intentCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ verifyZeroInteractions(bubbles)
+ verify(uiEventLogger)
+ .log(ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT, NOTES_UID, NOTES_PACKAGE_NAME)
}
@Test
fun showNoteTask_bubblesIsNull_shouldDoNothing() {
whenever(optionalBubbles.orElse(null)).thenReturn(null)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_keyguardManagerIsNull_shouldDoNothing() {
whenever(optionalKeyguardManager.orElse(null)).thenReturn(null)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_userManagerIsNull_shouldDoNothing() {
whenever(optionalUserManager.orElse(null)).thenReturn(null)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_intentResolverReturnsNull_shouldDoNothing() {
- whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null)
+ whenever(resolver.resolveInfo()).thenReturn(null)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_flagDisabled_shouldDoNothing() {
- createNoteTaskController(isEnabled = false).showNoteTask()
+ createNoteTaskController(isEnabled = false)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_userIsLocked_shouldDoNothing() {
whenever(userManager.isUserUnlocked).thenReturn(false)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
// endregion
@@ -206,4 +290,9 @@
assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString())
}
// endregion
+
+ private companion object {
+ const val NOTES_PACKAGE_NAME = "com.android.note.app"
+ const val NOTES_UID = 123456
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
new file mode 100644
index 0000000..d6495d8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import android.app.role.RoleManager
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.eq
+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.Mock
+import org.mockito.Mockito.any
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskInfoResolver].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskInfoResolverTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskInfoResolverTest : SysuiTestCase() {
+
+ @Mock lateinit var packageManager: PackageManager
+ @Mock lateinit var roleManager: RoleManager
+
+ private lateinit var underTest: NoteTaskInfoResolver
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest = NoteTaskInfoResolver(context, roleManager, packageManager)
+ }
+
+ @Test
+ fun resolveInfo_shouldReturnInfo() {
+ val packageName = "com.android.note.app"
+ val uid = 123456
+ whenever(roleManager.getRoleHoldersAsUser(NoteTaskInfoResolver.ROLE_NOTES, context.user))
+ .then { listOf(packageName) }
+ whenever(
+ packageManager.getApplicationInfoAsUser(
+ eq(packageName),
+ any<PackageManager.ApplicationInfoFlags>(),
+ eq(context.user)
+ )
+ )
+ .thenReturn(ApplicationInfo().apply { this.uid = uid })
+
+ val actual = underTest.resolveInfo()
+
+ requireNotNull(actual) { "Note task info must not be null" }
+ assertThat(actual.packageName).isEqualTo(packageName)
+ assertThat(actual.uid).isEqualTo(uid)
+ }
+
+ @Test
+ fun resolveInfo_packageManagerThrowsException_shouldReturnInfoWithZeroUid() {
+ val packageName = "com.android.note.app"
+ whenever(roleManager.getRoleHoldersAsUser(NoteTaskInfoResolver.ROLE_NOTES, context.user))
+ .then { listOf(packageName) }
+ whenever(
+ packageManager.getApplicationInfoAsUser(
+ eq(packageName),
+ any<PackageManager.ApplicationInfoFlags>(),
+ eq(context.user)
+ )
+ )
+ .thenThrow(PackageManager.NameNotFoundException(packageName))
+
+ val actual = underTest.resolveInfo()
+
+ requireNotNull(actual) { "Note task info must not be null" }
+ assertThat(actual.packageName).isEqualTo(packageName)
+ assertThat(actual.uid).isEqualTo(0)
+ }
+
+ @Test
+ fun resolveInfo_noRoleHolderIsSet_shouldReturnNull() {
+ whenever(roleManager.getRoleHoldersAsUser(eq(NoteTaskInfoResolver.ROLE_NOTES), any()))
+ .then { listOf<String>() }
+
+ val actual = underTest.resolveInfo()
+
+ assertThat(actual).isNull()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 010ac5b..53720ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -15,11 +15,14 @@
*/
package com.android.systemui.notetask
+import android.app.KeyguardManager
import android.test.suitebuilder.annotation.SmallTest
import android.view.KeyEvent
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
@@ -30,6 +33,7 @@
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
/**
@@ -55,12 +59,16 @@
whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
}
- private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer {
+ private fun createNoteTaskInitializer(
+ isEnabled: Boolean = true,
+ optionalKeyguardManager: Optional<KeyguardManager> = Optional.empty(),
+ ): NoteTaskInitializer {
return NoteTaskInitializer(
optionalBubbles = optionalBubbles,
noteTaskController = noteTaskController,
commandQueue = commandQueue,
isEnabled = isEnabled,
+ optionalKeyguardManager = optionalKeyguardManager,
)
}
@@ -105,19 +113,44 @@
// region handleSystemKey
@Test
- fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() {
- createNoteTaskInitializer()
+ fun handleSystemKey_receiveValidSystemKey_keyguardNotLocked_shouldShowNoteTaskWithUnlocked() {
+ val keyguardManager =
+ mock<KeyguardManager>() { whenever(isKeyguardLocked).thenReturn(false) }
+ createNoteTaskInitializer(optionalKeyguardManager = Optional.of(keyguardManager))
.callbacks
.handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
- verify(noteTaskController).showNoteTask()
+ verify(noteTaskController)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
+ }
+
+ @Test
+ fun handleSystemKey_receiveValidSystemKey_keyguardLocked_shouldShowNoteTaskWithLocked() {
+ val keyguardManager =
+ mock<KeyguardManager>() { whenever(isKeyguardLocked).thenReturn(true) }
+ createNoteTaskInitializer(optionalKeyguardManager = Optional.of(keyguardManager))
+ .callbacks
+ .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
+
+ verify(noteTaskController)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED)
+ }
+
+ @Test
+ fun handleSystemKey_receiveValidSystemKey_nullKeyguardManager_shouldShowNoteTaskWithUnlocked() {
+ createNoteTaskInitializer(optionalKeyguardManager = Optional.empty())
+ .callbacks
+ .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
+
+ verify(noteTaskController)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
}
@Test
fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
- verify(noteTaskController, never()).showNoteTask()
+ verifyZeroInteractions(noteTaskController)
}
// endregion
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
deleted file mode 100644
index 18be92b..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
+++ /dev/null
@@ -1,83 +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.notetask
-
-import android.app.role.RoleManager
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.test.suitebuilder.annotation.SmallTest
-import androidx.test.runner.AndroidJUnit4
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
-import com.android.systemui.util.mockito.eq
-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.Mock
-import org.mockito.Mockito.any
-import org.mockito.MockitoAnnotations
-
-/**
- * Tests for [NoteTaskIntentResolver].
- *
- * Build/Install/Run:
- * - atest SystemUITests:NoteTaskIntentResolverTest
- */
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-internal class NoteTaskIntentResolverTest : SysuiTestCase() {
-
- @Mock lateinit var packageManager: PackageManager
- @Mock lateinit var roleManager: RoleManager
-
- private lateinit var underTest: NoteTaskIntentResolver
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- underTest = NoteTaskIntentResolver(context, roleManager)
- }
-
- @Test
- fun resolveIntent_shouldReturnIntentInStylusMode() {
- val packageName = "com.android.note.app"
- whenever(roleManager.getRoleHoldersAsUser(NoteTaskIntentResolver.ROLE_NOTES, context.user))
- .then { listOf(packageName) }
-
- val actual = underTest.resolveIntent()
-
- requireNotNull(actual) { "Intent must not be null" }
- assertThat(actual.action).isEqualTo(ACTION_CREATE_NOTE)
- assertThat(actual.`package`).isEqualTo(packageName)
- val expectedExtra = actual.getExtra(NoteTaskIntentResolver.INTENT_EXTRA_USE_STYLUS_MODE)
- assertThat(expectedExtra).isEqualTo(true)
- val expectedFlag = actual.flags and Intent.FLAG_ACTIVITY_NEW_TASK
- assertThat(expectedFlag).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
-
- @Test
- fun resolveIntent_noRoleHolderIsSet_shouldReturnNull() {
- whenever(roleManager.getRoleHoldersAsUser(eq(NoteTaskIntentResolver.ROLE_NOTES), any()))
- .then { listOf<String>() }
-
- val actual = underTest.resolveIntent()
-
- assertThat(actual).isNull()
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
index a1d42a0..cdc683f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
@@ -27,7 +27,7 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState
import com.android.systemui.notetask.NoteTaskController
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
@@ -53,7 +53,6 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- whenever(noteTaskController.showNoteTask()).then {}
}
private fun createUnderTest(isEnabled: Boolean) =
@@ -96,6 +95,7 @@
underTest.onTriggered(expandable = null)
- verify(noteTaskController).showNoteTask()
+ verify(noteTaskController)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
index 80c39cf..addca9d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
@@ -37,10 +37,12 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
import com.android.systemui.statusbar.connectivity.AccessPointController;
import com.android.systemui.statusbar.connectivity.IconState;
import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.WifiIndicators;
import org.junit.Before;
import org.junit.Test;
@@ -135,4 +137,24 @@
assertThat(mTile.getState().secondaryLabel)
.isNotEqualTo(mContext.getString(R.string.status_bar_airplane));
}
+
+ @Test
+ public void setIsAirplaneMode_APM_enabled_after_wifi_disconnected() {
+ WifiIndicators wifiIndicators = new WifiIndicators(
+ /* enabled= */ true,
+ /* statusIcon= */ null,
+ /* qsIcon= */ null,
+ /* activityIn= */ false,
+ /* activityOut= */ false,
+ /* description= */ null,
+ /* isTransient= */ false,
+ /* statusLabel= */ null
+ );
+ mTile.mSignalCallback.setWifiIndicators(wifiIndicators);
+ IconState state = new IconState(true, 0, "");
+ mTile.mSignalCallback.setIsAirplaneMode(state);
+ mTestableLooper.processAllMessages();
+ assertThat(mTile.getState().icon).isEqualTo(
+ QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_no_internet_unavailable));
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
index 69f3e987..33aaa3f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
@@ -16,13 +16,15 @@
package com.android.systemui.screenrecord;
+import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
-
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import android.app.Dialog;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Looper;
@@ -31,7 +33,13 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
+import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.util.concurrency.FakeExecutor;
@@ -61,8 +69,15 @@
@Mock
private UserContextProvider mUserContextProvider;
@Mock
+ private ScreenCaptureDevicePolicyResolver mDevicePolicyResolver;
+ @Mock
+ private DialogLaunchAnimator mDialogLaunchAnimator;
+ @Mock
+ private ActivityStarter mActivityStarter;
+ @Mock
private UserTracker mUserTracker;
+ private FakeFeatureFlags mFeatureFlags;
private RecordingController mController;
private static final int USER_ID = 10;
@@ -70,8 +85,9 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mController = new RecordingController(mMainExecutor, mBroadcastDispatcher,
- mUserContextProvider, mUserTracker);
+ mFeatureFlags = new FakeFeatureFlags();
+ mController = new RecordingController(mMainExecutor, mBroadcastDispatcher, mContext,
+ mFeatureFlags, mUserContextProvider, () -> mDevicePolicyResolver, mUserTracker);
mController.addCallback(mCallback);
}
@@ -190,4 +206,67 @@
verify(mCallback).onRecordingEnd();
assertFalse(mController.isRecording());
}
+
+ @Test
+ public void testPoliciesFlagDisabled_screenCapturingNotAllowed_returnsNullDevicePolicyDialog() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, false);
+ when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(true);
+
+ Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+ mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+ assertThat(dialog).isInstanceOf(ScreenRecordPermissionDialog.class);
+ }
+
+ @Test
+ public void testPartialScreenSharingDisabled_returnsLegacyDialog() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, false);
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, false);
+
+ Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+ mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+ assertThat(dialog).isInstanceOf(ScreenRecordDialog.class);
+ }
+
+ @Test
+ public void testPoliciesFlagEnabled_screenCapturingNotAllowed_returnsDevicePolicyDialog() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, true);
+ when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(true);
+
+ Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+ mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+ assertThat(dialog).isInstanceOf(ScreenCaptureDisabledDialog.class);
+ }
+
+ @Test
+ public void testPoliciesFlagEnabled_screenCapturingAllowed_returnsNullDevicePolicyDialog() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, true);
+ when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(false);
+
+ Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+ mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+ assertThat(dialog).isInstanceOf(ScreenRecordPermissionDialog.class);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
index 0aa3621..5b094c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.screenrecord
+import android.os.UserHandle
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
@@ -59,6 +60,7 @@
dialog =
ScreenRecordPermissionDialog(
context,
+ UserHandle.of(0),
controller,
starter,
dialogLaunchAnimator,
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 541d6c2..2e73c0b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
@@ -23,7 +23,7 @@
import android.graphics.Rect
import android.hardware.HardwareBuffer
import android.os.UserHandle
-import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
+import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER
import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER
import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
@@ -35,6 +35,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
+import org.junit.Assert
import org.junit.Test
private const val USER_ID = 1
@@ -55,7 +56,7 @@
flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
val request =
- ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build()
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
var result: ScreenshotRequest? = null
@@ -78,8 +79,10 @@
fun testProcessAsync_ScreenshotData() {
flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
- val request = ScreenshotData.fromRequest(
- ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build())
+ val request =
+ ScreenshotData.fromRequest(
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
+ )
val processor = RequestProcessor(imageCapture, policy, flags, scope)
var result: ScreenshotData? = null
@@ -102,7 +105,7 @@
flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
val request =
- ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build()
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val processedRequest = processor.process(request)
@@ -162,7 +165,7 @@
)
val request =
- ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build()
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val processedRequest = processor.process(request)
@@ -191,6 +194,32 @@
}
@Test
+ fun testFullScreenshot_managedProfile_nullBitmap() {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+ // Provide a null task bitmap when asked
+ imageCapture.image = null
+
+ // Indicate that the primary content belongs to a manged profile
+ policy.setManagedProfile(USER_ID, true)
+ policy.setDisplayContentInfo(
+ policy.getDefaultDisplayId(),
+ DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)
+ )
+
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+ Assert.assertThrows(IllegalStateException::class.java) {
+ runBlocking { processor.process(request) }
+ }
+ Assert.assertThrows(IllegalStateException::class.java) {
+ runBlocking { processor.process(ScreenshotData.fromRequest(request)) }
+ }
+ }
+
+ @Test
fun testProvidedImageScreenshot_workProfilePolicyDisabled() = runBlocking {
flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
index 74969d0..1fa2ace 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
@@ -38,8 +38,9 @@
import com.android.internal.util.ScreenshotRequest
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY
import com.android.systemui.flags.Flags.SCREENSHOT_METADATA
+import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_OVERVIEW
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
@@ -47,6 +48,7 @@
import com.android.systemui.util.mockito.argThat
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import java.util.function.Consumer
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -55,10 +57,10 @@
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doThrow
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when` as whenever
private const val USER_ID = 1
private const val TASK_ID = 11
@@ -107,18 +109,20 @@
// Stub request processor as a synchronous no-op for tests with the flag enabled
doAnswer {
- val request: ScreenshotRequest = it.getArgument(0) as ScreenshotRequest
- val consumer: Consumer<ScreenshotRequest> = it.getArgument(1)
- consumer.accept(request)
- }.`when`(requestProcessor).processAsync(
- /* request= */ any(ScreenshotRequest::class.java), /* callback= */ any())
+ val request: ScreenshotRequest = it.getArgument(0) as ScreenshotRequest
+ val consumer: Consumer<ScreenshotRequest> = it.getArgument(1)
+ consumer.accept(request)
+ }
+ .whenever(requestProcessor)
+ .processAsync(/* request= */ any(ScreenshotRequest::class.java), /* callback= */ any())
doAnswer {
- val request: ScreenshotData = it.getArgument(0) as ScreenshotData
- val consumer: Consumer<ScreenshotData> = it.getArgument(1)
- consumer.accept(request)
- }.`when`(requestProcessor).processAsync(
- /* screenshot= */ any(ScreenshotData::class.java), /* callback= */ any())
+ val request: ScreenshotData = it.getArgument(0) as ScreenshotData
+ val consumer: Consumer<ScreenshotData> = it.getArgument(1)
+ consumer.accept(request)
+ }
+ .whenever(requestProcessor)
+ .processAsync(/* screenshot= */ any(ScreenshotData::class.java), /* callback= */ any())
// Flipped in selected test cases
flags.set(SCREENSHOT_WORK_PROFILE_POLICY, false)
@@ -162,37 +166,52 @@
/* requestCallback = */ any()
)
- assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
+ assertEquals("Expected one UiEvent", 1, eventLogger.numLogs())
val logEvent = eventLogger.get(0)
- assertEquals("Expected SCREENSHOT_REQUESTED UiEvent",
- logEvent.eventId, SCREENSHOT_REQUESTED_KEY_OTHER.id)
- assertEquals("Expected supplied package name",
- topComponent.packageName, eventLogger.get(0).packageName)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED UiEvent",
+ logEvent.eventId,
+ SCREENSHOT_REQUESTED_KEY_OTHER.id
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ eventLogger.get(0).packageName
+ )
}
@Test
fun takeScreenshotFullscreen_screenshotDataEnabled() {
flags.set(SCREENSHOT_METADATA, true)
- val request = ScreenshotRequest.Builder(
- TAKE_SCREENSHOT_FULLSCREEN,
- SCREENSHOT_KEY_OTHER).setTopComponent(topComponent).build()
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
- service.handleRequest(request, { /* onSaved */ }, callback)
+ service.handleRequest(request, { /* onSaved */}, callback)
- verify(controller, times(1)).handleScreenshot(
- eq(ScreenshotData.fromRequest(request)),
- /* onSavedListener = */ any(),
- /* requestCallback = */ any())
+ verify(controller, times(1))
+ .handleScreenshot(
+ eq(ScreenshotData.fromRequest(request)),
+ /* onSavedListener = */ any(),
+ /* requestCallback = */ any()
+ )
assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
val logEvent = eventLogger.get(0)
- assertEquals("Expected SCREENSHOT_REQUESTED UiEvent",
- logEvent.eventId, SCREENSHOT_REQUESTED_KEY_OTHER.id)
- assertEquals("Expected supplied package name",
- topComponent.packageName, eventLogger.get(0).packageName)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED UiEvent",
+ logEvent.eventId,
+ SCREENSHOT_REQUESTED_KEY_OTHER.id
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ eventLogger.get(0).packageName
+ )
}
@Test
@@ -224,7 +243,7 @@
/* requestCallback = */ any()
)
- assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
+ assertEquals("Expected one UiEvent", 1, eventLogger.numLogs())
val logEvent = eventLogger.get(0)
assertEquals(
@@ -241,6 +260,8 @@
@Test
fun takeScreenshotFullscreen_userLocked() {
+ flags.set(SCREENSHOT_METADATA, true)
+
whenever(userManager.isUserUnlocked).thenReturn(false)
val request =
@@ -253,10 +274,36 @@
verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
verify(callback, times(1)).reportError()
verifyZeroInteractions(controller)
+
+ assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+ val requestEvent = eventLogger.get(0)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ SCREENSHOT_REQUESTED_KEY_OTHER.id,
+ requestEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ requestEvent.packageName
+ )
+ val failureEvent = eventLogger.get(1)
+ assertEquals(
+ "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+ SCREENSHOT_CAPTURE_FAILED.id,
+ failureEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ failureEvent.packageName
+ )
}
@Test
fun takeScreenshotFullscreen_screenCaptureDisabled_allUsers() {
+ flags.set(SCREENSHOT_METADATA, true)
+
whenever(devicePolicyManager.getScreenCaptureDisabled(isNull(), eq(UserHandle.USER_ALL)))
.thenReturn(true)
@@ -279,6 +326,206 @@
// error shown: Toast.makeText(...).show(), untestable
verify(callback, times(1)).reportError()
verifyZeroInteractions(controller)
+ assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+ val requestEvent = eventLogger.get(0)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ SCREENSHOT_REQUESTED_KEY_OTHER.id,
+ requestEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ requestEvent.packageName
+ )
+ val failureEvent = eventLogger.get(1)
+ assertEquals(
+ "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+ SCREENSHOT_CAPTURE_FAILED.id,
+ failureEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ failureEvent.packageName
+ )
+ }
+
+ @Test
+ fun takeScreenshotFullscreen_userLocked_metadataDisabled() {
+ flags.set(SCREENSHOT_METADATA, false)
+ whenever(userManager.isUserUnlocked).thenReturn(false)
+
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
+
+ service.handleRequest(request, { /* onSaved */}, callback)
+
+ verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
+ verify(callback, times(1)).reportError()
+ verifyZeroInteractions(controller)
+
+ assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+ val requestEvent = eventLogger.get(0)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ SCREENSHOT_REQUESTED_KEY_OTHER.id,
+ requestEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ requestEvent.packageName
+ )
+ val failureEvent = eventLogger.get(1)
+ assertEquals(
+ "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+ SCREENSHOT_CAPTURE_FAILED.id,
+ failureEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ failureEvent.packageName
+ )
+ }
+
+ @Test
+ fun takeScreenshotFullscreen_screenCaptureDisabled_allUsers_metadataDisabled() {
+ flags.set(SCREENSHOT_METADATA, false)
+
+ whenever(devicePolicyManager.getScreenCaptureDisabled(isNull(), eq(UserHandle.USER_ALL)))
+ .thenReturn(true)
+
+ whenever(
+ devicePolicyResourcesManager.getString(
+ eq(SCREENSHOT_BLOCKED_BY_ADMIN),
+ /* Supplier<String> */
+ any(),
+ )
+ )
+ .thenReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
+
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
+
+ service.handleRequest(request, { /* onSaved */}, callback)
+
+ // error shown: Toast.makeText(...).show(), untestable
+ verify(callback, times(1)).reportError()
+ verifyZeroInteractions(controller)
+ assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+ val requestEvent = eventLogger.get(0)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ SCREENSHOT_REQUESTED_KEY_OTHER.id,
+ requestEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ requestEvent.packageName
+ )
+ val failureEvent = eventLogger.get(1)
+ assertEquals(
+ "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+ SCREENSHOT_CAPTURE_FAILED.id,
+ failureEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ failureEvent.packageName
+ )
+ }
+
+ @Test
+ fun takeScreenshot_workProfile_nullBitmap_metadataDisabled() {
+ flags.set(SCREENSHOT_METADATA, false)
+
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
+
+ doThrow(IllegalStateException::class.java)
+ .whenever(requestProcessor)
+ .processAsync(any(ScreenshotRequest::class.java), any())
+
+ service.handleRequest(request, { /* onSaved */}, callback)
+
+ verify(callback, times(1)).reportError()
+ verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
+ verifyZeroInteractions(controller)
+ assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+ val requestEvent = eventLogger.get(0)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ SCREENSHOT_REQUESTED_KEY_OTHER.id,
+ requestEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ requestEvent.packageName
+ )
+ val failureEvent = eventLogger.get(1)
+ assertEquals(
+ "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+ SCREENSHOT_CAPTURE_FAILED.id,
+ failureEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ failureEvent.packageName
+ )
+ }
+ @Test
+ fun takeScreenshot_workProfile_nullBitmap() {
+ flags.set(SCREENSHOT_METADATA, true)
+
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
+
+ doThrow(IllegalStateException::class.java)
+ .whenever(requestProcessor)
+ .processAsync(any(ScreenshotData::class.java), any())
+
+ service.handleRequest(request, { /* onSaved */}, callback)
+
+ verify(callback, times(1)).reportError()
+ verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
+ verifyZeroInteractions(controller)
+ assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+ val requestEvent = eventLogger.get(0)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ SCREENSHOT_REQUESTED_KEY_OTHER.id,
+ requestEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ requestEvent.packageName
+ )
+ val failureEvent = eventLogger.get(1)
+ assertEquals(
+ "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+ SCREENSHOT_CAPTURE_FAILED.id,
+ failureEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ failureEvent.packageName
+ )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
index 576652f..3440f91 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
@@ -59,9 +59,7 @@
@RunWith(AndroidTestingRunner.class)
public class WorkProfileMessageControllerTest extends SysuiTestCase {
private static final String DEFAULT_LABEL = "default label";
- private static final String BADGED_DEFAULT_LABEL = "badged default label";
private static final String APP_LABEL = "app label";
- private static final String BADGED_APP_LABEL = "badged app label";
private static final UserHandle NON_WORK_USER = UserHandle.of(0);
private static final UserHandle WORK_USER = UserHandle.of(10);
@@ -91,10 +89,6 @@
eq(WorkProfileMessageController.SHARED_PREFERENCES_NAME),
eq(Context.MODE_PRIVATE))).thenReturn(mSharedPreferences);
when(mMockContext.getString(ArgumentMatchers.anyInt())).thenReturn(DEFAULT_LABEL);
- when(mPackageManager.getUserBadgedLabel(eq(DEFAULT_LABEL), any()))
- .thenReturn(BADGED_DEFAULT_LABEL);
- when(mPackageManager.getUserBadgedLabel(eq(APP_LABEL), any()))
- .thenReturn(BADGED_APP_LABEL);
when(mPackageManager.getActivityIcon(any(ComponentName.class)))
.thenReturn(mActivityIcon);
when(mPackageManager.getUserBadgedIcon(
@@ -133,7 +127,7 @@
WorkProfileMessageController.WorkProfileFirstRunData data =
mMessageController.onScreenshotTaken(WORK_USER);
- assertEquals(BADGED_DEFAULT_LABEL, data.getAppName());
+ assertEquals(DEFAULT_LABEL, data.getAppName());
assertNull(data.getIcon());
}
@@ -142,7 +136,7 @@
WorkProfileMessageController.WorkProfileFirstRunData data =
mMessageController.onScreenshotTaken(WORK_USER);
- assertEquals(BADGED_APP_LABEL, data.getAppName());
+ assertEquals(APP_LABEL, data.getAppName());
assertEquals(mBadgedActivityIcon, data.getIcon());
}
@@ -151,7 +145,7 @@
ViewGroup layout = (ViewGroup) LayoutInflater.from(mContext).inflate(
R.layout.screenshot_work_profile_first_run, null);
WorkProfileMessageController.WorkProfileFirstRunData data =
- new WorkProfileMessageController.WorkProfileFirstRunData(BADGED_APP_LABEL,
+ new WorkProfileMessageController.WorkProfileFirstRunData(APP_LABEL,
mBadgedActivityIcon);
final CountDownLatch countdown = new CountDownLatch(1);
mMessageController.populateView(layout, data, () -> {
@@ -163,7 +157,7 @@
assertEquals(mBadgedActivityIcon, image.getDrawable());
TextView text = layout.findViewById(R.id.screenshot_message_content);
// The app name is used in a template, but at least validate that it was inserted.
- assertTrue(text.getText().toString().contains(BADGED_APP_LABEL));
+ assertTrue(text.getText().toString().contains(APP_LABEL));
// Validate that clicking the dismiss button calls back properly.
assertEquals(1, countdown.getCount());
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 4d7741ad..78bebb9 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
@@ -27,6 +27,7 @@
import com.android.systemui.plugins.ClockId
import com.android.systemui.plugins.ClockMetadata
import com.android.systemui.plugins.ClockProviderPlugin
+import com.android.systemui.plugins.ClockSettings
import com.android.systemui.plugins.PluginListener
import com.android.systemui.plugins.PluginManager
import com.android.systemui.util.mockito.argumentCaptor
@@ -59,7 +60,7 @@
private lateinit var pluginListener: PluginListener<ClockProviderPlugin>
private lateinit var registry: ClockRegistry
- private var settingValue: String = ""
+ private var settingValue: ClockSettings? = null
companion object {
private fun failFactory(): ClockController {
@@ -79,7 +80,8 @@
private val thumbnailCallbacks = mutableMapOf<ClockId, () -> Drawable?>()
override fun getClocks() = metadata
- override fun createClock(id: ClockId): ClockController = createCallbacks[id]!!()
+ override fun createClock(settings: ClockSettings): ClockController =
+ createCallbacks[settings.clockId!!]!!()
override fun getClockThumbnail(id: ClockId): Drawable? = thumbnailCallbacks[id]!!()
fun addClock(
@@ -110,7 +112,7 @@
userHandle = UserHandle.USER_ALL,
defaultClockProvider = fakeDefaultProvider
) {
- override var currentClockId: ClockId
+ override var settings: ClockSettings?
get() = settingValue
set(value) { settingValue = value }
}
@@ -185,7 +187,7 @@
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
- settingValue = "clock_3"
+ settingValue = ClockSettings("clock_3", null, null)
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3", { mockClock })
.addClock("clock_4", "clock 4")
@@ -203,7 +205,7 @@
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
- settingValue = "clock_3"
+ settingValue = ClockSettings("clock_3", null, null)
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3")
.addClock("clock_4", "clock 4")
@@ -222,7 +224,7 @@
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
- settingValue = "clock_3"
+ settingValue = ClockSettings("clock_3", null, null)
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3", { mockClock })
.addClock("clock_4", "clock 4")
@@ -242,8 +244,8 @@
@Test
fun jsonDeserialization_gotExpectedObject() {
- val expected = ClockRegistry.ClockSetting("ID", 500)
- val actual = ClockRegistry.ClockSetting.deserialize("""{
+ val expected = ClockSettings("ID", null, 500)
+ val actual = ClockSettings.deserialize("""{
"clockId":"ID",
"_applied_timestamp":500
}""")
@@ -252,15 +254,15 @@
@Test
fun jsonDeserialization_noTimestamp_gotExpectedObject() {
- val expected = ClockRegistry.ClockSetting("ID", null)
- val actual = ClockRegistry.ClockSetting.deserialize("{\"clockId\":\"ID\"}")
+ val expected = ClockSettings("ID", null, null)
+ val actual = ClockSettings.deserialize("{\"clockId\":\"ID\"}")
assertEquals(expected, actual)
}
@Test
fun jsonDeserialization_nullTimestamp_gotExpectedObject() {
- val expected = ClockRegistry.ClockSetting("ID", null)
- val actual = ClockRegistry.ClockSetting.deserialize("""{
+ val expected = ClockSettings("ID", null, null)
+ val actual = ClockSettings.deserialize("""{
"clockId":"ID",
"_applied_timestamp":null
}""")
@@ -269,22 +271,22 @@
@Test(expected = JSONException::class)
fun jsonDeserialization_noId_threwException() {
- val expected = ClockRegistry.ClockSetting("ID", 500)
- val actual = ClockRegistry.ClockSetting.deserialize("{\"_applied_timestamp\":500}")
+ val expected = ClockSettings("ID", null, 500)
+ val actual = ClockSettings.deserialize("{\"_applied_timestamp\":500}")
assertEquals(expected, actual)
}
@Test
fun jsonSerialization_gotExpectedString() {
val expected = "{\"clockId\":\"ID\",\"_applied_timestamp\":500}"
- val actual = ClockRegistry.ClockSetting.serialize( ClockRegistry.ClockSetting("ID", 500))
+ val actual = ClockSettings.serialize(ClockSettings("ID", null, 500))
assertEquals(expected, actual)
}
@Test
fun jsonSerialization_noTimestamp_gotExpectedString() {
val expected = "{\"clockId\":\"ID\"}"
- val actual = ClockRegistry.ClockSetting.serialize( ClockRegistry.ClockSetting("ID", null))
+ val actual = ClockSettings.serialize(ClockSettings("ID", null, null))
assertEquals(expected, actual)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
index a7588dd..cd2efc0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
@@ -114,7 +114,8 @@
@Test
fun defaultClock_events_onTimeTick() {
val clock = provider.createClock(DEFAULT_CLOCK_ID)
- clock.events.onTimeTick()
+ clock.smallClock.events.onTimeTick()
+ clock.largeClock.events.onTimeTick()
verify(mockSmallClockView).refreshTime()
verify(mockLargeClockView).refreshTime()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index dffa566..f9f2c45 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -620,6 +620,82 @@
}
@Test
+ public void onBiometricHelp_coEx_fpFailure_faceAlreadyUnlocked() {
+ createController();
+
+ // GIVEN face has already unlocked the device
+ when(mKeyguardUpdateMonitor.getUserUnlockedWithFace(anyInt())).thenReturn(true);
+
+ String message = "A message";
+ mController.setVisible(true);
+
+ // WHEN there's a fingerprint not recognized message
+ mController.getKeyguardCallback().onBiometricHelp(
+ BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
+ message,
+ BiometricSourceType.FINGERPRINT);
+
+ // THEN show sequential messages such as: 'Unlocked by face' and
+ // 'Swipe up to open'
+ verifyIndicationMessage(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE,
+ mContext.getString(R.string.keyguard_face_successful_unlock));
+ verifyIndicationMessage(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+ mContext.getString(R.string.keyguard_unlock));
+ }
+
+ @Test
+ public void onBiometricHelp_coEx_fpFailure_trustAgentAlreadyUnlocked() {
+ createController();
+
+ // GIVEN trust agent has already unlocked the device
+ when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);
+
+ String message = "A message";
+ mController.setVisible(true);
+
+ // WHEN there's a fingerprint not recognized message
+ mController.getKeyguardCallback().onBiometricHelp(
+ BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
+ message,
+ BiometricSourceType.FINGERPRINT);
+
+ // THEN show sequential messages such as: 'Kept unlocked by TrustAgent' and
+ // 'Swipe up to open'
+ verifyIndicationMessage(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE,
+ mContext.getString(R.string.keyguard_indication_trust_unlocked));
+ verifyIndicationMessage(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+ mContext.getString(R.string.keyguard_unlock));
+ }
+
+ @Test
+ public void onBiometricHelp_coEx_fpFailure_trustAgentUnlocked_emptyTrustGrantedMessage() {
+ createController();
+
+ // GIVEN trust agent has already unlocked the device & trust granted message is empty
+ when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);
+ mController.showTrustGrantedMessage(false, "");
+
+ String message = "A message";
+ mController.setVisible(true);
+
+ // WHEN there's a fingerprint not recognized message
+ mController.getKeyguardCallback().onBiometricHelp(
+ BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
+ message,
+ BiometricSourceType.FINGERPRINT);
+
+ // THEN show action to unlock (ie: 'Swipe up to open')
+ verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE);
+ verifyIndicationMessage(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+ mContext.getString(R.string.keyguard_unlock));
+ }
+
+ @Test
public void transientIndication_visibleWhenDozing_unlessSwipeUp_fromError() {
createController();
String message = mContext.getString(R.string.keyguard_unlock);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 2423f13..9c69a6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -32,6 +32,7 @@
import android.view.View
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
@@ -104,6 +105,9 @@
private lateinit var keyguardBypassController: KeyguardBypassController
@Mock
+ private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+
+ @Mock
private lateinit var deviceProvisionedController: DeviceProvisionedController
@Mock
@@ -113,6 +117,9 @@
private lateinit var handler: Handler
@Mock
+ private lateinit var datePlugin: BcSmartspaceDataPlugin
+
+ @Mock
private lateinit var weatherPlugin: BcSmartspaceDataPlugin
@Mock
@@ -155,6 +162,7 @@
KeyguardBypassController.OnBypassStateChangedListener
private lateinit var deviceProvisionedListener: DeviceProvisionedListener
+ private lateinit var dateSmartspaceView: SmartspaceView
private lateinit var weatherSmartspaceView: SmartspaceView
private lateinit var smartspaceView: SmartspaceView
@@ -190,6 +198,8 @@
`when`(secureSettings.getUriFor(NOTIF_ON_LOCKSCREEN_SETTING))
.thenReturn(fakeNotifOnLockscreenSettingUri)
`when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(smartspaceSession)
+ `when`(datePlugin.getView(any())).thenReturn(
+ createDateSmartspaceView(), createDateSmartspaceView())
`when`(weatherPlugin.getView(any())).thenReturn(
createWeatherSmartspaceView(), createWeatherSmartspaceView())
`when`(plugin.getView(any())).thenReturn(createSmartspaceView(), createSmartspaceView())
@@ -217,10 +227,12 @@
statusBarStateController,
deviceProvisionedController,
keyguardBypassController,
+ keyguardUpdateMonitor,
execution,
executor,
bgExecutor,
handler,
+ Optional.of(datePlugin),
Optional.of(weatherPlugin),
Optional.of(plugin),
Optional.of(configPlugin),
@@ -275,7 +287,8 @@
// THEN the listener is registered to the underlying plugin
verify(plugin).registerListener(controllerListener)
- // The listener is registered only for the plugin, not the weather plugin.
+ // The listener is registered only for the plugin, not the date, or weather plugin.
+ verify(datePlugin, never()).registerListener(any())
verify(weatherPlugin, never()).registerListener(any())
}
@@ -289,7 +302,8 @@
// THEN the listener is subsequently registered
verify(plugin).registerListener(controllerListener)
- // The listener is registered only for the plugin, not the weather plugin.
+ // The listener is registered only for the plugin, not the date, or the weather plugin.
+ verify(datePlugin, never()).registerListener(any())
verify(weatherPlugin, never()).registerListener(any())
}
@@ -308,6 +322,7 @@
verify(plugin).registerSmartspaceEventNotifier(null)
verify(weatherPlugin).onTargetsAvailable(emptyList())
verify(weatherPlugin).registerSmartspaceEventNotifier(null)
+ verify(datePlugin).registerSmartspaceEventNotifier(null)
}
@Test
@@ -357,6 +372,7 @@
configChangeListener.onThemeChanged()
// We update the new text color to match the wallpaper color
+ verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
verify(smartspaceView).setPrimaryTextColor(anyInt())
}
@@ -384,6 +400,7 @@
statusBarStateListener.onDozeAmountChanged(0.1f, 0.7f)
// We pass that along to the view
+ verify(dateSmartspaceView).setDozeAmount(0.7f)
verify(weatherSmartspaceView).setDozeAmount(0.7f)
verify(smartspaceView).setDozeAmount(0.7f)
}
@@ -502,6 +519,8 @@
verify(plugin).onTargetsAvailable(eq(listOf(targets[0], targets[1], targets[2])))
// No filtering is applied for the weather plugin
verify(weatherPlugin).onTargetsAvailable(eq(targets))
+ // No targets needed for the date plugin
+ verify(datePlugin, never()).onTargetsAvailable(any())
}
@Test
@@ -633,6 +652,18 @@
private fun connectSession() {
if (controller.isDateWeatherDecoupled()) {
+ val dateView = controller.buildAndConnectDateView(fakeParent)
+ dateSmartspaceView = dateView as SmartspaceView
+ fakeParent.addView(dateView)
+ controller.stateChangeListener.onViewAttachedToWindow(dateView)
+
+ verify(dateSmartspaceView).setUiSurface(
+ BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
+ verify(dateSmartspaceView).registerDataProvider(datePlugin)
+
+ verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
+ verify(dateSmartspaceView).setDozeAmount(0.5f)
+
val weatherView = controller.buildAndConnectWeatherView(fakeParent)
weatherSmartspaceView = weatherView as SmartspaceView
fakeParent.addView(weatherView)
@@ -686,6 +717,7 @@
verify(smartspaceView).setDozeAmount(0.5f)
if (controller.isDateWeatherDecoupled()) {
+ clearInvocations(dateSmartspaceView)
clearInvocations(weatherSmartspaceView)
}
clearInvocations(smartspaceView)
@@ -734,7 +766,38 @@
).thenReturn(if (value) 1 else 0)
}
- // Separate function for the weather view, which doesn't implement all functions in interface.
+ // Separate function for the date view, which implements a specific subset of all functions.
+ private fun createDateSmartspaceView(): SmartspaceView {
+ return spy(object : View(context), SmartspaceView {
+ override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
+ }
+
+ override fun setPrimaryTextColor(color: Int) {
+ }
+
+ override fun setIsDreaming(isDreaming: Boolean) {
+ }
+
+ override fun setUiSurface(uiSurface: String) {
+ }
+
+ override fun setDozeAmount(amount: Float) {
+ }
+
+ override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {
+ }
+
+ override fun setFalsingManager(falsingManager: FalsingManager?) {
+ }
+
+ override fun setDnd(image: Drawable?, description: String?) {
+ }
+
+ override fun setNextAlarm(image: Drawable?, description: String?) {
+ }
+ })
+ }
+ // Separate function for the weather view, which implements a specific subset of all functions.
private fun createWeatherSmartspaceView(): SmartspaceView {
return spy(object : View(context), SmartspaceView {
override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
index 33b94e3..bd03903 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
@@ -32,6 +32,7 @@
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import java.lang.RuntimeException
import kotlinx.coroutines.Dispatchers
import org.junit.Before
import org.junit.Test
@@ -113,6 +114,24 @@
assertThat(data).hasSize(2)
}
+ @Test
+ fun onPullAtom_throwsInterruptedException_failsGracefully() {
+ val pipeline: NotifPipeline = mock()
+ whenever(pipeline.allNotifs).thenAnswer { throw InterruptedException("Timeout") }
+ val logger = NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
+ assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, mutableListOf()))
+ .isEqualTo(StatsManager.PULL_SKIP)
+ }
+
+ @Test
+ fun onPullAtom_throwsRuntimeException_failsGracefully() {
+ val pipeline: NotifPipeline = mock()
+ whenever(pipeline.allNotifs).thenThrow(RuntimeException("Something broke!"))
+ val logger = NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
+ assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, mutableListOf()))
+ .isEqualTo(StatsManager.PULL_SKIP)
+ }
+
private fun createLoggerWithNotifications(
notifications: List<Notification>
): NotificationMemoryLogger {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
index ed3f710..7e69efa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
@@ -264,6 +264,19 @@
}
@Test
+ public void notifPositionAlignedWithClockAndBurnInOffsetInSplitShadeMode() {
+ setSplitShadeTopMargin(100); // this makes clock to be at 100
+ givenAOD();
+ mIsSplitShade = true;
+ givenMaxBurnInOffset(100);
+ givenHighestBurnInOffset(); // this makes clock to be at 200
+ // WHEN the position algorithm is run
+ positionClock();
+ // THEN the notif padding adjusts for burn-in offset: clock position - burn-in offset
+ assertThat(mClockPosition.stackScrollerPadding).isEqualTo(100);
+ }
+
+ @Test
public void clockPositionedDependingOnMarginInSplitShade() {
setSplitShadeTopMargin(400);
givenLockScreen();
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 f230b87..07e8d3c 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
@@ -23,10 +23,12 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -45,6 +47,7 @@
import androidx.test.filters.SmallTest;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.R;
import com.android.systemui.SysuiBaseFragmentTest;
import com.android.systemui.dump.DumpManager;
@@ -67,6 +70,8 @@
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
import com.android.systemui.util.CarrierConfigTracker;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.settings.SecureSettings;
@@ -79,6 +84,9 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
+import java.util.List;
+
@RunWith(AndroidTestingRunner.class)
@RunWithLooper(setAsMainLooper = true)
@SmallTest
@@ -118,6 +126,12 @@
private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
@Mock
private DumpManager mDumpManager;
+ @Mock
+ private StatusBarWindowStateController mStatusBarWindowStateController;
+ @Mock
+ private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+
+ private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>();
public CollapsedStatusBarFragmentTest() {
super(CollapsedStatusBarFragment.class);
@@ -127,6 +141,14 @@
public void setup() {
injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
mDependency.injectMockDependency(DarkIconDispatcher.class);
+
+ // Keep the window state listeners so we can dispatch to them to test the status bar
+ // fragment's response.
+ doAnswer(invocation -> {
+ mStatusBarWindowStateListeners.add(invocation.getArgument(0));
+ return null;
+ }).when(mStatusBarWindowStateController).addListener(
+ any(StatusBarWindowStateListener.class));
}
@Test
@@ -414,6 +436,27 @@
assertFalse(contains);
}
+ @Test
+ public void testStatusBarIcons_hiddenThroughoutCameraLaunch() {
+ final CollapsedStatusBarFragment fragment = resumeAndGetFragment();
+
+ mockSecureCameraLaunch(fragment, true /* launched */);
+
+ // Status icons should be invisible or gone, but certainly not VISIBLE.
+ assertNotEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+ assertNotEquals(View.VISIBLE, getClockView().getVisibility());
+
+ mockSecureCameraLaunchFinished();
+
+ assertNotEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+ assertNotEquals(View.VISIBLE, getClockView().getVisibility());
+
+ mockSecureCameraLaunch(fragment, false /* launched */);
+
+ assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+ assertEquals(View.VISIBLE, getClockView().getVisibility());
+ }
+
@Override
protected Fragment instantiate(Context context, String className, Bundle arguments) {
MockitoAnnotations.initMocks(this);
@@ -455,7 +498,9 @@
mOperatorNameViewControllerFactory,
mSecureSettings,
mExecutor,
- mDumpManager);
+ mDumpManager,
+ mStatusBarWindowStateController,
+ mKeyguardUpdateMonitor);
}
private void setUpDaggerComponent() {
@@ -478,6 +523,35 @@
mNotificationAreaInner);
}
+ /**
+ * Configure mocks to return values consistent with the secure camera animating itself launched
+ * over the keyguard.
+ */
+ private void mockSecureCameraLaunch(CollapsedStatusBarFragment fragment, boolean launched) {
+ when(mKeyguardUpdateMonitor.isSecureCameraLaunchedOverKeyguard()).thenReturn(launched);
+ when(mKeyguardStateController.isOccluded()).thenReturn(launched);
+
+ if (launched) {
+ fragment.onCameraLaunchGestureDetected(0 /* source */);
+ } else {
+ for (StatusBarWindowStateListener listener : mStatusBarWindowStateListeners) {
+ listener.onStatusBarWindowStateChanged(StatusBarManager.WINDOW_STATE_SHOWING);
+ }
+ }
+
+ fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
+ }
+
+ /**
+ * Configure mocks to return values consistent with the secure camera showing over the keyguard
+ * with its launch animation finished.
+ */
+ private void mockSecureCameraLaunchFinished() {
+ for (StatusBarWindowStateListener listener : mStatusBarWindowStateListeners) {
+ listener.onStatusBarWindowStateChanged(StatusBarManager.WINDOW_STATE_HIDDEN);
+ }
+ }
+
private CollapsedStatusBarFragment resumeAndGetFragment() {
mFragments.dispatchResume();
processAllMessages();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
index f822ba0..45189cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
@@ -19,7 +19,8 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.table.TableRowLogger
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION_IN
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION_OUT
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CARRIER_NETWORK_CHANGE
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CDMA_LEVEL
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CONNECTION_STATE
@@ -54,7 +55,19 @@
assertThat(logger.changes)
.contains(Pair(COL_CONNECTION_STATE, connection.dataConnectionState.toString()))
assertThat(logger.changes)
- .contains(Pair(COL_ACTIVITY_DIRECTION, connection.dataActivityDirection.toString()))
+ .contains(
+ Pair(
+ COL_ACTIVITY_DIRECTION_IN,
+ connection.dataActivityDirection.hasActivityIn.toString(),
+ )
+ )
+ assertThat(logger.changes)
+ .contains(
+ Pair(
+ COL_ACTIVITY_DIRECTION_OUT,
+ connection.dataActivityDirection.hasActivityOut.toString(),
+ )
+ )
assertThat(logger.changes)
.contains(
Pair(COL_CARRIER_NETWORK_CHANGE, connection.carrierNetworkChangeActive.toString())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index d6b8c0d..314e250 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -41,6 +41,8 @@
import android.telephony.TelephonyManager.DATA_CONNECTING
import android.telephony.TelephonyManager.DATA_DISCONNECTED
import android.telephony.TelephonyManager.DATA_DISCONNECTING
+import android.telephony.TelephonyManager.DATA_HANDOVER_IN_PROGRESS
+import android.telephony.TelephonyManager.DATA_SUSPENDED
import android.telephony.TelephonyManager.DATA_UNKNOWN
import android.telephony.TelephonyManager.ERI_OFF
import android.telephony.TelephonyManager.ERI_ON
@@ -255,6 +257,37 @@
}
@Test
+ fun testFlowForSubId_dataConnectionState_suspended() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ val callback =
+ getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(DATA_SUSPENDED, 200 /* unused */)
+
+ assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Suspended)
+
+ job.cancel()
+ }
+
+ @Test
+ fun testFlowForSubId_dataConnectionState_handoverInProgress() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ val callback =
+ getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(DATA_HANDOVER_IN_PROGRESS, 200 /* unused */)
+
+ assertThat(latest?.dataConnectionState)
+ .isEqualTo(DataConnectionState.HandoverInProgress)
+
+ job.cancel()
+ }
+
+ @Test
fun testFlowForSubId_dataConnectionState_unknown() =
runBlocking(IMMEDIATE) {
var latest: MobileConnectionModel? = null
@@ -270,6 +303,21 @@
}
@Test
+ fun testFlowForSubId_dataConnectionState_invalid() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ val callback =
+ getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(45, 200 /* unused */)
+
+ assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Invalid)
+
+ job.cancel()
+ }
+
+ @Test
fun testFlowForSubId_dataActivity() =
runBlocking(IMMEDIATE) {
var latest: MobileConnectionModel? = null
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 1b62d5c..bd24922 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
@@ -28,6 +28,7 @@
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.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -77,6 +78,7 @@
MobileIconsInteractorImpl(
connectionsRepository,
carrierConfigTracker,
+ logger = mock(),
userSetupRepository,
testScope.backgroundScope,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index a24e29ae..b91a4df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -17,6 +17,8 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
import androidx.test.filters.SmallTest
+import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH
+import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH_NONE
import com.android.settingslib.graph.SignalDrawable
import com.android.settingslib.mobile.TelephonyIcons.THREE_G
import com.android.systemui.SysuiTestCase
@@ -122,6 +124,39 @@
}
@Test
+ fun contentDescription_notInService_usesNoPhone() =
+ testScope.runTest {
+ var latest: ContentDescription? = null
+ val job = underTest.contentDescription.onEach { latest = it }.launchIn(this)
+
+ interactor.isInService.value = false
+
+ assertThat((latest as ContentDescription.Resource).res)
+ .isEqualTo(PHONE_SIGNAL_STRENGTH_NONE)
+
+ job.cancel()
+ }
+
+ @Test
+ fun contentDescription_inService_usesLevel() =
+ testScope.runTest {
+ var latest: ContentDescription? = null
+ val job = underTest.contentDescription.onEach { latest = it }.launchIn(this)
+
+ interactor.isInService.value = true
+
+ interactor.level.value = 2
+ assertThat((latest as ContentDescription.Resource).res)
+ .isEqualTo(PHONE_SIGNAL_STRENGTH[2])
+
+ interactor.level.value = 0
+ assertThat((latest as ContentDescription.Resource).res)
+ .isEqualTo(PHONE_SIGNAL_STRENGTH[0])
+
+ job.cancel()
+ }
+
+ @Test
fun networkType_dataEnabled_groupIsRepresented() =
testScope.runTest {
val expected =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt
index 3fe6983..e4c8fd0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.pipeline.shared.ui.view
+import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
@@ -118,6 +119,22 @@
assertThat(view.isIconVisible).isEqualTo(false)
}
+ @Test
+ fun getDrawingRect_takesTranslationIntoAccount() {
+ val view = createAndInitView()
+
+ view.translationX = 50f
+ view.translationY = 60f
+
+ val drawingRect = Rect()
+ view.getDrawingRect(drawingRect)
+
+ assertThat(drawingRect.left).isEqualTo(view.left + 50)
+ assertThat(drawingRect.right).isEqualTo(view.right + 50)
+ assertThat(drawingRect.top).isEqualTo(view.top + 60)
+ assertThat(drawingRect.bottom).isEqualTo(view.bottom + 60)
+ }
+
private fun createAndInitView(): ModernStatusBarView {
val view = ModernStatusBarView(context, null)
binding = TestBinding()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index dd04ac4..fc7436a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -79,6 +79,7 @@
@Mock private lateinit var viewUtil: ViewUtil
@Mock private lateinit var vibratorHelper: VibratorHelper
@Mock private lateinit var swipeGestureHandler: SwipeChipbarAwayGestureHandler
+ private lateinit var chipbarAnimator: TestChipbarAnimator
private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
private lateinit var fakeWakeLock: WakeLockFake
private lateinit var fakeClock: FakeSystemClock
@@ -98,6 +99,7 @@
fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
uiEventLoggerFake = UiEventLoggerFake()
+ chipbarAnimator = TestChipbarAnimator()
underTest =
ChipbarCoordinator(
@@ -109,6 +111,7 @@
configurationController,
dumpManager,
powerManager,
+ chipbarAnimator,
falsingManager,
falsingCollector,
swipeGestureHandler,
@@ -371,6 +374,26 @@
verify(vibratorHelper).vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK))
}
+ /** Regression test for b/266119467. */
+ @Test
+ fun displayView_animationFailure_viewsStillBecomeVisible() {
+ chipbarAnimator.allowAnimation = false
+
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.id.check_box, null),
+ Text.Loaded("text"),
+ endItem = ChipbarEndItem.Loading,
+ )
+ )
+
+ val view = getChipbarView()
+ assertThat(view.getInnerView().alpha).isEqualTo(1f)
+ assertThat(view.getStartIconView().alpha).isEqualTo(1f)
+ assertThat(view.getLoadingIcon().alpha).isEqualTo(1f)
+ assertThat(view.getChipTextView().alpha).isEqualTo(1f)
+ }
+
@Test
fun updateView_viewUpdated() {
// First, display a view
@@ -453,6 +476,25 @@
verify(windowManager).removeView(chipbarView)
}
+ /** Regression test for b/266209420. */
+ @Test
+ fun removeView_animationFailure_viewStillRemoved() {
+ chipbarAnimator.allowAnimation = false
+
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ Text.Loaded("title text"),
+ endItem = ChipbarEndItem.Error,
+ ),
+ )
+ val chipbarView = getChipbarView()
+
+ underTest.removeView(DEVICE_ID, "test reason")
+
+ verify(windowManager).removeView(chipbarView)
+ }
+
@Test
fun swipeToDismiss_false_neverListensForGesture() {
underTest.displayView(
@@ -560,8 +602,9 @@
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.getChipTextView() = this.requireViewById<TextView>(R.id.text)
+
+ private fun ViewGroup.getChipText(): String = this.getChipTextView().text as String
private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
@@ -574,6 +617,25 @@
verify(windowManager).addView(viewCaptor.capture(), any())
return viewCaptor.value as ViewGroup
}
+
+ /** Test class that lets us disallow animations. */
+ inner class TestChipbarAnimator : ChipbarAnimator() {
+ var allowAnimation: Boolean = true
+
+ override fun animateViewIn(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+ if (!allowAnimation) {
+ return false
+ }
+ return super.animateViewIn(innerView, onAnimationEnd)
+ }
+
+ override fun animateViewOut(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+ if (!allowAnimation) {
+ return false
+ }
+ return super.animateViewOut(innerView, onAnimationEnd)
+ }
+ }
}
private const val TIMEOUT = 10000
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 034c618..ccf378a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -17,12 +17,17 @@
package com.android.systemui.user.data.repository
+import android.app.IActivityManager
+import android.app.UserSwitchObserver
import android.content.pm.UserInfo
+import android.os.IRemoteCallback
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.util.settings.FakeSettings
@@ -39,7 +44,14 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@@ -48,6 +60,8 @@
class UserRepositoryImplTest : SysuiTestCase() {
@Mock private lateinit var manager: UserManager
+ @Mock private lateinit var activityManager: IActivityManager
+ @Captor private lateinit var userSwitchObserver: ArgumentCaptor<UserSwitchObserver>
private lateinit var underTest: UserRepositoryImpl
@@ -214,6 +228,34 @@
assertThat(selectedUserInfo?.id).isEqualTo(1)
}
+ @Test
+ fun userSwitchingInProgress_registersOnlyOneUserSwitchObserver() = runSelfCancelingTest {
+ underTest = create(this)
+
+ underTest.userSwitchingInProgress.launchIn(this)
+ underTest.userSwitchingInProgress.launchIn(this)
+ underTest.userSwitchingInProgress.launchIn(this)
+
+ verify(activityManager, times(1)).registerUserSwitchObserver(any(), anyString())
+ }
+
+ @Test
+ fun userSwitchingInProgress_propagatesStateFromActivityManager() = runSelfCancelingTest {
+ underTest = create(this)
+ verify(activityManager)
+ .registerUserSwitchObserver(userSwitchObserver.capture(), anyString())
+
+ userSwitchObserver.value.onUserSwitching(0, mock(IRemoteCallback::class.java))
+
+ var mostRecentSwitchingValue = false
+ underTest.userSwitchingInProgress.onEach { mostRecentSwitchingValue = it }.launchIn(this)
+
+ assertThat(mostRecentSwitchingValue).isTrue()
+
+ userSwitchObserver.value.onUserSwitchComplete(0)
+ assertThat(mostRecentSwitchingValue).isFalse()
+ }
+
private fun createUserInfo(
id: Int,
isGuest: Boolean,
@@ -280,6 +322,8 @@
}
private fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
+ val featureFlags = FakeFeatureFlags()
+ featureFlags.set(FACE_AUTH_REFACTOR, true)
return UserRepositoryImpl(
appContext = context,
manager = manager,
@@ -288,6 +332,8 @@
backgroundDispatcher = IMMEDIATE,
globalSettings = globalSettings,
tracker = tracker,
+ activityManager = activityManager,
+ featureFlags = featureFlags,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
similarity index 95%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
index f3e52de..044679d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
@@ -21,7 +21,7 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-class FakeBiometricRepository : BiometricRepository {
+class FakeBiometricSettingsRepository : BiometricSettingsRepository {
private val _isFingerprintEnrolled = MutableStateFlow<Boolean>(false)
override val isFingerprintEnrolled: StateFlow<Boolean> = _isFingerprintEnrolled.asStateFlow()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardBouncerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardBouncerRepository.kt
new file mode 100644
index 0000000..d0383e9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardBouncerRepository.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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 com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
+import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Fake implementation of [KeyguardRepository] */
+class FakeKeyguardBouncerRepository : KeyguardBouncerRepository {
+ private val _primaryBouncerVisible = MutableStateFlow(false)
+ override val primaryBouncerVisible = _primaryBouncerVisible.asStateFlow()
+ private val _primaryBouncerShow = MutableStateFlow<KeyguardBouncerModel?>(null)
+ override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
+ private val _primaryBouncerShowingSoon = MutableStateFlow(false)
+ override val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.asStateFlow()
+ private val _primaryBouncerHide = MutableStateFlow(false)
+ override val primaryBouncerHide = _primaryBouncerHide.asStateFlow()
+ private val _primaryBouncerStartingToHide = MutableStateFlow(false)
+ override val primaryBouncerStartingToHide = _primaryBouncerStartingToHide.asStateFlow()
+ private val _primaryBouncerDisappearAnimation = MutableStateFlow<Runnable?>(null)
+ override val primaryBouncerStartingDisappearAnimation =
+ _primaryBouncerDisappearAnimation.asStateFlow()
+ private val _primaryBouncerScrimmed = MutableStateFlow(false)
+ override val primaryBouncerScrimmed = _primaryBouncerScrimmed.asStateFlow()
+ private val _panelExpansionAmount = MutableStateFlow(EXPANSION_HIDDEN)
+ override val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
+ private val _keyguardPosition = MutableStateFlow(0f)
+ override val keyguardPosition = _keyguardPosition.asStateFlow()
+ private val _onScreenTurnedOff = MutableStateFlow(false)
+ override val onScreenTurnedOff = _onScreenTurnedOff.asStateFlow()
+ private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
+ override val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
+ private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null)
+ override val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
+ private val _showMessage = MutableStateFlow<BouncerShowMessageModel?>(null)
+ override val showMessage = _showMessage.asStateFlow()
+ private val _resourceUpdateRequests = MutableStateFlow(false)
+ override val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
+ override val bouncerPromptReason = 0
+ override val bouncerErrorMessage: CharSequence? = null
+ private val _isAlternateBouncerVisible = MutableStateFlow(false)
+ override val isAlternateBouncerVisible = _isAlternateBouncerVisible.asStateFlow()
+ override var lastAlternateBouncerVisibleTime: Long = 0L
+ private val _isAlternateBouncerUIAvailable = MutableStateFlow<Boolean>(false)
+ override val isAlternateBouncerUIAvailable = _isAlternateBouncerUIAvailable.asStateFlow()
+
+ override fun setPrimaryScrimmed(isScrimmed: Boolean) {
+ _primaryBouncerScrimmed.value = isScrimmed
+ }
+
+ override fun setPrimaryVisible(isVisible: Boolean) {
+ _primaryBouncerVisible.value = isVisible
+ }
+
+ override fun setAlternateVisible(isVisible: Boolean) {
+ _isAlternateBouncerVisible.value = isVisible
+ }
+
+ override fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
+ _isAlternateBouncerUIAvailable.value = isAvailable
+ }
+
+ override fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?) {
+ _primaryBouncerShow.value = keyguardBouncerModel
+ }
+
+ override fun setPrimaryShowingSoon(showingSoon: Boolean) {
+ _primaryBouncerShowingSoon.value = showingSoon
+ }
+
+ override fun setPrimaryHide(hide: Boolean) {
+ _primaryBouncerHide.value = hide
+ }
+
+ override fun setPrimaryStartingToHide(startingToHide: Boolean) {
+ _primaryBouncerStartingToHide.value = startingToHide
+ }
+
+ override fun setPrimaryStartDisappearAnimation(runnable: Runnable?) {
+ _primaryBouncerDisappearAnimation.value = runnable
+ }
+
+ override fun setPanelExpansion(panelExpansion: Float) {
+ _panelExpansionAmount.value = panelExpansion
+ }
+
+ override fun setKeyguardPosition(keyguardPosition: Float) {
+ _keyguardPosition.value = keyguardPosition
+ }
+
+ override fun setResourceUpdateRequests(willUpdateResources: Boolean) {
+ _resourceUpdateRequests.value = willUpdateResources
+ }
+
+ override fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?) {
+ _showMessage.value = bouncerShowMessageModel
+ }
+
+ override fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?) {
+ _keyguardAuthenticated.value = keyguardAuthenticated
+ }
+
+ override fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean) {
+ _isBackButtonEnabled.value = isBackButtonEnabled
+ }
+
+ override fun setOnScreenTurnedOff(onScreenTurnedOff: Boolean) {
+ _onScreenTurnedOff.value = onScreenTurnedOff
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt
index 6ae7c34..1403cea 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt
@@ -21,7 +21,7 @@
import android.view.Display
import java.util.concurrent.Executor
-class FakeDisplayTracker internal constructor(val context: Context) : DisplayTracker {
+class FakeDisplayTracker constructor(val context: Context) : DisplayTracker {
val displayManager: DisplayManager = context.getSystemService(DisplayManager::class.java)!!
override var defaultDisplayId: Int = Display.DEFAULT_DISPLAY
override var allDisplays: Array<Display> = displayManager.displays
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index ea5a302..1a8e244 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -39,6 +39,10 @@
private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
+ private val _userSwitchingInProgress = MutableStateFlow(false)
+ override val userSwitchingInProgress: Flow<Boolean>
+ get() = _userSwitchingInProgress
+
override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
private var _isGuestUserAutoCreated: Boolean = false
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 5d4dc39..1428fa8 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -14655,6 +14655,17 @@
throw new SecurityException(msg);
}
}
+ if (!Build.IS_DEBUGGABLE && callingUid != ROOT_UID && callingUid != SHELL_UID
+ && callingUid != SYSTEM_UID && !hasActiveInstrumentationLocked(callingPid)) {
+ // If it's not debug build and not called from root/shell/system uid, reject it.
+ final String msg = "Permission Denial: instrumentation test "
+ + className + " from pid=" + callingPid + ", uid=" + callingUid
+ + ", pkgName=" + getPackageNameByPid(callingPid)
+ + " not allowed because it's not started from SHELL";
+ Slog.wtfQuiet(TAG, msg);
+ reportStartInstrumentationFailureLocked(watcher, className, msg);
+ throw new SecurityException(msg);
+ }
boolean disableHiddenApiChecks = ai.usesNonSdkApi()
|| (flags & INSTR_FLAG_DISABLE_HIDDEN_API_CHECKS) != 0;
@@ -14877,6 +14888,29 @@
}
}
+ @GuardedBy("this")
+ private boolean hasActiveInstrumentationLocked(int pid) {
+ if (pid == 0) {
+ return false;
+ }
+ synchronized (mPidsSelfLocked) {
+ ProcessRecord process = mPidsSelfLocked.get(pid);
+ return process != null && process.getActiveInstrumentation() != null;
+ }
+ }
+
+ private String getPackageNameByPid(int pid) {
+ synchronized (mPidsSelfLocked) {
+ final ProcessRecord app = mPidsSelfLocked.get(pid);
+
+ if (app != null && app.info != null) {
+ return app.info.packageName;
+ }
+
+ return null;
+ }
+ }
+
private boolean isCallerShell() {
final int callingUid = Binder.getCallingUid();
return callingUid == SHELL_UID || callingUid == ROOT_UID;
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index dfb2467..b8e3a3a 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -2194,19 +2194,19 @@
AudioSystem.STREAM_ASSISTANT : AudioSystem.STREAM_MUSIC;
if (mIsSingleVolume) {
- mStreamVolumeAlias = STREAM_VOLUME_ALIAS_TELEVISION;
+ mStreamVolumeAlias = STREAM_VOLUME_ALIAS_TELEVISION.clone();
dtmfStreamAlias = AudioSystem.STREAM_MUSIC;
} else if (mUseVolumeGroupAliases) {
- mStreamVolumeAlias = STREAM_VOLUME_ALIAS_NONE;
+ mStreamVolumeAlias = STREAM_VOLUME_ALIAS_NONE.clone();
dtmfStreamAlias = AudioSystem.STREAM_DTMF;
} else {
switch (mPlatformType) {
case AudioSystem.PLATFORM_VOICE:
- mStreamVolumeAlias = STREAM_VOLUME_ALIAS_VOICE;
+ mStreamVolumeAlias = STREAM_VOLUME_ALIAS_VOICE.clone();
dtmfStreamAlias = AudioSystem.STREAM_RING;
break;
default:
- mStreamVolumeAlias = STREAM_VOLUME_ALIAS_DEFAULT;
+ mStreamVolumeAlias = STREAM_VOLUME_ALIAS_DEFAULT.clone();
dtmfStreamAlias = AudioSystem.STREAM_MUSIC;
}
if (!mNotifAliasRing) {
@@ -3507,11 +3507,12 @@
synchronized (VolumeStreamState.class) {
List<Integer> streamsToMute = new ArrayList<>();
for (int stream = 0; stream < mStreamStates.length; stream++) {
- if (streamAlias == mStreamVolumeAlias[stream]) {
+ VolumeStreamState vss = mStreamStates[stream];
+ if (streamAlias == mStreamVolumeAlias[stream] && vss.isMutable()) {
if (!(readCameraSoundForced()
- && (mStreamStates[stream].getStreamType()
+ && (vss.getStreamType()
== AudioSystem.STREAM_SYSTEM_ENFORCED))) {
- boolean changed = mStreamStates[stream].mute(state, /* apply= */ false);
+ boolean changed = vss.mute(state, /* apply= */ false);
if (changed) {
streamsToMute.add(stream);
}
@@ -5201,7 +5202,8 @@
if (!shouldMute) {
// unmute
// ring and notifications volume should never be 0 when not silenced
- if (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_RING) {
+ if (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_RING
+ || mStreamVolumeAlias[streamType] == AudioSystem.STREAM_NOTIFICATION) {
synchronized (VolumeStreamState.class) {
final VolumeStreamState vss = mStreamStates[streamType];
for (int i = 0; i < vss.mIndexMap.size(); i++) {
@@ -5926,6 +5928,8 @@
}
}
+ readVolumeGroupsSettings(userSwitch);
+
// apply new ringer mode before checking volume for alias streams so that streams
// muted by ringer mode have the correct volume
setRingerModeInt(getRingerModeInternal(), false);
@@ -5943,8 +5947,6 @@
}
}
- readVolumeGroupsSettings(userSwitch);
-
if (DEBUG_VOL) {
Log.d(TAG, "Restoring device volume behavior");
}
@@ -8384,9 +8386,10 @@
}
mVolumeGroupState.updateVolumeIndex(groupIndex, device);
// Only propage mute of stream when applicable
- if (mIndexMin == 0 || isCallStream(mStreamType)) {
+ if (isMutable()) {
// For call stream, align mute only when muted, not when index is set to 0
- mVolumeGroupState.mute(forceMuteState ? mIsMuted : groupIndex == 0);
+ mVolumeGroupState.mute(
+ forceMuteState ? mIsMuted : groupIndex == 0 || mIsMuted);
}
}
}
@@ -8435,6 +8438,12 @@
return mIsMuted || mIsMutedInternally;
}
+
+ private boolean isMutable() {
+ return isStreamAffectedByMute(mStreamType)
+ && (mIndexMin == 0 || isCallStream(mStreamType));
+ }
+
/**
* Mute/unmute the stream
* @param state the new mute state
@@ -10211,7 +10220,7 @@
private static final int CHECK_MODE_FOR_UID_PERIOD_MS = 6000;
private static final String ACTION_CHECK_MUSIC_ACTIVE =
- AudioService.class.getSimpleName() + ".CHECK_MUSIC_ACTIVE";
+ "com.android.server.audio.action.CHECK_MUSIC_ACTIVE";
private static final int REQUEST_CODE_CHECK_MUSIC_ACTIVE = 1;
private int safeMediaVolumeIndex(int device) {
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index fdfc20a..7c6b667 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -561,7 +561,7 @@
/**
* Sets the display mode switching type.
- * @param newType
+ * @param newType new mode switching type
*/
public void setModeSwitchingType(@DisplayManager.SwitchingType int newType) {
synchronized (mLock) {
@@ -678,6 +678,18 @@
notifyDesiredDisplayModeSpecsChangedLocked();
}
+ @GuardedBy("mLock")
+ private float getMaxRefreshRateLocked(int displayId) {
+ Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
+ float maxRefreshRate = 0f;
+ for (Display.Mode mode : modes) {
+ if (mode.getRefreshRate() > maxRefreshRate) {
+ maxRefreshRate = mode.getRefreshRate();
+ }
+ }
+ return maxRefreshRate;
+ }
+
private void notifyDesiredDisplayModeSpecsChangedLocked() {
if (mDesiredDisplayModeSpecsListener != null
&& !mHandler.hasMessages(MSG_REFRESH_RATE_RANGE_CHANGED)) {
@@ -996,25 +1008,29 @@
// of low priority voters. It votes [0, max(PEAK, MIN)]
public static final int PRIORITY_USER_SETTING_PEAK_REFRESH_RATE = 7;
+ // To avoid delay in switching between 60HZ -> 90HZ when activating LHBM, set refresh
+ // rate to max value (same as for PRIORITY_UDFPS) on lock screen
+ public static final int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 8;
+
// LOW_POWER_MODE force display to [0, 60HZ] if Settings.Global.LOW_POWER_MODE is on.
- public static final int PRIORITY_LOW_POWER_MODE = 8;
+ public static final int PRIORITY_LOW_POWER_MODE = 9;
// PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
// higher priority voters' result is a range, it will fix the rate to a single choice.
// It's used to avoid refresh rate switches in certain conditions which may result in the
// user seeing the display flickering when the switches occur.
- public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 9;
+ public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 10;
// Force display to [0, 60HZ] if skin temperature is at or above CRITICAL.
- public static final int PRIORITY_SKIN_TEMPERATURE = 10;
+ public static final int PRIORITY_SKIN_TEMPERATURE = 11;
// The proximity sensor needs the refresh rate to be locked in order to function, so this is
// set to a high priority.
- public static final int PRIORITY_PROXIMITY = 11;
+ public static final int PRIORITY_PROXIMITY = 12;
// The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order
// to function, so this needs to be the highest priority of all votes.
- public static final int PRIORITY_UDFPS = 12;
+ public static final int PRIORITY_UDFPS = 13;
// Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
// APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
@@ -1117,6 +1133,8 @@
return "PRIORITY_USER_SETTING_MIN_REFRESH_RATE";
case PRIORITY_USER_SETTING_PEAK_REFRESH_RATE:
return "PRIORITY_USER_SETTING_PEAK_REFRESH_RATE";
+ case PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE:
+ return "PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE";
default:
return Integer.toString(priority);
}
@@ -2329,6 +2347,7 @@
private class UdfpsObserver extends IUdfpsHbmListener.Stub {
private final SparseBooleanArray mLocalHbmEnabled = new SparseBooleanArray();
+ private final SparseBooleanArray mAuthenticationPossible = new SparseBooleanArray();
public void observe() {
StatusBarManagerInternal statusBar =
@@ -2354,25 +2373,28 @@
private void updateHbmStateLocked(int displayId, boolean enabled) {
mLocalHbmEnabled.put(displayId, enabled);
- updateVoteLocked(displayId);
+ updateVoteLocked(displayId, enabled, Vote.PRIORITY_UDFPS);
}
- private void updateVoteLocked(int displayId) {
+ @Override
+ public void onAuthenticationPossible(int displayId, boolean isPossible) {
+ synchronized (mLock) {
+ mAuthenticationPossible.put(displayId, isPossible);
+ updateVoteLocked(displayId, isPossible,
+ Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void updateVoteLocked(int displayId, boolean enabled, int votePriority) {
final Vote vote;
- if (mLocalHbmEnabled.get(displayId)) {
- Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
- float maxRefreshRate = 0f;
- for (Display.Mode mode : modes) {
- if (mode.getRefreshRate() > maxRefreshRate) {
- maxRefreshRate = mode.getRefreshRate();
- }
- }
+ if (enabled) {
+ float maxRefreshRate = DisplayModeDirector.this.getMaxRefreshRateLocked(displayId);
vote = Vote.forRefreshRates(maxRefreshRate, maxRefreshRate);
} else {
vote = null;
}
-
- DisplayModeDirector.this.updateVoteLocked(displayId, Vote.PRIORITY_UDFPS, vote);
+ DisplayModeDirector.this.updateVoteLocked(displayId, votePriority, vote);
}
void dumpLocked(PrintWriter pw) {
@@ -2383,6 +2405,13 @@
final String enabled = mLocalHbmEnabled.valueAt(i) ? "enabled" : "disabled";
pw.println(" Display " + displayId + ": " + enabled);
}
+ pw.println(" mAuthenticationPossible: ");
+ for (int i = 0; i < mAuthenticationPossible.size(); i++) {
+ final int displayId = mAuthenticationPossible.keyAt(i);
+ final String isPossible = mAuthenticationPossible.valueAt(i) ? "possible"
+ : "impossible";
+ pw.println(" Display " + displayId + ": " + isPossible);
+ }
}
}
@@ -2812,8 +2841,8 @@
.sendToTarget();
if (refreshRateInLowZone != -1) {
- mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone)
- .sendToTarget();
+ mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone,
+ 0).sendToTarget();
}
int[] highDisplayBrightnessThresholds = getHighDisplayBrightnessThresholds();
@@ -2825,8 +2854,8 @@
.sendToTarget();
if (refreshRateInHighZone != -1) {
- mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone)
- .sendToTarget();
+ mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone,
+ 0).sendToTarget();
}
final int refreshRateInHbmSunlight = getRefreshRateInHbmSunlight();
diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
index 85d5b4f..5b772fc 100644
--- a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
+++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
@@ -419,16 +419,16 @@
float ambientBrightness = mBrightnessFilter.getEstimate(time);
mLatestAmbientBrightness = ambientBrightness;
- if (ambientColorTemperature != -1.0f &&
- mLowLightAmbientBrightnessToBiasSpline != null) {
+ if (ambientColorTemperature != -1.0f && ambientBrightness != -1.0f
+ && mLowLightAmbientBrightnessToBiasSpline != null) {
float bias = mLowLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
ambientColorTemperature =
bias * ambientColorTemperature + (1.0f - bias)
* mLowLightAmbientColorTemperature;
mLatestLowLightBias = bias;
}
- if (ambientColorTemperature != -1.0f &&
- mHighLightAmbientBrightnessToBiasSpline != null) {
+ if (ambientColorTemperature != -1.0f && ambientBrightness != -1.0f
+ && mHighLightAmbientBrightnessToBiasSpline != null) {
float bias = mHighLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
ambientColorTemperature =
(1.0f - bias) * ambientColorTemperature + bias
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index 1235352..f0aff2a 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -300,6 +300,7 @@
public void deliverOnFlushComplete(int requestCode) throws PendingIntent.CanceledException {
BroadcastOptions options = BroadcastOptions.makeBasic();
options.setDontSendToRestrictedApps(true);
+ options.setPendingIntentBackgroundActivityLaunchAllowed(false);
mPendingIntent.send(mContext, 0, new Intent().putExtra(KEY_FLUSH_COMPLETE, requestCode),
null, null, null, options.toBundle());
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 6bc8582..b5fceb5 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -6687,6 +6687,31 @@
}
}
+ // Ensure all actions are present
+ if (notification.actions != null) {
+ boolean hasNullActions = false;
+ int nActions = notification.actions.length;
+ for (int i = 0; i < nActions; i++) {
+ if (notification.actions[i] == null) {
+ hasNullActions = true;
+ break;
+ }
+ }
+ if (hasNullActions) {
+ ArrayList<Notification.Action> nonNullActions = new ArrayList<>();
+ for (int i = 0; i < nActions; i++) {
+ if (notification.actions[i] != null) {
+ nonNullActions.add(notification.actions[i]);
+ }
+ }
+ if (nonNullActions.size() != 0) {
+ notification.actions = nonNullActions.toArray(new Notification.Action[0]);
+ } else {
+ notification.actions = null;
+ }
+ }
+ }
+
// Ensure CallStyle has all the correct actions
if (notification.isStyle(Notification.CallStyle.class)) {
Notification.Builder builder =
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 7da5f51..29d684a 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -2425,10 +2425,10 @@
// will be null whereas dataOwnerPkg will contain information about the package
// which was uninstalled while keeping its data.
AndroidPackage dataOwnerPkg = mPm.mPackages.get(packageName);
+ PackageSetting dataOwnerPs = mPm.mSettings.getPackageLPr(packageName);
if (dataOwnerPkg == null) {
- PackageSetting ps = mPm.mSettings.getPackageLPr(packageName);
- if (ps != null) {
- dataOwnerPkg = ps.getPkg();
+ if (dataOwnerPs != null) {
+ dataOwnerPkg = dataOwnerPs.getPkg();
}
}
@@ -2456,6 +2456,7 @@
if (dataOwnerPkg != null && !dataOwnerPkg.isSdkLibrary()) {
if (!PackageManagerServiceUtils.isDowngradePermitted(installFlags,
dataOwnerPkg.isDebuggable())) {
+ // Downgrade is not permitted; a lower version of the app will not be allowed
try {
PackageManagerServiceUtils.checkDowngrade(dataOwnerPkg, pkgLite);
} catch (PackageManagerException e) {
@@ -2464,6 +2465,28 @@
return Pair.create(
PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE, errorMsg);
}
+ } else if (dataOwnerPs.isSystem()) {
+ // Downgrade is permitted, but system apps can't be downgraded below
+ // the version preloaded onto the system image
+ final PackageSetting disabledPs = mPm.mSettings.getDisabledSystemPkgLPr(
+ dataOwnerPs);
+ if (disabledPs != null) {
+ dataOwnerPkg = disabledPs.getPkg();
+ }
+ if (!Build.IS_DEBUGGABLE && !dataOwnerPkg.isDebuggable()) {
+ // Only restrict non-debuggable builds and non-debuggable version of the app
+ try {
+ PackageManagerServiceUtils.checkDowngrade(dataOwnerPkg, pkgLite);
+ } catch (PackageManagerException e) {
+ String errorMsg =
+ "System app: " + packageName + " cannot be downgraded to"
+ + " older than its preloaded version on the system"
+ + " image. " + e.getMessage();
+ Slog.w(TAG, errorMsg);
+ return Pair.create(
+ PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE, errorMsg);
+ }
+ }
}
}
}
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 890c891..632a34e 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -885,7 +885,12 @@
* available ShareTarget definitions in this package.
*/
public List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
- @NonNull IntentFilter filter) {
+ @NonNull final IntentFilter filter) {
+ return getMatchingShareTargets(filter, null);
+ }
+
+ List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
+ @NonNull final IntentFilter filter, @Nullable final String pkgName) {
synchronized (mLock) {
final List<ShareTargetInfo> matchedTargets = new ArrayList<>();
for (int i = 0; i < mShareTargets.size(); i++) {
@@ -909,8 +914,7 @@
// included in the result
findAll(shortcuts, ShortcutInfo::isNonManifestVisible,
ShortcutInfo.CLONE_REMOVE_FOR_APP_PREDICTION,
- mShortcutUser.mService.mContext.getPackageName(),
- 0, /*getPinnedByAnyLauncher=*/ false);
+ pkgName, 0, /*getPinnedByAnyLauncher=*/ false);
final List<ShortcutManager.ShareShortcutInfo> result = new ArrayList<>();
for (int i = 0; i < shortcuts.size(); i++) {
@@ -1108,7 +1112,7 @@
// Now prepare to publish manifest shortcuts.
List<ShortcutInfo> newManifestShortcutList = null;
- final int shareTargetSize;
+ int shareTargetSize = 0;
synchronized (mLock) {
try {
shareTargetSize = mShareTargets.size();
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 0b20683..49831d7 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -2512,11 +2512,17 @@
}
enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APP_PREDICTIONS,
"getShareTargets");
+ final ComponentName chooser = injectChooserActivity();
+ final String pkg = (chooser != null
+ && mPackageManagerInternal.getComponentEnabledSetting(chooser,
+ injectBinderCallingUid(), userId) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED)
+ ? chooser.getPackageName() : mContext.getPackageName();
synchronized (mLock) {
throwIfUserLockedL(userId);
final List<ShortcutManager.ShareShortcutInfo> shortcutInfoList = new ArrayList<>();
final ShortcutUser user = getUserShortcutsLocked(userId);
- user.forAllPackages(p -> shortcutInfoList.addAll(p.getMatchingShareTargets(filter)));
+ user.forAllPackages(p -> shortcutInfoList.addAll(
+ p.getMatchingShareTargets(filter, pkg)));
return new ParceledListSlice<>(shortcutInfoList);
}
}
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
index 7ce7f7e..810fa5f 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
@@ -247,6 +247,9 @@
private static final String MAX_NUM_COMPONENTS_ERR_MSG =
"Total number of components has exceeded the maximum number: " + MAX_NUM_COMPONENTS;
+ /** The maximum permission name length. */
+ private static final int MAX_PERMISSION_NAME_LENGTH = 512;
+
@IntDef(flag = true, prefix = { "PARSE_" }, value = {
PARSE_CHATTY,
PARSE_COLLECT_CERTIFICATES,
@@ -1275,6 +1278,11 @@
// that may change.
String name = sa.getNonResourceString(
R.styleable.AndroidManifestUsesPermission_name);
+ if (TextUtils.length(name) > MAX_PERMISSION_NAME_LENGTH) {
+ return input.error(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
+ "The name in the <uses-permission> is greater than "
+ + MAX_PERMISSION_NAME_LENGTH);
+ }
int maxSdkVersion = 0;
TypedValue val = sa.peekValue(
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index d249547..7370d61 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -329,6 +329,8 @@
static public final String SYSTEM_DIALOG_REASON_SCREENSHOT = "screenshot";
static public final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav";
+ public static final String TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD = "waitForAllWindowsDrawn";
+
private static final String TALKBACK_LABEL = "TalkBack";
private static final int POWER_BUTTON_SUPPRESSION_DELAY_DEFAULT_MILLIS = 800;
@@ -4724,10 +4726,15 @@
// ... eventually calls finishWindowsDrawn which will finalize our screen turn on
// as well as enabling the orientation change logic/sensor.
+ Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER,
+ TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD, /* cookie= */ 0);
mWindowManagerInternal.waitForAllWindowsDrawn(() -> {
if (DEBUG_WAKEUP) Slog.i(TAG, "All windows ready for every display");
mHandler.sendMessage(mHandler.obtainMessage(MSG_WINDOW_MANAGER_DRAWN_COMPLETE,
INVALID_DISPLAY, 0));
+
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER,
+ TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD, /* cookie= */ 0);
}, WAITING_FOR_DRAWN_TIMEOUT, INVALID_DISPLAY);
}
@@ -4783,10 +4790,16 @@
}
} else {
mScreenOnListeners.put(displayId, screenOnListener);
+
+ Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER,
+ TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD, /* cookie= */ 0);
mWindowManagerInternal.waitForAllWindowsDrawn(() -> {
if (DEBUG_WAKEUP) Slog.i(TAG, "All windows ready for display: " + displayId);
mHandler.sendMessage(mHandler.obtainMessage(MSG_WINDOW_MANAGER_DRAWN_COMPLETE,
displayId, 0));
+
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER,
+ TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD, /* cookie= */ 0);
}, WAITING_FOR_DRAWN_TIMEOUT, displayId);
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 2ebf8d9..f7b9d8b 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -10214,8 +10214,8 @@
// TODO(b/263592337): Explore enabling compat fake focus for fullscreen, e.g. for when
// covered with bubbles.
boolean shouldSendCompatFakeFocus() {
- return mWmService.mLetterboxConfiguration.isCompatFakeFocusEnabled(info)
- && inMultiWindowMode() && !inPinnedWindowingMode() && !inFreeformWindowingMode();
+ return mLetterboxUiController.shouldSendFakeFocus() && inMultiWindowMode()
+ && !inPinnedWindowingMode() && !inFreeformWindowingMode();
}
static class Builder {
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index fad2dda..b8870a1 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
@@ -245,7 +246,22 @@
|| orientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) {
return false;
}
- return getIgnoreOrientationRequest();
+ return getIgnoreOrientationRequest()
+ && !shouldRespectOrientationRequestDueToPerAppOverride();
+ }
+
+ private boolean shouldRespectOrientationRequestDueToPerAppOverride() {
+ if (mDisplayContent == null) {
+ return false;
+ }
+ ActivityRecord activity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ return activity != null && activity.getTaskFragment() != null
+ // Checking TaskFragment rather than ActivityRecord to ensure that transition
+ // between fullscreen and PiP would work well. Checking TaskFragment rather than
+ // Task to ensure that Activity Embedding is excluded.
+ && activity.getTaskFragment().getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+ && activity.mLetterboxUiController.isOverrideRespectRequestedOrientationEnabled();
}
boolean getIgnoreOrientationRequest() {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 5767730..71a7e26 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1679,7 +1679,7 @@
}
// The orientation source may not be the top if it uses SCREEN_ORIENTATION_BEHIND.
final ActivityRecord topCandidate = !r.isVisibleRequested() ? topRunningActivity() : r;
- if (handleTopActivityLaunchingInDifferentOrientation(
+ if (topCandidate != null && handleTopActivityLaunchingInDifferentOrientation(
topCandidate, r, true /* checkOpening */)) {
// Display orientation should be deferred until the top fixed rotation is finished.
return false;
@@ -1710,12 +1710,9 @@
return ROTATION_UNDEFINED;
}
if (activityOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
- // TODO(b/266280737): Use ActivityRecord#canDefineOrientationForActivitiesAbove
final ActivityRecord nextCandidate = getActivity(
- a -> a.getOverrideOrientation() != SCREEN_ORIENTATION_UNSET
- && a.getOverrideOrientation()
- != ActivityInfo.SCREEN_ORIENTATION_BEHIND,
- r, false /* includeBoundary */, true /* traverseTopToBottom */);
+ a -> a.canDefineOrientationForActivitiesAbove() /* callback */,
+ r /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */);
if (nextCandidate != null) {
r = nextCandidate;
}
@@ -2119,6 +2116,10 @@
w.seamlesslyRotateIfAllowed(transaction, oldRotation, rotation, rotateSeamlessly);
}, true /* traverseTopToBottom */);
mPinnedTaskController.startSeamlessRotationIfNeeded(transaction, oldRotation, rotation);
+ if (!mDisplayRotation.hasSeamlessRotatingWindow()) {
+ // Make sure DisplayRotation#isRotatingSeamlessly() will return false.
+ mDisplayRotation.cancelSeamlessRotation();
+ }
}
mWmService.mDisplayManagerInternal.performTraversal(transaction);
@@ -4827,7 +4828,7 @@
mInsetsStateController.getImeSourceProvider().checkShowImePostLayout();
mLastHasContent = mTmpApplySurfaceChangesTransactionState.displayHasContent;
- if (!mWmService.mDisplayFrozen) {
+ if (!mWmService.mDisplayFrozen && !mDisplayRotation.isRotatingSeamlessly()) {
mWmService.mDisplayManagerInternal.setDisplayProperties(mDisplayId,
mLastHasContent,
mTmpApplySurfaceChangesTransactionState.preferredRefreshRate,
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index 3ffb2fa..e04900c 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -16,6 +16,8 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
@@ -34,6 +36,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.StringRes;
import android.app.servertransaction.ClientTransaction;
import android.app.servertransaction.RefreshCallbackItem;
import android.app.servertransaction.ResumeActivityItem;
@@ -44,10 +47,13 @@
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.widget.Toast;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.server.UiThread;
import java.util.Map;
import java.util.Set;
@@ -232,6 +238,27 @@
activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
}
+ /**
+ * Notifies that animation in {@link ScreenAnimationRotation} has finished.
+ *
+ * <p>This class uses this signal as a trigger for notifying the user about forced rotation
+ * reason with the {@link Toast}.
+ */
+ void onScreenRotationAnimationFinished() {
+ if (!isTreatmentEnabledForDisplay() || mCameraIdPackageBiMap.isEmpty()) {
+ return;
+ }
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (topActivity == null
+ // Checking windowing mode on activity level because we don't want to
+ // show toast in case of activity embedding.
+ || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+ return;
+ }
+ showToast(R.string.display_rotation_camera_compat_toast_after_rotation);
+ }
+
String getSummaryForDisplayRotationHistoryRecord() {
String summaryIfEnabled = "";
if (isTreatmentEnabledForDisplay()) {
@@ -281,6 +308,10 @@
&& mDisplayContent.getDisplay().getType() == TYPE_INTERNAL;
}
+ boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) {
+ return isTreatmentEnabledForDisplay() && isCameraActiveInFullscreen(activity);
+ }
+
/**
* Whether camera compat treatment is applicable for the given activity.
*
@@ -292,12 +323,16 @@
* </ul>
*/
boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
- return activity != null && !activity.inMultiWindowMode()
+ return activity != null && isCameraActiveInFullscreen(activity)
&& activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
// "locked" and "nosensor" values are often used by camera apps that can't
// handle dynamic changes so we shouldn't force rotate them.
&& activity.getOverrideOrientation() != SCREEN_ORIENTATION_NOSENSOR
- && activity.getOverrideOrientation() != SCREEN_ORIENTATION_LOCKED
+ && activity.getOverrideOrientation() != SCREEN_ORIENTATION_LOCKED;
+ }
+
+ private boolean isCameraActiveInFullscreen(@NonNull ActivityRecord activity) {
+ return !activity.inMultiWindowMode()
&& mCameraIdPackageBiMap.containsPackageName(activity.packageName)
&& activity.mLetterboxUiController.shouldForceRotateForCameraCompat();
}
@@ -334,7 +369,31 @@
}
mCameraIdPackageBiMap.put(packageName, cameraId);
}
- updateOrientationWithWmLock();
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (topActivity == null || topActivity.getTask() == null) {
+ return;
+ }
+ // Checking whether an activity in fullscreen rather than the task as this camera compat
+ // treatment doesn't cover activity embedding.
+ if (topActivity.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+ if (topActivity.mLetterboxUiController.isOverrideOrientationOnlyForCameraEnabled()) {
+ topActivity.recomputeConfiguration();
+ }
+ updateOrientationWithWmLock();
+ return;
+ }
+ // Checking that the whole app is in multi-window mode as we shouldn't show toast
+ // for the activity embedding case.
+ if (topActivity.getTask().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
+ showToast(R.string.display_rotation_camera_compat_toast_in_split_screen);
+ }
+ }
+
+ @VisibleForTesting
+ void showToast(@StringRes int stringRes) {
+ UiThread.getHandler().post(
+ () -> Toast.makeText(mWmService.mContext, stringRes, Toast.LENGTH_LONG).show());
}
private synchronized void notifyCameraClosed(@NonNull String cameraId) {
@@ -375,6 +434,17 @@
ProtoLog.v(WM_DEBUG_ORIENTATION,
"Display id=%d is notified that Camera %s is closed, updating rotation.",
mDisplayContent.mDisplayId, cameraId);
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (topActivity == null
+ // Checking whether an activity in fullscreen rather than the task as this camera
+ // compat treatment doesn't cover activity embedding.
+ || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+ return;
+ }
+ if (topActivity.mLetterboxUiController.isOverrideOrientationOnlyForCameraEnabled()) {
+ topActivity.recomputeConfiguration();
+ }
updateOrientationWithWmLock();
}
@@ -396,6 +466,10 @@
private final Map<String, String> mPackageToCameraIdMap = new ArrayMap<>();
private final Map<String, String> mCameraIdToPackageMap = new ArrayMap<>();
+ boolean isEmpty() {
+ return mCameraIdToPackageMap.isEmpty();
+ }
+
void put(String packageName, String cameraId) {
// Always using the last connected camera ID for the package even for the concurrent
// camera use case since we can't guess which camera is more important anyway.
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 800fe09..7066a33 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -18,14 +18,14 @@
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ALLOW_IGNORE_ORIENTATION_REQUEST;
+import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_CAMERA_COMPAT_TREATMENT;
import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
import android.graphics.Color;
import android.provider.DeviceConfig;
import android.util.Slog;
@@ -309,14 +309,28 @@
mIsDisplayRotationImmersiveAppCompatPolicyEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsDisplayRotationImmersiveAppCompatPolicyEnabled);
mDeviceConfig.updateFlagActiveStatus(
+ /* isActive */ mIsCameraCompatTreatmentEnabled,
+ /* key */ KEY_ENABLE_CAMERA_COMPAT_TREATMENT);
+ mDeviceConfig.updateFlagActiveStatus(
/* isActive */ mIsDisplayRotationImmersiveAppCompatPolicyEnabled,
/* key */ KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY);
+ mDeviceConfig.updateFlagActiveStatus(
+ /* isActive */ true,
+ /* key */ KEY_ALLOW_IGNORE_ORIENTATION_REQUEST);
mLetterboxConfigurationPersister = letterboxConfigurationPersister;
mLetterboxConfigurationPersister.start();
}
/**
+ * Whether enabling ignoreOrientationRequest is allowed on the device. This value is controlled
+ * via {@link android.provider.DeviceConfig}.
+ */
+ boolean isIgnoreOrientationRequestAllowed() {
+ return mDeviceConfig.getFlag(KEY_ALLOW_IGNORE_ORIENTATION_REQUEST);
+ }
+
+ /**
* Overrides the aspect ratio of letterbox for fixed orientation. If given value is <= {@link
* #MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO}, both it and a value of {@link
* com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio} will be ignored and
@@ -1050,38 +1064,8 @@
"enable_translucent_activity_letterbox", false);
}
- @VisibleForTesting
- boolean getPackageManagerProperty(PackageManager pm, String property) {
- boolean enabled = false;
- try {
- final PackageManager.Property p = pm.getProperty(property, mContext.getPackageName());
- enabled = p.getBoolean();
- } catch (PackageManager.NameNotFoundException e) {
- // Property not found
- }
- return enabled;
- }
-
- @VisibleForTesting
- boolean isCompatFakeFocusEnabled(ActivityInfo info) {
- if (!isCompatFakeFocusEnabledOnDevice()) {
- return false;
- }
- // See if the developer has chosen to opt in / out of treatment
- PackageManager pm = mContext.getPackageManager();
- if (getPackageManagerProperty(pm, PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT)) {
- return false;
- } else if (getPackageManagerProperty(pm, PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN)) {
- return true;
- }
- if (info.isChangeEnabled(ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS)) {
- return true;
- }
- return false;
- }
-
/** Whether fake sending focus is enabled for unfocused apps in splitscreen */
- boolean isCompatFakeFocusEnabledOnDevice() {
+ boolean isCompatFakeFocusEnabled() {
return mIsCompatFakeFocusEnabled
&& DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, true);
@@ -1106,15 +1090,8 @@
/** Whether camera compatibility treatment is enabled. */
boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) {
- return mIsCameraCompatTreatmentEnabled
- && (!checkDeviceConfig || isCameraCompatTreatmentAllowed());
- }
-
- // TODO(b/262977416): Cache a runtime flag and implement
- // DeviceConfig.OnPropertiesChangedListener
- private static boolean isCameraCompatTreatmentAllowed() {
- return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
- "enable_compat_camera_treatment", true);
+ return mIsCameraCompatTreatmentEnabled && (!checkDeviceConfig
+ || mDeviceConfig.getFlag(KEY_ENABLE_CAMERA_COMPAT_TREATMENT));
}
/** Whether camera compatibility refresh is enabled. */
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
index cf123a1..d004fa6 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
@@ -33,17 +33,33 @@
final class LetterboxConfigurationDeviceConfig
implements DeviceConfig.OnPropertiesChangedListener {
+ static final String KEY_ENABLE_CAMERA_COMPAT_TREATMENT = "enable_compat_camera_treatment";
+ private static final boolean DEFAULT_VALUE_ENABLE_CAMERA_COMPAT_TREATMENT = true;
+
static final String KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY =
"enable_display_rotation_immersive_app_compat_policy";
private static final boolean DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY =
true;
+ static final String KEY_ALLOW_IGNORE_ORIENTATION_REQUEST =
+ "allow_ignore_orientation_request";
+ private static final boolean DEFAULT_VALUE_ALLOW_IGNORE_ORIENTATION_REQUEST = true;
+
@VisibleForTesting
static final Map<String, Boolean> sKeyToDefaultValueMap = Map.of(
+ KEY_ENABLE_CAMERA_COMPAT_TREATMENT,
+ DEFAULT_VALUE_ENABLE_CAMERA_COMPAT_TREATMENT,
KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY,
- DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY
+ DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY,
+ KEY_ALLOW_IGNORE_ORIENTATION_REQUEST,
+ DEFAULT_VALUE_ALLOW_IGNORE_ORIENTATION_REQUEST
);
+ // Whether camera compatibility treatment is enabled.
+ // See DisplayRotationCompatPolicy for context.
+ private boolean mIsCameraCompatTreatmentEnabled =
+ DEFAULT_VALUE_ENABLE_CAMERA_COMPAT_TREATMENT;
+
// Whether enabling rotation compat policy for immersive apps that prevents auto rotation
// into non-optimal screen orientation while in fullscreen. This is needed because immersive
// apps, such as games, are often not optimized for all orientations and can have a poor UX
@@ -52,6 +68,10 @@
private boolean mIsDisplayRotationImmersiveAppCompatPolicyEnabled =
DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY;
+ // Whether enabling ignoreOrientationRequest is allowed on the device.
+ private boolean mIsAllowIgnoreOrientationRequest =
+ DEFAULT_VALUE_ALLOW_IGNORE_ORIENTATION_REQUEST;
+
// Set of active device configs that need to be updated in
// DeviceConfig.OnPropertiesChangedListener#onPropertiesChanged.
private final ArraySet<String> mActiveDeviceConfigsSet = new ArraySet<>();
@@ -91,8 +111,12 @@
*/
boolean getFlag(String key) {
switch (key) {
+ case KEY_ENABLE_CAMERA_COMPAT_TREATMENT:
+ return mIsCameraCompatTreatmentEnabled;
case KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY:
return mIsDisplayRotationImmersiveAppCompatPolicyEnabled;
+ case KEY_ALLOW_IGNORE_ORIENTATION_REQUEST:
+ return mIsAllowIgnoreOrientationRequest;
default:
throw new AssertionError("Unexpected flag name: " + key);
}
@@ -104,10 +128,18 @@
throw new AssertionError("Haven't found default value for flag: " + key);
}
switch (key) {
+ case KEY_ENABLE_CAMERA_COMPAT_TREATMENT:
+ mIsCameraCompatTreatmentEnabled =
+ getDeviceConfig(key, defaultValue);
+ break;
case KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY:
mIsDisplayRotationImmersiveAppCompatPolicyEnabled =
getDeviceConfig(key, defaultValue);
break;
+ case KEY_ALLOW_IGNORE_ORIENTATION_REQUEST:
+ mIsAllowIgnoreOrientationRequest =
+ getDeviceConfig(key, defaultValue);
+ break;
default:
throw new AssertionError("Unexpected flag name: " + key);
}
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index c5a50ca..9681789 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -21,8 +21,11 @@
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
+import static android.content.pm.ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
@@ -41,6 +44,7 @@
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS;
import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
@@ -136,8 +140,12 @@
private final boolean mIsOverrideToNosensorOrientationEnabled;
// Corresponds to OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE
private final boolean mIsOverrideToReverseLandscapeOrientationEnabled;
+ // Corresponds to OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA
+ private final boolean mIsOverrideOrientationOnlyForCameraEnabled;
// Corresponds to OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION
private final boolean mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled;
+ // Corresponds to OVERRIDE_RESPECT_REQUESTED_ORIENTATION
+ private final boolean mIsOverrideRespectRequestedOrientationEnabled;
// Corresponds to OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION
private final boolean mIsOverrideCameraCompatDisableForceRotationEnabled;
@@ -149,6 +157,9 @@
// Corresponds to OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION
private final boolean mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled;
+ // Corresponds to OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS
+ private final boolean mIsOverrideEnableCompatFakeFocusEnabled;
+
@Nullable
private final Boolean mBooleanPropertyAllowOrientationOverride;
@Nullable
@@ -204,6 +215,9 @@
@Nullable
private final Boolean mBooleanPropertyIgnoreRequestedOrientation;
+ @Nullable
+ private final Boolean mBooleanPropertyFakeFocus;
+
private boolean mIsRelauchingAfterRequestedOrientationChanged;
LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
@@ -218,6 +232,10 @@
readComponentProperty(packageManager, mActivityRecord.packageName,
mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled,
PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+ mBooleanPropertyFakeFocus =
+ readComponentProperty(packageManager, mActivityRecord.packageName,
+ mLetterboxConfiguration::isCompatFakeFocusEnabled,
+ PROPERTY_COMPAT_ENABLE_FAKE_FOCUS);
mBooleanPropertyCameraCompatAllowForceRotation =
readComponentProperty(packageManager, mActivityRecord.packageName,
() -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
@@ -253,8 +271,12 @@
isCompatChangeEnabled(OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE);
mIsOverrideToNosensorOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR);
+ mIsOverrideOrientationOnlyForCameraEnabled =
+ isCompatChangeEnabled(OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA);
mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION);
+ mIsOverrideRespectRequestedOrientationEnabled =
+ isCompatChangeEnabled(OVERRIDE_RESPECT_REQUESTED_ORIENTATION);
mIsOverrideCameraCompatDisableForceRotationEnabled =
isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION);
@@ -265,6 +287,9 @@
mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+
+ mIsOverrideEnableCompatFakeFocusEnabled =
+ isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS);
}
/**
@@ -360,6 +385,25 @@
}
/**
+ * Whether sending compat fake focus for split screen resumed activities is enabled. Needed
+ * because some game engines wait to get focus before drawing the content of the app which isn't
+ * guaranteed by default in multi-window modes.
+ *
+ * <p>This treatment is enabled when the following conditions are met:
+ * <ul>
+ * <li>Flag gating the treatment is enabled
+ * <li>Component property is NOT set to false
+ * <li>Component property is set to true or per-app override is enabled
+ * </ul>
+ */
+ boolean shouldSendFakeFocus() {
+ return shouldEnableWithOverrideAndProperty(
+ /* gatingCondition */ mLetterboxConfiguration::isCompatFakeFocusEnabled,
+ mIsOverrideEnableCompatFakeFocusEnabled,
+ mBooleanPropertyFakeFocus);
+ }
+
+ /**
* Sets whether an activity is relaunching after the app has called {@link
* android.app.Activity#setRequestedOrientation}.
*/
@@ -379,6 +423,10 @@
mIsRefreshAfterRotationRequested = isRequested;
}
+ boolean isOverrideRespectRequestedOrientationEnabled() {
+ return mIsOverrideRespectRequestedOrientationEnabled;
+ }
+
/**
* Whether should fix display orientation to landscape natural orientation when a task is
* fullscreen and the display is ignoring orientation requests.
@@ -410,6 +458,14 @@
return candidate;
}
+ DisplayContent displayContent = mActivityRecord.mDisplayContent;
+ if (mIsOverrideOrientationOnlyForCameraEnabled && displayContent != null
+ && (displayContent.mDisplayRotationCompatPolicy == null
+ || !displayContent.mDisplayRotationCompatPolicy
+ .isActivityEligibleForOrientationOverride(mActivityRecord))) {
+ return candidate;
+ }
+
if (mIsOverrideToReverseLandscapeOrientationEnabled
&& (isFixedOrientationLandscape(candidate) || mIsOverrideAnyOrientationEnabled)) {
Slog.w(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
@@ -439,6 +495,10 @@
return candidate;
}
+ boolean isOverrideOrientationOnlyForCameraEnabled() {
+ return mIsOverrideOrientationOnlyForCameraEnabled;
+ }
+
/**
* Whether activity is eligible for activity "refresh" after camera compat force rotation
* treatment. See {@link DisplayRotationCompatPolicy} for context.
diff --git a/services/core/java/com/android/server/wm/RefreshRatePolicy.java b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
index f3713eb..6b3c533 100644
--- a/services/core/java/com/android/server/wm/RefreshRatePolicy.java
+++ b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
@@ -53,6 +53,8 @@
}
}
+ private final DisplayInfo mDisplayInfo;
+ private final Mode mDefaultMode;
private final Mode mLowRefreshRateMode;
private final PackageRefreshRate mNonHighRefreshRatePackages = new PackageRefreshRate();
private final HighRefreshRateDenylist mHighRefreshRateDenylist;
@@ -83,7 +85,9 @@
RefreshRatePolicy(WindowManagerService wmService, DisplayInfo displayInfo,
HighRefreshRateDenylist denylist) {
- mLowRefreshRateMode = findLowRefreshRateMode(displayInfo);
+ mDisplayInfo = displayInfo;
+ mDefaultMode = displayInfo.getDefaultMode();
+ mLowRefreshRateMode = findLowRefreshRateMode(displayInfo, mDefaultMode);
mHighRefreshRateDenylist = denylist;
mWmService = wmService;
}
@@ -92,10 +96,9 @@
* Finds the mode id with the lowest refresh rate which is >= 60hz and same resolution as the
* default mode.
*/
- private Mode findLowRefreshRateMode(DisplayInfo displayInfo) {
- Mode mode = displayInfo.getDefaultMode();
+ private Mode findLowRefreshRateMode(DisplayInfo displayInfo, Mode defaultMode) {
float[] refreshRates = displayInfo.getDefaultRefreshRates();
- float bestRefreshRate = mode.getRefreshRate();
+ float bestRefreshRate = defaultMode.getRefreshRate();
mMinSupportedRefreshRate = bestRefreshRate;
mMaxSupportedRefreshRate = bestRefreshRate;
for (int i = refreshRates.length - 1; i >= 0; i--) {
@@ -121,13 +124,39 @@
}
int getPreferredModeId(WindowState w) {
- // If app is animating, it's not able to control refresh rate because we want the animation
- // to run in default refresh rate.
- if (w.isAnimating(TRANSITION | PARENTS)) {
+ final int preferredDisplayModeId = w.mAttrs.preferredDisplayModeId;
+ if (preferredDisplayModeId <= 0) {
+ // Unspecified, use default mode.
return 0;
}
- return w.mAttrs.preferredDisplayModeId;
+ // If app is animating, it's not able to control refresh rate because we want the animation
+ // to run in default refresh rate. But if the display size of default mode is different
+ // from the using preferred mode, then still keep the preferred mode to avoid disturbing
+ // the animation.
+ if (w.isAnimating(TRANSITION | PARENTS)) {
+ Display.Mode preferredMode = null;
+ for (Display.Mode mode : mDisplayInfo.supportedModes) {
+ if (preferredDisplayModeId == mode.getModeId()) {
+ preferredMode = mode;
+ break;
+ }
+ }
+ if (preferredMode != null) {
+ final int pW = preferredMode.getPhysicalWidth();
+ final int pH = preferredMode.getPhysicalHeight();
+ if ((pW != mDefaultMode.getPhysicalWidth()
+ || pH != mDefaultMode.getPhysicalHeight())
+ && pW == mDisplayInfo.getNaturalWidth()
+ && pH == mDisplayInfo.getNaturalHeight()) {
+ // Prefer not to change display size when animating.
+ return preferredDisplayModeId;
+ }
+ }
+ return 0;
+ }
+
+ return preferredDisplayModeId;
}
/**
@@ -165,12 +194,9 @@
// of that mode id.
final int preferredModeId = w.mAttrs.preferredDisplayModeId;
if (preferredModeId > 0) {
- DisplayInfo info = w.getDisplayInfo();
- if (info != null) {
- for (Display.Mode mode : info.supportedModes) {
- if (preferredModeId == mode.getModeId()) {
- return mode.getRefreshRate();
- }
+ for (Display.Mode mode : mDisplayInfo.supportedModes) {
+ if (preferredModeId == mode.getModeId()) {
+ return mode.getRefreshRate();
}
}
}
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index fd8b614..ef45c22 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -800,6 +800,10 @@
if (mDisplayContent.getRotationAnimation() == ScreenRotationAnimation.this) {
// It also invokes kill().
mDisplayContent.setRotationAnimation(null);
+ if (mDisplayContent.mDisplayRotationCompatPolicy != null) {
+ mDisplayContent.mDisplayRotationCompatPolicy
+ .onScreenRotationAnimationFinished();
+ }
} else {
kill();
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 5065014..8931d80 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -113,6 +113,7 @@
import static com.android.internal.util.LatencyTracker.ACTION_ROTATE_SCREEN;
import static com.android.server.LockGuard.INDEX_WINDOW;
import static com.android.server.LockGuard.installLock;
+import static com.android.server.policy.PhoneWindowManager.TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
import static com.android.server.wm.DisplayContent.IME_TARGET_CONTROL;
@@ -355,6 +356,7 @@
public class WindowManagerService extends IWindowManager.Stub
implements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs {
private static final String TAG = TAG_WITH_CLASS_NAME ? "WindowManagerService" : TAG_WM;
+ private static final int TRACE_MAX_SECTION_NAME_LENGTH = 127;
static final int LAYOUT_REPEAT_THRESHOLD = 4;
@@ -4170,7 +4172,8 @@
* <p>Note: this assumes that {@link #mGlobalLock} is held by the caller.
*/
boolean isIgnoreOrientationRequestDisabled() {
- return mIsIgnoreOrientationRequestDisabled;
+ return mIsIgnoreOrientationRequestDisabled
+ || !mLetterboxConfiguration.isIgnoreOrientationRequestAllowed();
}
@Override
@@ -5441,10 +5444,15 @@
case WAITING_FOR_DRAWN_TIMEOUT: {
Runnable callback = null;
- final WindowContainer container = (WindowContainer) msg.obj;
+ final WindowContainer<?> container = (WindowContainer<?>) msg.obj;
synchronized (mGlobalLock) {
ProtoLog.w(WM_ERROR, "Timeout waiting for drawn: undrawn=%s",
container.mWaitingForDrawn);
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ for (int i = 0; i < container.mWaitingForDrawn.size(); i++) {
+ traceEndWaitingForWindowDrawn(container.mWaitingForDrawn.get(i));
+ }
+ }
container.mWaitingForDrawn.clear();
callback = mWaitingForDrawnCallbacks.remove(container);
}
@@ -6051,10 +6059,16 @@
// Window has been removed or hidden; no draw will now happen, so stop waiting.
ProtoLog.w(WM_DEBUG_SCREEN_ON, "Aborted waiting for drawn: %s", win);
container.mWaitingForDrawn.remove(win);
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ traceEndWaitingForWindowDrawn(win);
+ }
} else if (win.hasDrawn()) {
// Window is now drawn (and shown).
ProtoLog.d(WM_DEBUG_SCREEN_ON, "Window drawn win=%s", win);
container.mWaitingForDrawn.remove(win);
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ traceEndWaitingForWindowDrawn(win);
+ }
}
}
if (container.mWaitingForDrawn.isEmpty()) {
@@ -6065,6 +6079,22 @@
});
}
+ private void traceStartWaitingForWindowDrawn(WindowState window) {
+ final String traceName = TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD + "#"
+ + window.getWindowTag();
+ final String shortenedTraceName = traceName.substring(0, Math.min(
+ TRACE_MAX_SECTION_NAME_LENGTH, traceName.length()));
+ Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, shortenedTraceName, /* cookie= */ 0);
+ }
+
+ private void traceEndWaitingForWindowDrawn(WindowState window) {
+ final String traceName = TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD + "#"
+ + window.getWindowTag();
+ final String shortenedTraceName = traceName.substring(0, Math.min(
+ TRACE_MAX_SECTION_NAME_LENGTH, traceName.length()));
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER, shortenedTraceName, /* cookie= */ 0);
+ }
+
void requestTraversal() {
mWindowPlacerLocked.requestTraversal();
}
@@ -7799,7 +7829,7 @@
@Override
public void waitForAllWindowsDrawn(Runnable callback, long timeout, int displayId) {
- final WindowContainer container = displayId == INVALID_DISPLAY
+ final WindowContainer<?> container = displayId == INVALID_DISPLAY
? mRoot : mRoot.getDisplayContent(displayId);
if (container == null) {
// The waiting container doesn't exist, no need to wait to run the callback. Run and
@@ -7815,6 +7845,12 @@
if (container.mWaitingForDrawn.isEmpty()) {
allWindowsDrawn = true;
} else {
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ for (int i = 0; i < container.mWaitingForDrawn.size(); i++) {
+ traceStartWaitingForWindowDrawn(container.mWaitingForDrawn.get(i));
+ }
+ }
+
mWaitingForDrawnCallbacks.put(container, callback);
mH.sendNewMessageDelayed(H.WAITING_FOR_DRAWN_TIMEOUT, container, timeout);
checkDrawnWindowsLocked();
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index ae03fbb..45dacbb 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -2387,7 +2387,11 @@
// IME parent may failed to attach to the app during rotating the screen.
// See DisplayContent#shouldImeAttachedToApp, DisplayContent#isImeControlledByApp
if (windowConfigChanged) {
- getDisplayContent().updateImeControlTarget();
+ // If the window was the IME layering target, updates the IME surface parent in case
+ // the IME surface may be wrongly positioned when the window configuration affects the
+ // IME surface association. (e.g. Attach IME surface on the display instead of the
+ // app when the app bounds being letterboxed.)
+ mDisplayContent.updateImeControlTarget(isImeLayeringTarget() /* updateImeParent */);
}
}
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 af39dd4..2cc6bd5 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -2011,6 +2011,74 @@
eq(lightSensorTwo), anyInt(), any(Handler.class));
}
+ @Test
+ public void testAuthenticationPossibleSetsPhysicalRateRangesToMax() throws RemoteException {
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ // don't call director.start(createMockSensorManager());
+ // DisplayObserver will reset mSupportedModesByDisplay
+ director.onBootCompleted();
+ ArgumentCaptor<IUdfpsHbmListener> captor =
+ ArgumentCaptor.forClass(IUdfpsHbmListener.class);
+ verify(mStatusBarMock).setUdfpsHbmListener(captor.capture());
+
+ captor.getValue().onAuthenticationPossible(DISPLAY_ID, true);
+
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
+ assertThat(vote.refreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
+ assertThat(vote.refreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+ }
+
+ @Test
+ public void testAuthenticationPossibleUnsetsVote() throws RemoteException {
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ director.start(createMockSensorManager());
+ director.onBootCompleted();
+ ArgumentCaptor<IUdfpsHbmListener> captor =
+ ArgumentCaptor.forClass(IUdfpsHbmListener.class);
+ verify(mStatusBarMock).setUdfpsHbmListener(captor.capture());
+ captor.getValue().onAuthenticationPossible(DISPLAY_ID, true);
+ captor.getValue().onAuthenticationPossible(DISPLAY_ID, false);
+
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
+ assertNull(vote);
+ }
+
+ @Test
+ public void testUdfpsRequestSetsPhysicalRateRangesToMax() throws RemoteException {
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ // don't call director.start(createMockSensorManager());
+ // DisplayObserver will reset mSupportedModesByDisplay
+ director.onBootCompleted();
+ ArgumentCaptor<IUdfpsHbmListener> captor =
+ ArgumentCaptor.forClass(IUdfpsHbmListener.class);
+ verify(mStatusBarMock).setUdfpsHbmListener(captor.capture());
+
+ captor.getValue().onHbmEnabled(DISPLAY_ID);
+
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_UDFPS);
+ assertThat(vote.refreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
+ assertThat(vote.refreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+ }
+
+ @Test
+ public void testUdfpsRequestUnsetsUnsetsVote() throws RemoteException {
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ director.start(createMockSensorManager());
+ director.onBootCompleted();
+ ArgumentCaptor<IUdfpsHbmListener> captor =
+ ArgumentCaptor.forClass(IUdfpsHbmListener.class);
+ verify(mStatusBarMock).setUdfpsHbmListener(captor.capture());
+ captor.getValue().onHbmEnabled(DISPLAY_ID);
+ captor.getValue().onHbmEnabled(DISPLAY_ID);
+
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_UDFPS);
+ assertNull(vote);
+ }
+
private Temperature getSkinTemp(@Temperature.ThrottlingStatus int status) {
return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status);
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 3f3b052..96e2a09 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -1645,6 +1645,46 @@
}
@Test
+ public void testEnqueueNotificationWithTag_nullAction_fixed() throws Exception {
+ Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .addAction(new Notification.Action.Builder(null, "one", null).build())
+ .addAction(new Notification.Action.Builder(null, "two", null).build())
+ .addAction(new Notification.Action.Builder(null, "three", null).build())
+ .build();
+ n.actions[1] = null;
+
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, 0);
+ waitForIdle();
+
+ StatusBarNotification[] posted = mBinderService.getActiveNotifications(PKG);
+ assertThat(posted).hasLength(1);
+ assertThat(posted[0].getNotification().actions).hasLength(2);
+ assertThat(posted[0].getNotification().actions[0].title.toString()).isEqualTo("one");
+ assertThat(posted[0].getNotification().actions[1].title.toString()).isEqualTo("three");
+ }
+
+ @Test
+ public void testEnqueueNotificationWithTag_allNullActions_fixed() throws Exception {
+ Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .addAction(new Notification.Action.Builder(null, "one", null).build())
+ .addAction(new Notification.Action.Builder(null, "two", null).build())
+ .build();
+ n.actions[0] = null;
+ n.actions[1] = null;
+
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, 0);
+ waitForIdle();
+
+ StatusBarNotification[] posted = mBinderService.getActiveNotifications(PKG);
+ assertThat(posted).hasLength(1);
+ assertThat(posted[0].getNotification().actions).isNull();
+ }
+
+ @Test
public void testCancelNonexistentNotification() throws Exception {
mBinderService.cancelNotificationWithTag(PKG, PKG,
"testCancelNonexistentNotification", 0, 0);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
index 45b30b2..4954e89 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
@@ -30,7 +31,9 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+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;
@@ -58,6 +61,8 @@
import androidx.test.filters.SmallTest;
+import com.android.internal.R;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -128,6 +133,69 @@
}
@Test
+ public void testOpenedCameraInSplitScreen_showToast() {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ spyOn(mTask);
+ spyOn(mDisplayRotationCompatPolicy);
+ doReturn(WINDOWING_MODE_MULTI_WINDOW).when(mActivity).getWindowingMode();
+ doReturn(WINDOWING_MODE_MULTI_WINDOW).when(mTask).getWindowingMode();
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ verify(mDisplayRotationCompatPolicy).showToast(
+ R.string.display_rotation_camera_compat_toast_in_split_screen);
+ }
+
+ @Test
+ public void testOnScreenRotationAnimationFinished_treatmentNotEnabled_doNotShowToast() {
+ when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ anyBoolean()))
+ .thenReturn(false);
+ spyOn(mDisplayRotationCompatPolicy);
+
+ mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+ verify(mDisplayRotationCompatPolicy, never()).showToast(
+ R.string.display_rotation_camera_compat_toast_after_rotation);
+ }
+
+ @Test
+ public void testOnScreenRotationAnimationFinished_noOpenCamera_doNotShowToast() {
+ spyOn(mDisplayRotationCompatPolicy);
+
+ mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+ verify(mDisplayRotationCompatPolicy, never()).showToast(
+ R.string.display_rotation_camera_compat_toast_after_rotation);
+ }
+
+ @Test
+ public void testOnScreenRotationAnimationFinished_notFullscreen_doNotShowToast() {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ doReturn(WINDOWING_MODE_MULTI_WINDOW).when(mActivity).getWindowingMode();
+ spyOn(mDisplayRotationCompatPolicy);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+ verify(mDisplayRotationCompatPolicy, never()).showToast(
+ R.string.display_rotation_camera_compat_toast_after_rotation);
+ }
+
+ @Test
+ public void testOnScreenRotationAnimationFinished_showToast() {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ spyOn(mDisplayRotationCompatPolicy);
+
+ mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+ verify(mDisplayRotationCompatPolicy).showToast(
+ R.string.display_rotation_camera_compat_toast_after_rotation);
+ }
+
+ @Test
public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception {
when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
/* checkDeviceConfig */ anyBoolean()))
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
index ead1a86..12b7c9d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
@@ -51,7 +51,7 @@
* Tests for the {@link LetterboxConfiguration} class.
*
* Build/Install/Run:
- * atest WmTests:LetterboxConfigurationTests
+ * atest WmTests:LetterboxConfigurationTest
*/
@SmallTest
@Presubmit
@@ -243,18 +243,18 @@
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, "true", false);
mLetterboxConfiguration.setIsCompatFakeFocusEnabled(false);
- assertFalse(mLetterboxConfiguration.isCompatFakeFocusEnabledOnDevice());
+ assertFalse(mLetterboxConfiguration.isCompatFakeFocusEnabled());
// Set runtime flag to false and build time flag to true
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, "false", false);
mLetterboxConfiguration.setIsCompatFakeFocusEnabled(true);
- assertFalse(mLetterboxConfiguration.isCompatFakeFocusEnabledOnDevice());
+ assertFalse(mLetterboxConfiguration.isCompatFakeFocusEnabled());
// Set runtime flag to true so that both are enabled
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, "true", false);
- assertTrue(mLetterboxConfiguration.isCompatFakeFocusEnabledOnDevice());
+ assertTrue(mLetterboxConfiguration.isCompatFakeFocusEnabled());
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, Boolean.toString(wasFakeFocusEnabled),
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index c7f19fb..0d20f17 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -20,8 +20,10 @@
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
@@ -37,6 +39,7 @@
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS;
import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -71,6 +74,7 @@
import com.android.internal.R;
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
import org.junit.Before;
@@ -576,6 +580,42 @@
/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_UNSPECIFIED);
}
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
+ OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
+ public void testOverrideOrientationIfNeeded_whenCameraNotActive_returnsUnchanged() {
+ doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(anyBoolean());
+
+ // Recreate DisplayContent with DisplayRotationCompatPolicy
+ mActivity = setUpActivityWithComponent();
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
+ doReturn(false).when(mDisplayContent.mDisplayRotationCompatPolicy)
+ .isActivityEligibleForOrientationOverride(eq(mActivity));
+
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_UNSPECIFIED);
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
+ OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
+ public void testOverrideOrientationIfNeeded_whenCameraActive_returnsPortrait() {
+ doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(anyBoolean());
+
+ // Recreate DisplayContent with DisplayRotationCompatPolicy
+ mActivity = setUpActivityWithComponent();
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
+ doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
+ .isActivityEligibleForOrientationOverride(eq(mActivity));
+
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_PORTRAIT);
+ }
+
// shouldUseDisplayLandscapeNaturalOrientation
@Test
@@ -626,6 +666,72 @@
assertFalse(mController.shouldUseDisplayLandscapeNaturalOrientation());
}
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
+ public void testShouldSendFakeFocus_overrideEnabled_returnsTrue() {
+ doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertTrue(mController.shouldSendFakeFocus());
+ }
+
+ @Test
+ @DisableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
+ public void testShouldSendFakeFocus_overrideDisabled_returnsFalse() {
+ doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertFalse(mController.shouldSendFakeFocus());
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
+ public void testIsCompatFakeFocusEnabled_propertyDisabledAndOverrideEnabled_fakeFocusDisabled()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+ mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ false);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertFalse(mController.shouldSendFakeFocus());
+ }
+
+ @Test
+ @DisableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
+ public void testIsCompatFakeFocusEnabled_propertyEnabled_noOverride_fakeFocusEnabled()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+ mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ true);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertTrue(mController.shouldSendFakeFocus());
+ }
+
+ @Test
+ public void testIsCompatFakeFocusEnabled_propertyDisabled_fakeFocusDisabled()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+ mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ false);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertFalse(mController.shouldSendFakeFocus());
+ }
+
+ @Test
+ public void testIsCompatFakeFocusEnabled_propertyEnabled_fakeFocusEnabled()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+ mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ true);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertTrue(mController.shouldSendFakeFocus());
+ }
+
private void mockThatProperty(String propertyName, boolean value) throws Exception {
Property property = new Property(propertyName, /* value */ value, /* packageName */ "",
/* className */ "");
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index 9d2eb26..63797778 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -21,7 +21,9 @@
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.os.Parcel;
@@ -258,6 +260,14 @@
assertEquals(0, mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+
+ // If there will be display size change when switching from preferred mode to default mode,
+ // then keep the current preferred mode during animating.
+ mDisplayInfo = spy(mDisplayInfo);
+ final Mode defaultMode = new Mode(4321 /* width */, 1234 /* height */, LOW_REFRESH_RATE);
+ doReturn(defaultMode).when(mDisplayInfo).getDefaultMode();
+ mPolicy = new RefreshRatePolicy(mWm, mDisplayInfo, mDenylist);
+ assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 0bcee92..fb5fda1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -16,8 +16,10 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
@@ -53,8 +55,6 @@
import static com.android.server.wm.ActivityRecord.State.RESUMED;
import static com.android.server.wm.ActivityRecord.State.STOPPED;
import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
-import static com.android.server.wm.LetterboxConfiguration.PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN;
-import static com.android.server.wm.LetterboxConfiguration.PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT;
import static com.android.server.wm.WindowContainer.POSITION_TOP;
import static com.google.common.truth.Truth.assertThat;
@@ -1876,6 +1876,43 @@
}
@Test
+ @EnableCompatChanges({ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION})
+ public void testOverrideRespectRequestedOrientationIsEnabled_orientationIsRespected() {
+ // Set up a display in landscape
+ setUpDisplaySizeWithApp(2800, 1400);
+
+ final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */ false,
+ RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT);
+ activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+ // Display should be rotated.
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, activity.mDisplayContent.getOrientation());
+
+ // No size compat mode
+ assertFalse(activity.inSizeCompatMode());
+ }
+
+ @Test
+ @EnableCompatChanges({ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION})
+ public void testOverrideRespectRequestedOrientationIsEnabled_multiWindow_orientationIgnored() {
+ // Set up a display in landscape
+ setUpDisplaySizeWithApp(2800, 1400);
+
+ final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */ false,
+ RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT);
+ TaskFragment taskFragment = activity.getTaskFragment();
+ spyOn(taskFragment);
+ activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ doReturn(WINDOWING_MODE_MULTI_WINDOW).when(taskFragment).getWindowingMode();
+
+ // Display should not be rotated.
+ assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, activity.mDisplayContent.getOrientation());
+
+ // No size compat mode
+ assertFalse(activity.inSizeCompatMode());
+ }
+
+ @Test
public void testSplitAspectRatioForUnresizableLandscapeApps() {
// Set up a display in portrait and ignoring orientation request.
int screenWidth = 1400;
@@ -2313,6 +2350,29 @@
}
@Test
+ public void testDisplayIgnoreOrientationRequest_disabledViaDeviceConfig_orientationRespected() {
+ // Set up a display in landscape
+ setUpDisplaySizeWithApp(2800, 1400);
+
+ final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */ false,
+ RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT);
+ activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+ spyOn(activity.mWmService.mLetterboxConfiguration);
+ doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
+ .isIgnoreOrientationRequestAllowed();
+
+ // Display should not be rotated.
+ assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, activity.mDisplayContent.getOrientation());
+
+ doReturn(false).when(activity.mWmService.mLetterboxConfiguration)
+ .isIgnoreOrientationRequestAllowed();
+
+ // Display should be rotated.
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, activity.mDisplayContent.getOrientation());
+ }
+
+ @Test
public void testSandboxDisplayApis_unresizableAppNotSandboxed() {
// Set up a display in landscape with an unresizable app.
setUpDisplaySizeWithApp(2500, 1000);
@@ -3615,7 +3675,8 @@
assertEquals(newDensity, mActivity.getConfiguration().densityDpi);
}
- private ActivityRecord setUpActivityForCompatFakeFocusTest() {
+ @Test
+ public void testShouldSendFakeFocus_compatFakeFocusEnabled() {
final ActivityRecord activity = new ActivityBuilder(mAtm)
.setCreateTask(true)
.setOnTop(true)
@@ -3624,69 +3685,40 @@
com.android.server.wm.SizeCompatTests.class.getName()))
.build();
final Task task = activity.getTask();
+ spyOn(activity.mLetterboxUiController);
+ doReturn(true).when(activity.mLetterboxUiController).shouldSendFakeFocus();
+
task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
- spyOn(activity.mWmService.mLetterboxConfiguration);
- doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
- .isCompatFakeFocusEnabledOnDevice();
- return activity;
- }
-
- @Test
- @EnableCompatChanges({ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
- public void testShouldSendFakeFocus_overrideEnabled_returnsTrue() {
- ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
-
assertTrue(activity.shouldSendCompatFakeFocus());
- }
- @Test
- @DisableCompatChanges({ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
- public void testShouldSendFakeFocus_overrideDisabled_returnsFalse() {
- ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
+ task.setWindowingMode(WINDOWING_MODE_PINNED);
+ assertFalse(activity.shouldSendCompatFakeFocus());
+ task.setWindowingMode(WINDOWING_MODE_FREEFORM);
assertFalse(activity.shouldSendCompatFakeFocus());
}
@Test
- @EnableCompatChanges({ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
- public void testIsCompatFakeFocusEnabled_optOutPropertyAndOverrideEnabled_fakeFocusDisabled() {
- ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
- doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
- .getPackageManagerProperty(any(), eq(PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT));
+ public void testShouldSendFakeFocus_compatFakeFocusDisabled() {
+ final ActivityRecord activity = new ActivityBuilder(mAtm)
+ .setCreateTask(true)
+ .setOnTop(true)
+ // Set the component to be that of the test class in order to enable compat changes
+ .setComponent(ComponentName.createRelative(mContext,
+ com.android.server.wm.SizeCompatTests.class.getName()))
+ .build();
+ final Task task = activity.getTask();
+ spyOn(activity.mLetterboxUiController);
+ doReturn(false).when(activity.mLetterboxUiController).shouldSendFakeFocus();
- assertFalse(activity.mWmService.mLetterboxConfiguration
- .isCompatFakeFocusEnabled(activity.info));
- }
+ task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+ assertFalse(activity.shouldSendCompatFakeFocus());
- @Test
- @DisableCompatChanges({ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
- public void testIsCompatFakeFocusEnabled_optInPropertyEnabled_noOverride_fakeFocusEnabled() {
- ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
- doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
- .getPackageManagerProperty(any(), eq(PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN));
+ task.setWindowingMode(WINDOWING_MODE_PINNED);
+ assertFalse(activity.shouldSendCompatFakeFocus());
- assertTrue(activity.mWmService.mLetterboxConfiguration
- .isCompatFakeFocusEnabled(activity.info));
- }
-
- @Test
- public void testIsCompatFakeFocusEnabled_optOutPropertyEnabled_fakeFocusDisabled() {
- ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
- doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
- .getPackageManagerProperty(any(), eq(PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT));
-
- assertFalse(activity.mWmService.mLetterboxConfiguration
- .isCompatFakeFocusEnabled(activity.info));
- }
-
- @Test
- public void testIsCompatFakeFocusEnabled_optInPropertyEnabled_fakeFocusEnabled() {
- ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
- doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
- .getPackageManagerProperty(any(), eq(PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN));
-
- assertTrue(activity.mWmService.mLetterboxConfiguration
- .isCompatFakeFocusEnabled(activity.info));
+ task.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ assertFalse(activity.shouldSendCompatFakeFocus());
}
private int getExpectedSplitSize(int dimensionToSplit) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index fd3776f..514aec1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -1138,7 +1138,9 @@
spyOn(app.getDisplayContent());
app.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
- verify(app.getDisplayContent()).updateImeControlTarget();
+ // Expect updateImeParent will be invoked when the configuration of the IME control
+ // target has changed.
+ verify(app.getDisplayContent()).updateImeControlTarget(eq(true) /* updateImeParent */);
assertEquals(mAppWindow, mDisplayContent.getImeTarget(IME_TARGET_CONTROL).getWindow());
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
index aacc17a4..3361502 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
@@ -113,4 +113,18 @@
}
return false
}
+
+ fun toggleFixPortraitOrientation(wmHelper: WindowManagerStateHelper) {
+ val button = uiDevice.wait(Until.findObject(By.res(getPackage(),
+ "toggle_fixed_portrait_btn")), FIND_TIMEOUT)
+ require(button != null) {
+ "Button not found, this usually happens when the device " +
+ "was left in an unknown state (e.g. Screen turned off)"
+ }
+ button.click()
+ mInstrumentation.waitForIdleSync()
+ // Ensure app relaunching transition finish and the IME has shown
+ wmHelper.waitForAppTransitionIdle()
+ wmHelper.waitImeShown()
+ }
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
new file mode 100644
index 0000000..3b3bce6
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.wm.flicker.ime
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.view.Surface
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.RequiresDevice
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group2
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.traces.region.RegionSubject
+import com.android.server.wm.traces.common.FlickerComponentName
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test IME window shown on the app with fixing portrait orientation.
+ * To run this test: `atest FlickerTests:OpenImeWindowToFixedPortraitAppTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group2
+class OpenImeWindowToFixedPortraitAppTest (private val testSpec: FlickerTestParameter) {
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
+
+ @FlickerBuilderProvider
+ fun buildFlicker(): FlickerBuilder {
+ return FlickerBuilder(instrumentation).apply {
+ setup {
+ eachRun {
+ testApp.launchViaIntent(wmHelper)
+ testApp.openIME(device, wmHelper)
+ // Enable letterbox when the app calls setRequestedOrientation
+ device.executeShellCommand("cmd window set-ignore-orientation-request true")
+ }
+ }
+ transitions {
+ testApp.toggleFixPortraitOrientation(wmHelper)
+ }
+ teardown {
+ eachRun {
+ testApp.exit()
+ device.executeShellCommand("cmd window set-ignore-orientation-request false")
+ }
+ }
+ }
+ }
+
+ @Postsubmit
+ @Test
+ fun imeLayerVisibleStart() {
+ testSpec.assertLayersStart {
+ this.isVisible(FlickerComponentName.IME)
+ }
+ }
+
+ @Postsubmit
+ @Test
+ fun imeLayerExistsEnd() {
+ testSpec.assertLayersEnd {
+ this.isVisible(FlickerComponentName.IME)
+ }
+ }
+
+ @Postsubmit
+ @Test
+ fun imeLayerVisibleRegionKeepsTheSame() {
+ var imeLayerVisibleRegionBeforeTransition: RegionSubject? = null
+ testSpec.assertLayersStart {
+ imeLayerVisibleRegionBeforeTransition = this.visibleRegion(FlickerComponentName.IME)
+ }
+ testSpec.assertLayersEnd {
+ this.visibleRegion(FlickerComponentName.IME)
+ .coversExactly(imeLayerVisibleRegionBeforeTransition!!.region)
+ }
+ }
+
+ @Postsubmit
+ @Test
+ fun appWindowWithLetterboxCoversExactlyOnScreen() {
+ val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation)
+ testSpec.assertLayersEnd {
+ this.visibleRegion(testApp.component, FlickerComponentName.LETTERBOX)
+ .coversExactly(displayBounds)
+ }
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTestParameter> {
+ return FlickerTestParameterFactory.getInstance()
+ .getConfigNonRotationTests(
+ supportedRotations = listOf(Surface.ROTATION_90, Surface.ROTATION_270),
+ supportedNavigationModes = listOf(
+ WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
+ WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index b8ef195..efd80f2 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -45,7 +45,7 @@
android:theme="@style/CutoutShortEdges"
android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivityAutoFocus"
android:windowSoftInputMode="stateVisible"
- android:configChanges="orientation|screenSize"
+ android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:label="ImeAppAutoFocus"
android:exported="true">
<intent-filter>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
index baaf707..e71fe80 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
@@ -26,14 +26,27 @@
android:layout_width="match_parent"
android:imeOptions="flagNoExtractUi"
android:inputType="text"/>
- <Button
- android:id="@+id/finish_activity_btn"
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="Finish activity" />
- <Button
- android:id="@+id/start_dialog_themed_activity_btn"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="Start dialog themed activity" />
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+ <Button
+ android:id="@+id/finish_activity_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Finish activity" />
+ <Button
+ android:id="@+id/start_dialog_themed_activity_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Dialog activity" />
+ <ToggleButton
+ android:id="@+id/toggle_fixed_portrait_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textOn="Portrait (On)"
+ android:textOff="Portrait (Off)"
+ />
+ </LinearLayout>
</LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
index bb200f1..7ee8deb 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
@@ -16,21 +16,29 @@
package com.android.server.wm.flicker.testapp;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
import android.content.Intent;
import android.widget.Button;
import android.widget.EditText;
+import android.widget.ToggleButton;
public class ImeActivityAutoFocus extends ImeActivity {
-
@Override
protected void onStart() {
super.onStart();
- EditText editTextField = findViewById(R.id.plain_text_input);
- editTextField.requestFocus();
-
Button startThemedActivityButton = findViewById(R.id.start_dialog_themed_activity_btn);
startThemedActivityButton.setOnClickListener(
button -> startActivity(new Intent(this, DialogThemedActivity.class)));
+
+ ToggleButton toggleFixedPortraitButton = findViewById(R.id.toggle_fixed_portrait_btn);
+ toggleFixedPortraitButton.setOnCheckedChangeListener(
+ (button, isChecked) -> setRequestedOrientation(
+ isChecked ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_UNSPECIFIED));
+
+ EditText editTextField = findViewById(R.id.plain_text_input);
+ editTextField.requestFocus();
}
}