Merge "Update split screen divider to match new taskbar color" into sc-v2-dev
diff --git a/core/api/current.txt b/core/api/current.txt
index 3aabe8d..8ba8c1a 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -46892,8 +46892,15 @@
}
@UiThread public interface AttachedSurfaceControl {
+ method public default void addOnSurfaceTransformHintChangedListener(@NonNull android.view.AttachedSurfaceControl.OnSurfaceTransformHintChangedListener);
method public boolean applyTransactionOnDraw(@NonNull android.view.SurfaceControl.Transaction);
method @Nullable public android.view.SurfaceControl.Transaction buildReparentTransaction(@NonNull android.view.SurfaceControl);
+ method public default int getSurfaceTransformHint();
+ method public default void removeOnSurfaceTransformHintChangedListener(@NonNull android.view.AttachedSurfaceControl.OnSurfaceTransformHintChangedListener);
+ }
+
+ @UiThread public static interface AttachedSurfaceControl.OnSurfaceTransformHintChangedListener {
+ method public void onSurfaceTransformHintChanged(int);
}
public final class Choreographer {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index dd9031a..1d406a5 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -14450,6 +14450,7 @@
method public int getFlags();
method @Nullable public android.view.contentcapture.ContentCaptureSessionId getParentSessionId();
method public int getTaskId();
+ method @Nullable public android.os.IBinder getWindowToken();
field public static final int FLAG_DISABLED_BY_APP = 1; // 0x1
field public static final int FLAG_DISABLED_BY_FLAG_SECURE = 2; // 0x2
field public static final int FLAG_RECONNECTED = 4; // 0x4
@@ -14457,6 +14458,7 @@
public final class ContentCaptureEvent implements android.os.Parcelable {
method public int describeContents();
+ method @Nullable public android.graphics.Rect getBounds();
method @Nullable public android.view.contentcapture.ContentCaptureContext getContentCaptureContext();
method public long getEventTime();
method @Nullable public android.view.autofill.AutofillId getId();
@@ -14476,6 +14478,7 @@
field public static final int TYPE_VIEW_TEXT_CHANGED = 3; // 0x3
field public static final int TYPE_VIEW_TREE_APPEARED = 5; // 0x5
field public static final int TYPE_VIEW_TREE_APPEARING = 4; // 0x4
+ field public static final int TYPE_WINDOW_BOUNDS_CHANGED = 10; // 0xa
}
public final class ContentCaptureManager {
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 2e22b92..80554d7 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -3616,6 +3616,11 @@
}
activity.mLaunchedFromBubble = r.mLaunchedFromBubble;
activity.mCalled = false;
+ // Assigning the activity to the record before calling onCreate() allows
+ // ActivityThread#getActivity() lookup for the callbacks triggered from
+ // ActivityLifecycleCallbacks#onActivityCreated() or
+ // ActivityLifecycleCallback#onActivityPostCreated().
+ r.activity = activity;
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
@@ -3626,7 +3631,6 @@
"Activity " + r.intent.getComponent().toShortString() +
" did not call through to super.onCreate()");
}
- r.activity = activity;
mLastReportedWindowingMode.put(activity.getActivityToken(),
config.windowConfiguration.getWindowingMode());
}
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index d48d562..a3d595c 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -146,6 +146,7 @@
private CryptoObject mCryptoObject;
@Nullable private RemoveTracker mRemoveTracker;
private Handler mHandler;
+ @Nullable private float[] mEnrollStageThresholds;
/**
* Retrieves a list of properties for all fingerprint sensors on the device.
@@ -1329,6 +1330,46 @@
/**
* @hide
*/
+ public int getEnrollStageCount() {
+ if (mEnrollStageThresholds == null) {
+ mEnrollStageThresholds = createEnrollStageThresholds(mContext);
+ }
+ return mEnrollStageThresholds.length + 1;
+ }
+
+ /**
+ * @hide
+ */
+ public float getEnrollStageThreshold(int index) {
+ if (mEnrollStageThresholds == null) {
+ mEnrollStageThresholds = createEnrollStageThresholds(mContext);
+ }
+
+ if (index < 0 || index > mEnrollStageThresholds.length) {
+ Slog.w(TAG, "Unsupported enroll stage index: " + index);
+ return index < 0 ? 0f : 1f;
+ }
+
+ // The implicit threshold for the final stage is always 1.
+ return index == mEnrollStageThresholds.length ? 1f : mEnrollStageThresholds[index];
+ }
+
+ @NonNull
+ private static float[] createEnrollStageThresholds(@NonNull Context context) {
+ // TODO(b/200604947): Fetch this value from FingerprintService, rather than internal config
+ final String[] enrollStageThresholdStrings = context.getResources().getStringArray(
+ com.android.internal.R.array.config_udfps_enroll_stage_thresholds);
+
+ final float[] enrollStageThresholds = new float[enrollStageThresholdStrings.length];
+ for (int i = 0; i < enrollStageThresholds.length; i++) {
+ enrollStageThresholds[i] = Float.parseFloat(enrollStageThresholdStrings[i]);
+ }
+ return enrollStageThresholds;
+ }
+
+ /**
+ * @hide
+ */
public static String getErrorString(Context context, int errMsg, int vendorCode) {
switch (errMsg) {
case FINGERPRINT_ERROR_HW_UNAVAILABLE:
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 0899ef5..1bdfdc2 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8966,6 +8966,15 @@
public static final String UNSAFE_VOLUME_MUSIC_ACTIVE_MS = "unsafe_volume_music_active_ms";
/**
+ * Indicates whether the spatial audio feature was enabled for this user.
+ *
+ * Type : int (0 disabled, 1 enabled)
+ *
+ * @hide
+ */
+ public static final String SPATIAL_AUDIO_ENABLED = "spatial_audio_enabled";
+
+ /**
* Indicates whether notification display on the lock screen is enabled.
* <p>
* Type: int (0 for false, 1 for true)
diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java
index bcc5b56..b2fc9a0 100644
--- a/core/java/android/view/AttachedSurfaceControl.java
+++ b/core/java/android/view/AttachedSurfaceControl.java
@@ -53,4 +53,72 @@
* to the View hierarchy you may need to call {@link android.view.View#invalidate}
*/
boolean applyTransactionOnDraw(@NonNull SurfaceControl.Transaction t);
+
+ /**
+ * The transform hint can be used by a buffer producer to pre-rotate the rendering such that the
+ * final transformation in the system composer is identity. This can be very useful when used in
+ * conjunction with the h/w composer HAL in situations where it cannot handle rotations or
+ * handle them with an additional power cost.
+ *
+ * The transform hint should be used with ASurfaceControl APIs when submitting buffers.
+ * Example usage:
+ *
+ * 1. After a configuration change, before dequeuing a buffer, the buffer producer queries the
+ * function for the transform hint.
+ *
+ * 2. The desired buffer width and height is rotated by the transform hint.
+ *
+ * 3. The producer dequeues a buffer of the new pre-rotated size.
+ *
+ * 4. The producer renders to the buffer such that the image is already transformed, that is
+ * applying the transform hint to the rendering.
+ *
+ * 5. The producer applies the inverse transform hint to the buffer it just rendered.
+ *
+ * 6. The producer queues the pre-transformed buffer with the buffer transform.
+ *
+ * 7. The composer combines the buffer transform with the display transform. If the buffer
+ * transform happens to cancel out the display transform then no rotation is needed and there
+ * will be no performance penalties.
+ *
+ * Note, when using ANativeWindow APIs in conjunction with a NativeActivity Surface or
+ * SurfaceView Surface, the buffer producer will already have access to the transform hint and
+ * no additional work is needed.
+ */
+ default @Surface.Rotation int getSurfaceTransformHint() {
+ return Surface.ROTATION_0;
+ }
+
+ /**
+ * Surface transform hint change listener.
+ * @see #getSurfaceTransformHint
+ */
+ @UiThread
+ interface OnSurfaceTransformHintChangedListener {
+ /**
+ * @param hint new surface transform hint
+ * @see #getSurfaceTransformHint
+ */
+ void onSurfaceTransformHintChanged(@Surface.Rotation int hint);
+ }
+
+ /**
+ * Registers a surface transform hint changed listener to receive notifications about when
+ * the transform hint changes.
+ *
+ * @see #getSurfaceTransformHint
+ * @see #removeOnSurfaceTransformHintChangedListener
+ */
+ default void addOnSurfaceTransformHintChangedListener(
+ @NonNull OnSurfaceTransformHintChangedListener listener) {
+ }
+
+ /**
+ * Unregisters a surface transform hint changed listener.
+ *
+ * @see #addOnSurfaceTransformHintChangedListener
+ */
+ default void removeOnSurfaceTransformHintChangedListener(
+ @NonNull OnSurfaceTransformHintChangedListener listener) {
+ }
}
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index b1fd0be..5b8dc40 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -159,6 +159,7 @@
private static native boolean nativeGetAnimationFrameStats(WindowAnimationFrameStats outStats);
private static native long[] nativeGetPhysicalDisplayIds();
+ private static native long nativeGetPrimaryPhysicalDisplayId();
private static native IBinder nativeGetPhysicalDisplayToken(long physicalDisplayId);
private static native IBinder nativeCreateDisplay(String name, boolean secure);
private static native void nativeDestroyDisplay(IBinder displayToken);
@@ -2279,6 +2280,15 @@
}
/**
+ * Exposed to identify the correct display to apply the primary display orientation. Avoid using
+ * for any other purpose.
+ * @hide
+ */
+ public static long getPrimaryPhysicalDisplayId() {
+ return nativeGetPrimaryPhysicalDisplayId();
+ }
+
+ /**
* @hide
*/
public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 4c36fc9..e38b883 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -312,6 +312,9 @@
static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList<>();
static boolean sFirstDrawComplete = false;
+ private ArrayList<OnSurfaceTransformHintChangedListener> mTransformHintListeners =
+ new ArrayList<>();
+ private @Surface.Rotation int mPreviousTransformHint = Surface.ROTATION_0;
/**
* Callback for notifying about global configuration changes.
*/
@@ -4273,6 +4276,14 @@
try {
if (!isContentCaptureEnabled()) return;
+ // Initial dispatch of window bounds to content capture
+ if (mAttachInfo.mContentCaptureManager != null) {
+ MainContentCaptureSession session =
+ mAttachInfo.mContentCaptureManager.getMainContentCaptureSession();
+ session.notifyWindowBoundsChanged(session.getId(),
+ getConfiguration().windowConfiguration.getBounds());
+ }
+
// Content capture is a go!
rootView.dispatchInitialProvideContentCaptureStructure();
} finally {
@@ -7804,6 +7815,14 @@
insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets,
mTempControls, mSurfaceSize);
+
+ if (mAttachInfo.mContentCaptureManager != null) {
+ MainContentCaptureSession mainSession = mAttachInfo.mContentCaptureManager
+ .getMainContentCaptureSession();
+ mainSession.notifyWindowBoundsChanged(mainSession.getId(),
+ getConfiguration().windowConfiguration.getBounds());
+ }
+
mPendingBackDropFrame.set(mTmpFrames.backdropFrame);
if (mSurfaceControl.isValid()) {
if (!useBLAST()) {
@@ -7824,6 +7843,11 @@
}
mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl);
}
+ int transformHint = mSurfaceControl.getTransformHint();
+ if (mPreviousTransformHint != transformHint) {
+ mPreviousTransformHint = transformHint;
+ dispatchTransformHintChanged(transformHint);
+ }
} else {
destroySurface();
}
@@ -10447,7 +10471,39 @@
return true;
}
- int getSurfaceTransformHint() {
+ @Override
+ public @Surface.Rotation int getSurfaceTransformHint() {
return mSurfaceControl.getTransformHint();
}
+
+ @Override
+ public void addOnSurfaceTransformHintChangedListener(
+ OnSurfaceTransformHintChangedListener listener) {
+ Objects.requireNonNull(listener);
+ if (mTransformHintListeners.contains(listener)) {
+ throw new IllegalArgumentException(
+ "attempt to call addOnSurfaceTransformHintChangedListener() "
+ + "with a previously registered listener");
+ }
+ mTransformHintListeners.add(listener);
+ }
+
+ @Override
+ public void removeOnSurfaceTransformHintChangedListener(
+ OnSurfaceTransformHintChangedListener listener) {
+ Objects.requireNonNull(listener);
+ mTransformHintListeners.remove(listener);
+ }
+
+ private void dispatchTransformHintChanged(@Surface.Rotation int hint) {
+ if (mTransformHintListeners.isEmpty()) {
+ return;
+ }
+ ArrayList<OnSurfaceTransformHintChangedListener> listeners =
+ (ArrayList<OnSurfaceTransformHintChangedListener>) mTransformHintListeners.clone();
+ for (int i = 0; i < listeners.size(); i++) {
+ OnSurfaceTransformHintChangedListener listener = listeners.get(i);
+ listener.onSurfaceTransformHintChanged(hint);
+ }
+ }
}
diff --git a/core/java/android/view/contentcapture/ContentCaptureContext.java b/core/java/android/view/contentcapture/ContentCaptureContext.java
index 9998fbc..0da54e5 100644
--- a/core/java/android/view/contentcapture/ContentCaptureContext.java
+++ b/core/java/android/view/contentcapture/ContentCaptureContext.java
@@ -27,6 +27,7 @@
import android.content.Context;
import android.content.LocusId;
import android.os.Bundle;
+import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.Display;
@@ -105,6 +106,7 @@
private final int mFlags;
private final int mDisplayId;
private final ActivityId mActivityId;
+ private final IBinder mWindowToken;
// Fields below are set by the service upon "delivery" and are not marshalled in the parcel
private int mParentSessionId = NO_SESSION_ID;
@@ -112,7 +114,7 @@
/** @hide */
public ContentCaptureContext(@Nullable ContentCaptureContext clientContext,
@NonNull ActivityId activityId, @NonNull ComponentName componentName, int displayId,
- int flags) {
+ IBinder windowToken, int flags) {
if (clientContext != null) {
mHasClientContext = true;
mExtras = clientContext.mExtras;
@@ -126,6 +128,7 @@
mFlags = flags;
mDisplayId = displayId;
mActivityId = activityId;
+ mWindowToken = windowToken;
}
private ContentCaptureContext(@NonNull Builder builder) {
@@ -137,6 +140,7 @@
mFlags = 0;
mDisplayId = Display.INVALID_DISPLAY;
mActivityId = null;
+ mWindowToken = null;
}
/** @hide */
@@ -148,6 +152,7 @@
mFlags = original.mFlags | extraFlags;
mDisplayId = original.mDisplayId;
mActivityId = original.mActivityId;
+ mWindowToken = original.mWindowToken;
}
/**
@@ -230,6 +235,20 @@
}
/**
+ * Gets the window token of the activity associated with this context.
+ *
+ * <p>The token can be used to attach relevant overlay views to the activity's window. This can
+ * be done through {@link android.view.WindowManager.LayoutParams#token}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @Nullable
+ public IBinder getWindowToken() {
+ return mWindowToken;
+ }
+
+ /**
* Gets the flags associated with this context.
*
* @return any combination of {@link #FLAG_DISABLED_BY_FLAG_SECURE},
@@ -328,6 +347,7 @@
}
pw.print(", activityId="); pw.print(mActivityId);
pw.print(", displayId="); pw.print(mDisplayId);
+ pw.print(", windowToken="); pw.print(mWindowToken);
if (mParentSessionId != NO_SESSION_ID) {
pw.print(", parentId="); pw.print(mParentSessionId);
}
@@ -352,6 +372,7 @@
builder.append("act=").append(ComponentName.flattenToShortString(mComponentName))
.append(", activityId=").append(mActivityId)
.append(", displayId=").append(mDisplayId)
+ .append(", windowToken=").append(mWindowToken)
.append(", flags=").append(mFlags);
} else {
builder.append("id=").append(mId);
@@ -381,6 +402,7 @@
parcel.writeParcelable(mComponentName, flags);
if (fromServer()) {
parcel.writeInt(mDisplayId);
+ parcel.writeStrongBinder(mWindowToken);
parcel.writeInt(mFlags);
mActivityId.writeToParcel(parcel, flags);
}
@@ -411,11 +433,12 @@
return clientContext;
} else {
final int displayId = parcel.readInt();
+ final IBinder windowToken = parcel.readStrongBinder();
final int flags = parcel.readInt();
final ActivityId activityId = new ActivityId(parcel);
return new ContentCaptureContext(clientContext, activityId, componentName,
- displayId, flags);
+ displayId, windowToken, flags);
}
}
diff --git a/core/java/android/view/contentcapture/ContentCaptureEvent.java b/core/java/android/view/contentcapture/ContentCaptureEvent.java
index ce6d034..4b2d3a9 100644
--- a/core/java/android/view/contentcapture/ContentCaptureEvent.java
+++ b/core/java/android/view/contentcapture/ContentCaptureEvent.java
@@ -24,6 +24,7 @@
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.graphics.Insets;
+import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.Selection;
@@ -122,6 +123,12 @@
*/
public static final int TYPE_VIEW_INSETS_CHANGED = 9;
+ /**
+ * Called before {@link #TYPE_VIEW_TREE_APPEARING}, or after the size of the window containing
+ * the views changed.
+ */
+ public static final int TYPE_WINDOW_BOUNDS_CHANGED = 10;
+
/** @hide */
@IntDef(prefix = { "TYPE_" }, value = {
TYPE_VIEW_APPEARED,
@@ -132,7 +139,8 @@
TYPE_CONTEXT_UPDATED,
TYPE_SESSION_PAUSED,
TYPE_SESSION_RESUMED,
- TYPE_VIEW_INSETS_CHANGED
+ TYPE_VIEW_INSETS_CHANGED,
+ TYPE_WINDOW_BOUNDS_CHANGED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface EventType{}
@@ -150,6 +158,7 @@
private int mParentSessionId = NO_SESSION_ID;
private @Nullable ContentCaptureContext mClientContext;
private @Nullable Insets mInsets;
+ private @Nullable Rect mBounds;
private int mComposingStart = MAX_INVALID_VALUE;
private int mComposingEnd = MAX_INVALID_VALUE;
@@ -346,6 +355,13 @@
return this;
}
+ /** @hide */
+ @NonNull
+ public ContentCaptureEvent setBounds(@NonNull Rect bounds) {
+ mBounds = bounds;
+ return this;
+ }
+
/**
* Gets the type of the event.
*
@@ -419,6 +435,16 @@
}
/**
+ * Gets the {@link Rect} bounds of the window associated with the event. Valid bounds will only
+ * be returned if the type of the event is {@link #TYPE_WINDOW_BOUNDS_CHANGED}, otherwise they
+ * will be null.
+ */
+ @Nullable
+ public Rect getBounds() {
+ return mBounds;
+ }
+
+ /**
* Merges event of the same type, either {@link #TYPE_VIEW_TEXT_CHANGED}
* or {@link #TYPE_VIEW_DISAPPEARED}.
*
@@ -489,6 +515,9 @@
if (mInsets != null) {
pw.print(", insets="); pw.println(mInsets);
}
+ if (mBounds != null) {
+ pw.print(", bounds="); pw.println(mBounds);
+ }
if (mComposingStart > MAX_INVALID_VALUE) {
pw.print(", composing("); pw.print(mComposingStart);
pw.print(", "); pw.print(mComposingEnd); pw.print(")");
@@ -533,6 +562,9 @@
if (mInsets != null) {
string.append(", insets=").append(mInsets);
}
+ if (mBounds != null) {
+ string.append(", bounds=").append(mBounds);
+ }
if (mComposingStart > MAX_INVALID_VALUE) {
string.append(", composing=[")
.append(mComposingStart).append(",").append(mComposingEnd).append("]");
@@ -568,6 +600,9 @@
if (mType == TYPE_VIEW_INSETS_CHANGED) {
parcel.writeParcelable(mInsets, flags);
}
+ if (mType == TYPE_WINDOW_BOUNDS_CHANGED) {
+ parcel.writeParcelable(mBounds, flags);
+ }
if (mType == TYPE_VIEW_TEXT_CHANGED) {
parcel.writeInt(mComposingStart);
parcel.writeInt(mComposingEnd);
@@ -608,6 +643,9 @@
if (type == TYPE_VIEW_INSETS_CHANGED) {
event.setInsets(parcel.readParcelable(null));
}
+ if (type == TYPE_WINDOW_BOUNDS_CHANGED) {
+ event.setBounds(parcel.readParcelable(null));
+ }
if (type == TYPE_VIEW_TEXT_CHANGED) {
event.setComposingIndex(parcel.readInt(), parcel.readInt());
event.restoreComposingSpan();
@@ -649,6 +687,8 @@
return "CONTEXT_UPDATED";
case TYPE_VIEW_INSETS_CHANGED:
return "VIEW_INSETS_CHANGED";
+ case TYPE_WINDOW_BOUNDS_CHANGED:
+ return "TYPE_WINDOW_BOUNDS_CHANGED";
default:
return "UKNOWN_TYPE: " + type;
}
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index 4cf5532..98ef4e7 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -26,6 +26,7 @@
import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_WINDOW_BOUNDS_CHANGED;
import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString;
import static android.view.contentcapture.ContentCaptureHelper.sDebug;
import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
@@ -38,6 +39,7 @@
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.graphics.Insets;
+import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -776,6 +778,14 @@
.setClientContext(context), FORCE_FLUSH));
}
+ /** public because is also used by ViewRootImpl */
+ public void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds) {
+ mHandler.post(() -> sendEvent(
+ new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED)
+ .setBounds(bounds)
+ ));
+ }
+
@Override
void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
super.dump(prefix, pw);
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 4253a1b..5ce43df 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -898,6 +898,12 @@
return array;
}
+static jlong nativeGetPrimaryPhysicalDisplayId(JNIEnv* env, jclass clazz) {
+ PhysicalDisplayId displayId;
+ SurfaceComposerClient::getPrimaryPhysicalDisplayId(&displayId);
+ return static_cast<jlong>(displayId.value);
+}
+
static jobject nativeGetPhysicalDisplayToken(JNIEnv* env, jclass clazz, jlong physicalDisplayId) {
sp<IBinder> token =
SurfaceComposerClient::getPhysicalDisplayToken(PhysicalDisplayId(physicalDisplayId));
@@ -1899,6 +1905,8 @@
(void*)nativeReleaseFrameRateFlexibilityToken },
{"nativeGetPhysicalDisplayIds", "()[J",
(void*)nativeGetPhysicalDisplayIds },
+ {"nativeGetPrimaryPhysicalDisplayId", "()J",
+ (void*)nativeGetPrimaryPhysicalDisplayId },
{"nativeGetPhysicalDisplayToken", "(J)Landroid/os/IBinder;",
(void*)nativeGetPhysicalDisplayToken },
{"nativeCreateDisplay", "(Ljava/lang/String;Z)Landroid/os/IBinder;",
diff --git a/core/res/res/drawable-nodpi/default_wallpaper.png b/core/res/res/drawable-nodpi/default_wallpaper.png
index 490ebee..5152972 100644
--- a/core/res/res/drawable-nodpi/default_wallpaper.png
+++ b/core/res/res/drawable-nodpi/default_wallpaper.png
Binary files differ
diff --git a/core/res/res/drawable-sw600dp-nodpi/default_wallpaper.png b/core/res/res/drawable-sw600dp-nodpi/default_wallpaper.png
new file mode 100644
index 0000000..26376fb
--- /dev/null
+++ b/core/res/res/drawable-sw600dp-nodpi/default_wallpaper.png
Binary files differ
diff --git a/core/res/res/drawable-sw720dp-nodpi/default_wallpaper.png b/core/res/res/drawable-sw720dp-nodpi/default_wallpaper.png
new file mode 100644
index 0000000..490ebee
--- /dev/null
+++ b/core/res/res/drawable-sw720dp-nodpi/default_wallpaper.png
Binary files differ
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 985d810..e3e5f84 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4601,6 +4601,13 @@
<!-- Indicates whether device has a power button fingerprint sensor. -->
<bool name="config_is_powerbutton_fps" translatable="false" >false</bool>
+ <!-- When each intermediate UDFPS enroll stage ends, as a fraction of total progress. -->
+ <string-array name="config_udfps_enroll_stage_thresholds" translatable="false">
+ <item>0.25</item>
+ <item>0.5</item>
+ <item>0.75</item>
+ </string-array>
+
<!-- Messages that should not be shown to the user during face auth enrollment. This should be
used to hide messages that may be too chatty or messages that the user can't do much about.
Entries are defined in android.hardware.biometrics.face@1.0 types.hal -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 97d5e28..b6c7659 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2613,6 +2613,7 @@
<java-symbol type="array" name="config_sfps_sensor_props" />
<java-symbol type="integer" name="config_udfps_illumination_transition_ms" />
<java-symbol type="bool" name="config_is_powerbutton_fps" />
+ <java-symbol type="array" name="config_udfps_enroll_stage_thresholds" />
<java-symbol type="array" name="config_face_acquire_enroll_ignorelist" />
<java-symbol type="array" name="config_face_acquire_vendor_enroll_ignorelist" />
@@ -4418,7 +4419,7 @@
<java-symbol type="string" name="view_and_control_notification_title" />
<java-symbol type="string" name="view_and_control_notification_content" />
<java-symbol type="array" name="config_accessibility_allowed_install_source" />
-
+
<!-- Translation -->
<java-symbol type="string" name="ui_translation_accessibility_translated_text" />
<java-symbol type="string" name="ui_translation_accessibility_translation_finished" />
diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureContextTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureContextTest.java
index ddb6729..4b19391 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureContextTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureContextTest.java
@@ -39,9 +39,10 @@
public void testConstructorAdditionalFlags() {
final ComponentName componentName = new ComponentName("component", "name");
final IBinder token = new Binder();
+ final IBinder windowToken = new Binder();
final ContentCaptureContext ctx = new ContentCaptureContext(/* clientContext= */ null,
new ActivityId(/* taskId= */ 666, token), componentName, /* displayId= */
- 42, /* flags= */ 1);
+ 42, windowToken, /* flags= */ 1);
final ContentCaptureContext newCtx = new ContentCaptureContext(ctx, /* extraFlags= */ 2);
assertThat(newCtx.getFlags()).isEqualTo(3);
assertThat(newCtx.getActivityComponent()).isEqualTo(componentName);
@@ -50,6 +51,7 @@
assertThat(activityId.getTaskId()).isEqualTo(666);
assertThat(activityId.getToken()).isEqualTo(token);
assertThat(newCtx.getDisplayId()).isEqualTo(42);
+ assertThat(newCtx.getWindowToken()).isEqualTo(windowToken);
assertThat(newCtx.getExtras()).isNull();
assertThat(newCtx.getLocusId()).isNull();
assertThat(newCtx.getParentSessionId()).isNull();
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 119c9391..6adac13 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -103,12 +103,6 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/TaskFragment.java"
},
- "-2006946193": {
- "message": "setClientVisible: %s clientVisible=%b Callers=%s",
- "level": "VERBOSE",
- "group": "WM_DEBUG_APP_TRANSITIONS",
- "at": "com\/android\/server\/wm\/ActivityRecord.java"
- },
"-2002500255": {
"message": "Defer removing snapshot surface in %dms",
"level": "VERBOSE",
@@ -511,6 +505,12 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
+ "-1556507536": {
+ "message": "Passing transform hint %d for window %s%s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/WindowManagerService.java"
+ },
"-1554521902": {
"message": "showInsets(ime) was requested by different window: %s ",
"level": "WARN",
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java
deleted file mode 100644
index ce4e103..0000000
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.window.extensions;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
-import androidx.window.extensions.organizer.EmbeddingExtensionImpl;
-
-/**
- * Provider class that will instantiate the library implementation. It must be included in the
- * vendor library, and the vendor implementation must match the signature of this class.
- */
-public class ExtensionProvider {
- /**
- * Provides a simple implementation of {@link ExtensionInterface} that can be replaced by
- * an OEM by overriding this method.
- */
- public static ExtensionInterface getExtensionImpl(Context context) {
- return new SampleExtensionImpl(context);
- }
-
- /** Provides a reference implementation of {@link ActivityEmbeddingComponent}. */
- public static ActivityEmbeddingComponent getActivityEmbeddingExtensionImpl(
- @NonNull Context context) {
- return new EmbeddingExtensionImpl();
- }
-
- /**
- * The support library will use this method to check API version compatibility.
- * @return API version string in MAJOR.MINOR.PATCH-description format.
- */
- public static String getApiVersion() {
- return "1.0.0-settings_sample";
- }
-}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java
deleted file mode 100644
index 6a53efe..0000000
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.window.extensions;
-
-import android.app.Activity;
-
-import androidx.annotation.NonNull;
-
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Basic implementation of the {@link ExtensionInterface}. An OEM can choose to use it as the base
- * class for their implementation.
- */
-abstract class StubExtension implements ExtensionInterface {
-
- private ExtensionCallback mExtensionCallback;
- private final Set<Activity> mWindowLayoutChangeListenerActivities = new HashSet<>();
-
- StubExtension() {
- }
-
- @Override
- public void setExtensionCallback(@NonNull ExtensionCallback extensionCallback) {
- this.mExtensionCallback = extensionCallback;
- }
-
- @Override
- public void onWindowLayoutChangeListenerAdded(@NonNull Activity activity) {
- this.mWindowLayoutChangeListenerActivities.add(activity);
- this.onListenersChanged();
- }
-
- @Override
- public void onWindowLayoutChangeListenerRemoved(@NonNull Activity activity) {
- this.mWindowLayoutChangeListenerActivities.remove(activity);
- this.onListenersChanged();
- }
-
- void updateWindowLayout(@NonNull Activity activity,
- @NonNull ExtensionWindowLayoutInfo newLayout) {
- if (this.mExtensionCallback != null) {
- mExtensionCallback.onWindowLayoutChanged(activity, newLayout);
- }
- }
-
- @NonNull
- Set<Activity> getActivitiesListeningForLayoutChanges() {
- return mWindowLayoutChangeListenerActivities;
- }
-
- protected boolean hasListeners() {
- return !mWindowLayoutChangeListenerActivities.isEmpty();
- }
-
- protected abstract void onListenersChanged();
-}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
new file mode 100644
index 0000000..990d7b6
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions;
+
+import android.app.ActivityThread;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
+import androidx.window.extensions.embedding.SplitController;
+import androidx.window.extensions.layout.WindowLayoutComponent;
+import androidx.window.extensions.layout.WindowLayoutComponentImpl;
+
+/**
+ * The reference implementation of {@link WindowExtensions} that implements the initial API version.
+ */
+public class WindowExtensionsImpl implements WindowExtensions {
+
+ private final Object mLock = new Object();
+ private volatile WindowLayoutComponent mWindowLayoutComponent;
+ private volatile SplitController mSplitController;
+
+ @Override
+ public int getVendorApiLevel() {
+ return 1;
+ }
+
+ @Override
+ public boolean isWindowLayoutComponentAvailable() {
+ return true;
+ }
+
+ @Override
+ public WindowLayoutComponent getWindowLayoutComponent() {
+ if (mWindowLayoutComponent == null) {
+ synchronized (mLock) {
+ if (mWindowLayoutComponent == null) {
+ Context context = ActivityThread.currentApplication();
+ mWindowLayoutComponent = new WindowLayoutComponentImpl(context);
+ }
+ }
+ }
+ return mWindowLayoutComponent;
+ }
+
+ /**
+ * Returns {@code true} if {@link ActivityEmbeddingComponent} is present on the device,
+ * {@code false} otherwise. If the component is not available the developer will receive a
+ * single callback with empty data or default values where possible.
+ */
+ @Override
+ public boolean isEmbeddingComponentAvailable() {
+ return true;
+ }
+
+ /**
+ * Returns the OEM implementation of {@link ActivityEmbeddingComponent} if it is supported on
+ * the device. The implementation must match the API level reported in
+ * {@link androidx.window.extensions.WindowExtensions}. An
+ * {@link UnsupportedOperationException} will be thrown if the device does not support
+ * Activity Embedding. Use
+ * {@link WindowExtensions#isEmbeddingComponentAvailable()} to determine if
+ * {@link ActivityEmbeddingComponent} is present.
+ * @return the OEM implementation of {@link ActivityEmbeddingComponent}
+ */
+ @NonNull
+ public ActivityEmbeddingComponent getActivityEmbeddingComponent() {
+ if (mSplitController == null) {
+ synchronized (mLock) {
+ if (mSplitController == null) {
+ mSplitController = new SplitController();
+ }
+ }
+ }
+ return mSplitController;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java
new file mode 100644
index 0000000..f9e1f07
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions;
+
+import android.annotation.NonNull;
+
+/**
+ * Provides the OEM implementation of {@link WindowExtensions}.
+ */
+public class WindowExtensionsProvider {
+
+ private static final WindowExtensions sWindowExtensions = new WindowExtensionsImpl();
+
+ /**
+ * Returns the OEM implementation of {@link WindowExtensions}. This method is implemented in
+ * the library provided on the device and overwrites one in the Jetpack library included in
+ * apps.
+ * @return the OEM implementation of {@link WindowExtensions}
+ */
+ @NonNull
+ public static WindowExtensions getWindowExtensions() {
+ return sWindowExtensions;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
similarity index 98%
rename from libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java
rename to libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 46c8ffe..85ef270 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.window.extensions.organizer;
+package androidx.window.extensions.embedding;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -36,7 +36,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.window.extensions.embedding.SplitRule;
import java.util.Map;
import java.util.concurrent.Executor;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
similarity index 93%
rename from libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.java
rename to libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index a41557d..06e7d14 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -14,15 +14,11 @@
* limitations under the License.
*/
-package androidx.window.extensions.organizer;
+package androidx.window.extensions.embedding;
import android.annotation.NonNull;
import android.app.Activity;
-import androidx.window.extensions.embedding.SplitPairRule;
-import androidx.window.extensions.embedding.SplitPlaceholderRule;
-import androidx.window.extensions.embedding.SplitRule;
-
/**
* Client-side descriptor of a split that holds two containers.
*/
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
similarity index 88%
rename from libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java
rename to libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index a783fcd..e1c8b11 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.window.extensions.organizer;
+package androidx.window.extensions.embedding;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -31,19 +31,10 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
-import android.util.Pair;
import android.window.TaskFragmentAppearedInfo;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;
-import androidx.window.extensions.embedding.ActivityRule;
-import androidx.window.extensions.embedding.EmbeddingRule;
-import androidx.window.extensions.embedding.SplitInfo;
-import androidx.window.extensions.embedding.SplitPairRule;
-import androidx.window.extensions.embedding.SplitPlaceholderRule;
-import androidx.window.extensions.embedding.SplitRule;
-import androidx.window.extensions.embedding.TaskFragment;
-
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@@ -53,7 +44,8 @@
/**
* Main controller class that manages split states and presentation.
*/
-public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback {
+public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback,
+ ActivityEmbeddingComponent {
private final SplitPresenter mPresenter;
@@ -64,6 +56,7 @@
// Callback to Jetpack to notify about changes to split states.
private @NonNull Consumer<List<SplitInfo>> mEmbeddingCallback;
+ private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>();
public SplitController() {
mPresenter = new SplitPresenter(new MainThreadExecutor(), this);
@@ -77,6 +70,7 @@
}
/** Updates the embedding rules applied to future activity launches. */
+ @Override
public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) {
mSplitRules.clear();
mSplitRules.addAll(rules);
@@ -103,7 +97,8 @@
/**
* Registers the split organizer callback to notify about changes to active splits.
*/
- public void setEmbeddingCallback(@NonNull Consumer<List<SplitInfo>> callback) {
+ @Override
+ public void setSplitInfoCallback(@NonNull Consumer<List<SplitInfo>> callback) {
mEmbeddingCallback = callback;
updateCallbackIfNecessary();
}
@@ -119,8 +114,8 @@
container.setInfo(taskFragmentAppearedInfo.getTaskFragmentInfo());
if (container.isFinished()) {
mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
- updateCallbackIfNecessary();
}
+ updateCallbackIfNecessary();
}
@Override
@@ -139,8 +134,8 @@
final boolean shouldFinishDependent =
!taskFragmentInfo.isTaskClearedForReuse();
mPresenter.cleanupContainer(container, shouldFinishDependent);
- updateCallbackIfNecessary();
}
+ updateCallbackIfNecessary();
}
@Override
@@ -164,18 +159,23 @@
}
}
+ void onActivityCreated(@NonNull Activity launchedActivity) {
+ handleActivityCreated(launchedActivity);
+ updateCallbackIfNecessary();
+ }
+
/**
* Checks if the activity start should be routed to a particular container. It can create a new
* container for the activity and a new split container if necessary.
*/
// TODO(b/190433398): Break down into smaller functions.
- void onActivityCreated(@NonNull Activity launchedActivity) {
+ void handleActivityCreated(@NonNull Activity launchedActivity) {
final List<EmbeddingRule> splitRules = getSplitRules();
final TaskFragmentContainer currentContainer = getContainerWithActivity(
launchedActivity.getActivityToken(), launchedActivity);
// Check if the activity is configured to always be expanded.
- if (shouldExpand(launchedActivity, splitRules)) {
+ if (shouldExpand(launchedActivity, null, splitRules)) {
if (shouldContainerBeExpanded(currentContainer)) {
// Make sure that the existing container is expanded
mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken());
@@ -240,8 +240,6 @@
mPresenter.createNewSplitContainer(activityBelow, launchedActivity,
splitPairRule);
-
- updateCallbackIfNecessary();
}
private void onActivityConfigurationChanged(@NonNull Activity activity) {
@@ -501,7 +499,7 @@
continue;
}
SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) rule;
- if (placeholderRule.getActivityPredicate().test(activity)) {
+ if (placeholderRule.matchesActivity(activity)) {
return placeholderRule;
}
}
@@ -515,8 +513,16 @@
if (mEmbeddingCallback == null) {
return;
}
- // TODO(b/190433398): Check if something actually changed
- mEmbeddingCallback.accept(getActiveSplitStates());
+ if (!allActivitiesCreated()) {
+ return;
+ }
+ List<SplitInfo> currentSplitStates = getActiveSplitStates();
+ if (mLastReportedSplitStates.equals(currentSplitStates)) {
+ return;
+ }
+ mLastReportedSplitStates.clear();
+ mLastReportedSplitStates.addAll(currentSplitStates);
+ mEmbeddingCallback.accept(currentSplitStates);
}
/**
@@ -525,20 +531,46 @@
private List<SplitInfo> getActiveSplitStates() {
List<SplitInfo> splitStates = new ArrayList<>();
for (SplitContainer container : mSplitContainers) {
- TaskFragment primaryContainer =
- new TaskFragment(
+ if (container.getPrimaryContainer().isEmpty()
+ || container.getSecondaryContainer().isEmpty()) {
+ // Skipping containers that do not have any activities to report.
+ continue;
+ }
+ ActivityStack primaryContainer =
+ new ActivityStack(
container.getPrimaryContainer().collectActivities());
- TaskFragment secondaryContainer =
- new TaskFragment(
+ ActivityStack secondaryContainer =
+ new ActivityStack(
container.getSecondaryContainer().collectActivities());
SplitInfo splitState = new SplitInfo(primaryContainer,
- secondaryContainer, container.getSplitRule().getSplitRatio());
+ secondaryContainer,
+ // Splits that are not showing side-by-side are reported as having 0 split
+ // ratio, since by definition in the API the primary container occupies no
+ // width of the split when covered by the secondary.
+ mPresenter.shouldShowSideBySide(container)
+ ? container.getSplitRule().getSplitRatio()
+ : 0.0f);
splitStates.add(splitState);
}
return splitStates;
}
/**
+ * Checks if all activities that are registered with the containers have already appeared in
+ * the client.
+ */
+ private boolean allActivitiesCreated() {
+ for (TaskFragmentContainer container : mContainers) {
+ if (container.getInfo() == null
+ || container.getInfo().getActivities().size()
+ != container.collectActivities().size()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
* Returns {@code true} if the container is expanded to occupy full task size.
* Returns {@code false} if the container is included in an active split.
*/
@@ -567,8 +599,7 @@
continue;
}
SplitPairRule pairRule = (SplitPairRule) rule;
- if (pairRule.getActivityIntentPredicate().test(
- new Pair(primaryActivity, secondaryActivityIntent))) {
+ if (pairRule.matchesActivityIntentPair(primaryActivity, secondaryActivityIntent)) {
return pairRule;
}
}
@@ -587,10 +618,9 @@
}
SplitPairRule pairRule = (SplitPairRule) rule;
final Intent intent = secondaryActivity.getIntent();
- if (pairRule.getActivityPairPredicate().test(
- new Pair(primaryActivity, secondaryActivity))
- && (intent == null || pairRule.getActivityIntentPredicate().test(
- new Pair(primaryActivity, intent)))) {
+ if (pairRule.matchesActivityPair(primaryActivity, secondaryActivity)
+ && (intent == null
+ || pairRule.matchesActivityIntentPair(primaryActivity, intent))) {
return pairRule;
}
}
@@ -611,7 +641,7 @@
* Returns {@code true} if an Activity with the provided component name should always be
* expanded to occupy full task bounds. Such activity must not be put in a split.
*/
- private static boolean shouldExpand(@NonNull Activity activity,
+ private static boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent,
List<EmbeddingRule> splitRules) {
if (splitRules == null) {
return false;
@@ -624,7 +654,9 @@
if (!activityRule.shouldAlwaysExpand()) {
continue;
}
- if (activityRule.getActivityPredicate().test(activity)) {
+ if (activity != null && activityRule.matchesActivity(activity)) {
+ return true;
+ } else if (intent != null && activityRule.matchesIntent(intent)) {
return true;
}
}
@@ -678,11 +710,11 @@
/** Executor that posts on the main application thread. */
private static class MainThreadExecutor implements Executor {
- private final Handler handler = new Handler(Looper.getMainLooper());
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
@Override
public void execute(Runnable r) {
- handler.post(r);
+ mHandler.post(r);
}
}
@@ -703,13 +735,25 @@
}
final Activity launchingActivity = (Activity) who;
- if (!setLaunchingToSideContainer(launchingActivity, intent, options)) {
+ if (shouldExpand(null, intent, getSplitRules())) {
+ setLaunchingInExpandedContainer(launchingActivity, options);
+ } else if (!setLaunchingToSideContainer(launchingActivity, intent, options)) {
setLaunchingInSameContainer(launchingActivity, intent, options);
}
return super.onStartActivity(who, intent, options);
}
+ private void setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options) {
+ TaskFragmentContainer newContainer = mPresenter.createNewExpandedContainer(
+ launchingActivity);
+
+ // Amend the request to let the WM know that the activity should be placed in the
+ // dedicated container.
+ options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
+ newContainer.getTaskFragmentToken());
+ }
+
/**
* Returns {@code true} if the activity that is going to be started via the
* {@code intent} should be paired with the {@code launchingActivity} and is set to be
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
similarity index 80%
rename from libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java
rename to libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index ac85ac8..25292b9 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -14,16 +14,19 @@
* limitations under the License.
*/
-package androidx.window.extensions.organizer;
+package androidx.window.extensions.embedding;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
+import android.util.LayoutDirection;
+import android.view.View;
import android.view.WindowInsets;
import android.view.WindowMetrics;
import android.window.TaskFragmentCreationParams;
@@ -32,8 +35,6 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.window.extensions.embedding.SplitPairRule;
-import androidx.window.extensions.embedding.SplitRule;
import java.util.concurrent.Executor;
@@ -42,13 +43,13 @@
* {@link SplitController}.
*/
class SplitPresenter extends JetpackTaskFragmentOrganizer {
- private static final int POSITION_LEFT = 0;
- private static final int POSITION_RIGHT = 1;
+ private static final int POSITION_START = 0;
+ private static final int POSITION_END = 1;
private static final int POSITION_FILL = 2;
@IntDef(value = {
- POSITION_LEFT,
- POSITION_RIGHT,
+ POSITION_START,
+ POSITION_END,
POSITION_FILL,
})
private @interface Position {}
@@ -96,13 +97,15 @@
final WindowContainerTransaction wct = new WindowContainerTransaction();
final Rect parentBounds = getParentContainerBounds(primaryActivity);
- final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
+ final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
+ isLtr(primaryActivity, rule));
final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
primaryActivity, primaryRectBounds, null);
// Create new empty task fragment
- TaskFragmentContainer secondaryContainer = mController.newContainer(null);
- final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
+ final TaskFragmentContainer secondaryContainer = mController.newContainer(null);
+ final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds,
+ rule, isLtr(primaryActivity, rule));
createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
primaryActivity.getActivityToken(), secondaryRectBounds,
WINDOWING_MODE_MULTI_WINDOW);
@@ -135,11 +138,13 @@
final WindowContainerTransaction wct = new WindowContainerTransaction();
final Rect parentBounds = getParentContainerBounds(primaryActivity);
- final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
+ final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
+ isLtr(primaryActivity, rule));
final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
primaryActivity, primaryRectBounds, null);
- final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
+ final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
+ isLtr(primaryActivity, rule));
final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct,
secondaryActivity, secondaryRectBounds, primaryContainer);
@@ -153,6 +158,20 @@
}
/**
+ * Creates a new expanded container.
+ */
+ TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) {
+ final TaskFragmentContainer newContainer = mController.newContainer(null);
+
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ createTaskFragment(wct, newContainer.getTaskFragmentToken(),
+ launchingActivity.getActivityToken(), new Rect(), WINDOWING_MODE_MULTI_WINDOW);
+
+ applyTransaction(wct);
+ return newContainer;
+ }
+
+ /**
* Creates a new container or resizes an existing container for activity to the provided bounds.
* @param activity The activity to be re-parented to the container if necessary.
* @param containerToAvoid Re-parent from this container if an activity is already in it.
@@ -197,8 +216,10 @@
void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent,
@Nullable Bundle activityOptions, @NonNull SplitRule rule) {
final Rect parentBounds = getParentContainerBounds(launchingActivity);
- final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
- final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
+ final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
+ isLtr(launchingActivity, rule));
+ final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
+ isLtr(launchingActivity, rule));
TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
launchingActivity.getActivityToken());
@@ -231,8 +252,15 @@
// Getting the parent bounds using the updated container - it will have the recent value.
final Rect parentBounds = getParentContainerBounds(updatedContainer);
final SplitRule rule = splitContainer.getSplitRule();
- final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
- final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
+ final Activity activity = splitContainer.getPrimaryContainer().getTopNonFinishingActivity();
+ if (activity == null) {
+ return;
+ }
+ final boolean isLtr = isLtr(activity, rule);
+ final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
+ isLtr);
+ final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
+ isLtr);
// If the task fragments are not registered yet, the positions will be updated after they
// are created again.
@@ -283,36 +311,64 @@
// TODO(b/190433398): Supply correct insets.
final WindowMetrics parentMetrics = new WindowMetrics(parentBounds,
new WindowInsets(new Rect()));
- return rule.getParentWindowMetricsPredicate().test(parentMetrics);
+ return rule.checkParentMetrics(parentMetrics);
}
@NonNull
private Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds,
- @NonNull SplitRule rule) {
+ @NonNull SplitRule rule, boolean isLtr) {
if (!shouldShowSideBySide(parentBounds, rule)) {
return new Rect();
}
- float splitRatio = rule.getSplitRatio();
+ final float splitRatio = rule.getSplitRatio();
+ final float rtlSplitRatio = 1 - splitRatio;
switch (position) {
- case POSITION_LEFT:
- return new Rect(
- parentBounds.left,
- parentBounds.top,
- (int) (parentBounds.left + parentBounds.width() * splitRatio),
- parentBounds.bottom);
- case POSITION_RIGHT:
- return new Rect(
- (int) (parentBounds.left + parentBounds.width() * splitRatio),
- parentBounds.top,
- parentBounds.right,
- parentBounds.bottom);
+ case POSITION_START:
+ return isLtr ? getLeftContainerBounds(parentBounds, splitRatio)
+ : getRightContainerBounds(parentBounds, rtlSplitRatio);
+ case POSITION_END:
+ return isLtr ? getRightContainerBounds(parentBounds, splitRatio)
+ : getLeftContainerBounds(parentBounds, rtlSplitRatio);
case POSITION_FILL:
return parentBounds;
}
return parentBounds;
}
+ private Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) {
+ return new Rect(
+ parentBounds.left,
+ parentBounds.top,
+ (int) (parentBounds.left + parentBounds.width() * splitRatio),
+ parentBounds.bottom);
+ }
+
+ private Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) {
+ return new Rect(
+ (int) (parentBounds.left + parentBounds.width() * splitRatio),
+ parentBounds.top,
+ parentBounds.right,
+ parentBounds.bottom);
+ }
+
+ /**
+ * Checks if a split with the provided rule should be displays in left-to-right layout
+ * direction, either always or with the current configuration.
+ */
+ private boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) {
+ switch (rule.getLayoutDirection()) {
+ case LayoutDirection.LOCALE:
+ return context.getResources().getConfiguration().getLayoutDirection()
+ == View.LAYOUT_DIRECTION_LTR;
+ case LayoutDirection.RTL:
+ return false;
+ case LayoutDirection.LTR:
+ default:
+ return true;
+ }
+ }
+
@NonNull
Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) {
final Configuration parentConfig = mFragmentParentConfigs.get(
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
similarity index 63%
rename from libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationAdapter.java
rename to libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
index 2fa0045..65bd9f3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationAdapter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package androidx.window.extensions.organizer;
+package androidx.window.extensions.embedding;
+import android.graphics.Rect;
import android.view.Choreographer;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
@@ -31,15 +32,28 @@
private final Animation mAnimation;
private final RemoteAnimationTarget mTarget;
private final SurfaceControl mLeash;
+ private final boolean mSizeChanged;
private final Transformation mTransformation = new Transformation();
private final float[] mMatrix = new float[9];
+ private final float[] mVecs = new float[4];
+ private final Rect mRect = new Rect();
private boolean mIsFirstFrame = true;
TaskFragmentAnimationAdapter(@NonNull Animation animation,
@NonNull RemoteAnimationTarget target) {
+ this(animation, target, target.leash, false /* sizeChanged */);
+ }
+
+ /**
+ * @param sizeChanged whether the surface size needs to be changed.
+ */
+ TaskFragmentAnimationAdapter(@NonNull Animation animation,
+ @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash,
+ boolean sizeChanged) {
mAnimation = animation;
mTarget = target;
- mLeash = target.leash;
+ mLeash = leash;
+ mSizeChanged = sizeChanged;
}
/** Called on frame update. */
@@ -56,6 +70,22 @@
t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
t.setAlpha(mLeash, mTransformation.getAlpha());
t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+
+ if (mSizeChanged) {
+ // The following applies an inverse scale to the clip-rect so that it crops "after" the
+ // scale instead of before.
+ mVecs[1] = mVecs[2] = 0;
+ mVecs[0] = mVecs[3] = 1;
+ mTransformation.getMatrix().mapVectors(mVecs);
+ mVecs[0] = 1.f / mVecs[0];
+ mVecs[3] = 1.f / mVecs[3];
+ final Rect clipRect = mTransformation.getClipRect();
+ mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f);
+ mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f);
+ mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f);
+ mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f);
+ t.setWindowCrop(mLeash, mRect);
+ }
}
/** Called after animation finished. */
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
similarity index 97%
rename from libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationController.java
rename to libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
index 6631243..6579766 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.window.extensions.organizer;
+package androidx.window.extensions.embedding;
import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
similarity index 78%
rename from libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationRunner.java
rename to libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
index 7ac1118..bb37fff 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationRunner.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.window.extensions.organizer;
+package androidx.window.extensions.embedding;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
@@ -23,8 +23,6 @@
import android.animation.Animator;
import android.animation.ValueAnimator;
-import android.app.ActivityThread;
-import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
@@ -39,10 +37,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.internal.R;
-import com.android.internal.policy.AttributeCache;
-import com.android.internal.policy.TransitionAnimation;
-
import java.util.ArrayList;
import java.util.List;
@@ -51,13 +45,10 @@
private static final String TAG = "TaskFragAnimationRunner";
private final Handler mHandler = new Handler(Looper.myLooper());
- private final TransitionAnimation mTransitionAnimation;
+ private final TaskFragmentAnimationSpec mAnimationSpec;
TaskFragmentAnimationRunner() {
- final Context context = ActivityThread.currentActivityThread().getApplication();
- mTransitionAnimation = new TransitionAnimation(context, false /* debug */, TAG);
- // Initialize the AttributeCache for the TransitionAnimation.
- AttributeCache.init(context);
+ mAnimationSpec = new TaskFragmentAnimationSpec(mHandler);
}
@Nullable
@@ -175,11 +166,10 @@
private List<TaskFragmentAnimationAdapter> createOpenAnimationAdapters(
@NonNull RemoteAnimationTarget[] targets) {
- // TODO(b/196173550) We need to customize the animation to handle two open window as one.
final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
for (RemoteAnimationTarget target : targets) {
final Animation animation =
- loadOpenAnimation(target.mode != MODE_CLOSING /* isEnter */);
+ mAnimationSpec.loadOpenAnimation(target.mode != MODE_CLOSING /* isEnter */);
adapters.add(new TaskFragmentAnimationAdapter(animation, target));
}
return adapters;
@@ -187,11 +177,10 @@
private List<TaskFragmentAnimationAdapter> createCloseAnimationAdapters(
@NonNull RemoteAnimationTarget[] targets) {
- // TODO(b/196173550) We need to customize the animation to handle two open window as one.
final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
for (RemoteAnimationTarget target : targets) {
final Animation animation =
- loadCloseAnimation(target.mode != MODE_CLOSING /* isEnter */);
+ mAnimationSpec.loadCloseAnimation(target.mode != MODE_CLOSING /* isEnter */);
adapters.add(new TaskFragmentAnimationAdapter(animation, target));
}
return adapters;
@@ -199,29 +188,29 @@
private List<TaskFragmentAnimationAdapter> createChangeAnimationAdapters(
@NonNull RemoteAnimationTarget[] targets) {
- // TODO(b/196173550) We need to hard code the change animation instead of using the default
- // open. See WindowChangeAnimationSpec.java as an example.
- final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
for (RemoteAnimationTarget target : targets) {
- // The start leash is snapshot of the previous window. Hide it for now, will need to use
- // it for the fade in.
- if (target.startLeash != null) {
- t.hide(target.startLeash);
+ if (target.startBounds != null) {
+ final Animation[] animations =
+ mAnimationSpec.createChangeBoundsChangeAnimations(target);
+ adapters.add(new TaskFragmentAnimationAdapter(animations[0], target,
+ target.startLeash, false /* sizeChanged */));
+ adapters.add(new TaskFragmentAnimationAdapter(animations[1], target,
+ target.leash, true /* sizeChanged */));
+ continue;
}
+
+ final Animation animation;
+ if (target.hasAnimatingParent) {
+ // No-op if it will be covered by the changing parent window.
+ animation = TaskFragmentAnimationSpec.createNoopAnimation(target);
+ } else if (target.mode == MODE_CLOSING) {
+ animation = mAnimationSpec.createChangeBoundsCloseAnimation(target);
+ } else {
+ animation = mAnimationSpec.createChangeBoundsOpenAnimation(target);
+ }
+ adapters.add(new TaskFragmentAnimationAdapter(animation, target));
}
- t.apply();
- return createOpenAnimationAdapters(targets);
- }
-
- private Animation loadOpenAnimation(boolean isEnter) {
- return mTransitionAnimation.loadDefaultAnimationAttr(isEnter
- ? R.styleable.WindowAnimation_activityOpenEnterAnimation
- : R.styleable.WindowAnimation_activityOpenExitAnimation);
- }
-
- private Animation loadCloseAnimation(boolean isEnter) {
- return mTransitionAnimation.loadDefaultAnimationAttr(isEnter
- ? R.styleable.WindowAnimation_activityCloseEnterAnimation
- : R.styleable.WindowAnimation_activityCloseExitAnimation);
+ return adapters;
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
new file mode 100644
index 0000000..7129590
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+
+import android.app.ActivityThread;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.provider.Settings;
+import android.view.RemoteAnimationTarget;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.ClipRectAnimation;
+import android.view.animation.Interpolator;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.TranslateAnimation;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+import com.android.internal.policy.AttributeCache;
+import com.android.internal.policy.TransitionAnimation;
+
+/** Animation spec for TaskFragment transition. */
+class TaskFragmentAnimationSpec {
+
+ private static final String TAG = "TaskFragAnimationSpec";
+ private static final int CHANGE_ANIMATION_DURATION = 517;
+ private static final int CHANGE_ANIMATION_FADE_DURATION = 82;
+ private static final int CHANGE_ANIMATION_FADE_OFFSET = 67;
+
+ private final Context mContext;
+ private final TransitionAnimation mTransitionAnimation;
+ private final Interpolator mFastOutExtraSlowInInterpolator;
+ private float mTransitionAnimationScaleSetting;
+
+ TaskFragmentAnimationSpec(@NonNull Handler handler) {
+ mContext = ActivityThread.currentActivityThread().getApplication();
+ mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG);
+ // Initialize the AttributeCache for the TransitionAnimation.
+ AttributeCache.init(mContext);
+ mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator(
+ mContext, android.R.interpolator.fast_out_extra_slow_in);
+
+ // The transition animation should be adjusted based on the developer option.
+ final ContentResolver resolver = mContext.getContentResolver();
+ mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver,
+ Settings.Global.TRANSITION_ANIMATION_SCALE,
+ mContext.getResources().getFloat(
+ R.dimen.config_appTransitionAnimationDurationScaleDefault));
+ resolver.registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false,
+ new SettingsObserver(handler));
+ }
+
+ /** For target that doesn't need to be animated. */
+ static Animation createNoopAnimation(@NonNull RemoteAnimationTarget target) {
+ // Noop but just keep the target showing/hiding.
+ final float alpha = target.mode == MODE_CLOSING ? 0f : 1f;
+ return new AlphaAnimation(alpha, alpha);
+ }
+
+ /** Animation for target that is opening in a change transition. */
+ Animation createChangeBoundsOpenAnimation(@NonNull RemoteAnimationTarget target) {
+ final Rect bounds = target.localBounds;
+ // The target will be animated in from left or right depends on its position.
+ final int startLeft = bounds.left == 0 ? -bounds.width() : bounds.width();
+
+ // The position should be 0-based as we will post translate in
+ // TaskFragmentAnimationAdapter#onAnimationUpdate
+ final Animation animation = new TranslateAnimation(startLeft, 0, 0, 0);
+ animation.setInterpolator(mFastOutExtraSlowInInterpolator);
+ animation.setDuration(CHANGE_ANIMATION_DURATION);
+ animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+
+ /** Animation for target that is closing in a change transition. */
+ Animation createChangeBoundsCloseAnimation(@NonNull RemoteAnimationTarget target) {
+ final Rect bounds = target.localBounds;
+ // The target will be animated out to left or right depends on its position.
+ final int endLeft = bounds.left == 0 ? -bounds.width() : bounds.width();
+
+ // The position should be 0-based as we will post translate in
+ // TaskFragmentAnimationAdapter#onAnimationUpdate
+ final Animation animation = new TranslateAnimation(0, endLeft, 0, 0);
+ animation.setInterpolator(mFastOutExtraSlowInInterpolator);
+ animation.setDuration(CHANGE_ANIMATION_DURATION);
+ animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+
+ /**
+ * Animation for target that is changing (bounds change) in a change transition.
+ * @return the return array always has two elements. The first one is for the start leash, and
+ * the second one is for the end leash.
+ */
+ Animation[] createChangeBoundsChangeAnimations(@NonNull RemoteAnimationTarget target) {
+ final Rect startBounds = target.startBounds;
+ final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds();
+ final Rect endBounds = target.localBounds;
+ float scaleX = ((float) startBounds.width()) / endBounds.width();
+ float scaleY = ((float) startBounds.height()) / endBounds.height();
+ // Start leash is a child of the end leash. Reverse the scale so that the start leash won't
+ // be scaled up with its parent.
+ float startScaleX = 1.f / scaleX;
+ float startScaleY = 1.f / scaleY;
+
+ // The start leash will be fade out.
+ final AnimationSet startSet = new AnimationSet(true /* shareInterpolator */);
+ startSet.setInterpolator(mFastOutExtraSlowInInterpolator);
+ final Animation startAlpha = new AlphaAnimation(1f, 0f);
+ startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION);
+ startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET);
+ startSet.addAnimation(startAlpha);
+ final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY,
+ startScaleY);
+ startScale.setDuration(CHANGE_ANIMATION_DURATION);
+ startSet.addAnimation(startScale);
+ startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(),
+ endBounds.height());
+ startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+
+ // The end leash will be moved into the end position while scaling.
+ final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */);
+ endSet.setInterpolator(mFastOutExtraSlowInInterpolator);
+ final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1);
+ endScale.setDuration(CHANGE_ANIMATION_DURATION);
+ endSet.addAnimation(endScale);
+ // The position should be 0-based as we will post translate in
+ // TaskFragmentAnimationAdapter#onAnimationUpdate
+ final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
+ 0, 0);
+ endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
+ endSet.addAnimation(endTranslate);
+ // The end leash is resizing, we should update the window crop based on the clip rect.
+ final Rect startClip = new Rect(startBounds);
+ final Rect endClip = new Rect(endBounds);
+ startClip.offsetTo(0, 0);
+ endClip.offsetTo(0, 0);
+ final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
+ clipAnim.setDuration(CHANGE_ANIMATION_DURATION);
+ endSet.addAnimation(clipAnim);
+ endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
+ parentBounds.height());
+ endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+
+ return new Animation[]{startSet, endSet};
+ }
+
+ Animation loadOpenAnimation(boolean isEnter) {
+ // TODO(b/196173550) We need to customize the animation to handle two open window as one.
+ return mTransitionAnimation.loadDefaultAnimationAttr(isEnter
+ ? R.styleable.WindowAnimation_activityOpenEnterAnimation
+ : R.styleable.WindowAnimation_activityOpenExitAnimation);
+ }
+
+ Animation loadCloseAnimation(boolean isEnter) {
+ // TODO(b/196173550) We need to customize the animation to handle two open window as one.
+ return mTransitionAnimation.loadDefaultAnimationAttr(isEnter
+ ? R.styleable.WindowAnimation_activityCloseEnterAnimation
+ : R.styleable.WindowAnimation_activityCloseExitAnimation);
+ }
+
+ private class SettingsObserver extends ContentObserver {
+ SettingsObserver(@NonNull Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ mTransitionAnimationScaleSetting = Settings.Global.getFloat(
+ mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE,
+ mTransitionAnimationScaleSetting);
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
similarity index 98%
rename from libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java
rename to libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 8503b9f..54e44a7 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.window.extensions.organizer;
+package androidx.window.extensions.embedding;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -212,7 +212,9 @@
@NonNull WindowContainerTransaction wct, @NonNull SplitController controller) {
// Finish own activities
for (Activity activity : collectActivities()) {
- activity.finish();
+ if (!activity.isFinishing()) {
+ activity.finish();
+ }
}
if (!shouldFinishDependent) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
similarity index 63%
rename from libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java
rename to libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index a0d5b00..383d91d 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.window.extensions;
+package androidx.window.extensions.layout;
import static android.view.Display.DEFAULT_DISPLAY;
@@ -36,19 +36,27 @@
import androidx.window.util.PriorityDataProducer;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
/**
- * Reference implementation of androidx.window.extensions OEM interface for use with
+ * Reference implementation of androidx.window.extensions.layout OEM interface for use with
* WindowManager Jetpack.
*
* NOTE: This version is a work in progress and under active development. It MUST NOT be used in
* production builds since the interface can still change before reaching stable version.
* Please refer to {@link androidx.window.sidecar.SampleSidecarImpl} instead.
*/
-class SampleExtensionImpl extends StubExtension {
+public class WindowLayoutComponentImpl implements WindowLayoutComponent {
private static final String TAG = "SampleExtension";
+ private static WindowLayoutComponent sInstance;
+
+ private final Map<Activity, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners =
+ new HashMap<>();
private final SettingsDevicePostureProducer mSettingsDevicePostureProducer;
private final DataProducer<Integer> mDevicePostureProducer;
@@ -56,7 +64,7 @@
private final SettingsDisplayFeatureProducer mSettingsDisplayFeatureProducer;
private final DataProducer<List<DisplayFeature>> mDisplayFeatureProducer;
- SampleExtensionImpl(Context context) {
+ public WindowLayoutComponentImpl(Context context) {
mSettingsDevicePostureProducer = new SettingsDevicePostureProducer(context);
mDevicePostureProducer = new PriorityDataProducer<>(List.of(
mSettingsDevicePostureProducer,
@@ -73,28 +81,68 @@
mDisplayFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
}
+ /**
+ * Adds a listener interested in receiving updates to {@link WindowLayoutInfo}
+ * @param activity hosting a {@link android.view.Window}
+ * @param consumer interested in receiving updates to {@link WindowLayoutInfo}
+ */
+ public void addWindowLayoutInfoListener(@NonNull Activity activity,
+ @NonNull Consumer<WindowLayoutInfo> consumer) {
+ mWindowLayoutChangeListeners.put(activity, consumer);
+ updateRegistrations();
+ }
+
+ /**
+ * Removes a listener no longer interested in receiving updates.
+ * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo}
+ */
+ public void removeWindowLayoutInfoListener(
+ @NonNull Consumer<WindowLayoutInfo> consumer) {
+ mWindowLayoutChangeListeners.values().remove(consumer);
+ updateRegistrations();
+ }
+
+ void updateWindowLayout(@NonNull Activity activity,
+ @NonNull WindowLayoutInfo newLayout) {
+ Consumer<WindowLayoutInfo> consumer = mWindowLayoutChangeListeners.get(activity);
+ if (consumer != null) {
+ consumer.accept(newLayout);
+ }
+ }
+
+ @NonNull
+ Set<Activity> getActivitiesListeningForLayoutChanges() {
+ return mWindowLayoutChangeListeners.keySet();
+ }
+
+ protected boolean hasListeners() {
+ return !mWindowLayoutChangeListeners.isEmpty();
+ }
+
private int getFeatureState(DisplayFeature feature) {
Integer featureState = feature.getState();
Optional<Integer> posture = mDevicePostureProducer.getData();
- int fallbackPosture = posture.orElse(ExtensionFoldingFeature.STATE_FLAT);
+ int fallbackPosture = posture.orElse(FoldingFeature.STATE_FLAT);
return featureState == null ? fallbackPosture : featureState;
}
private void onDisplayFeaturesChanged() {
for (Activity activity : getActivitiesListeningForLayoutChanges()) {
- ExtensionWindowLayoutInfo newLayout = getWindowLayoutInfo(activity);
+ WindowLayoutInfo newLayout = getWindowLayoutInfo(activity);
updateWindowLayout(activity, newLayout);
}
}
@NonNull
- private ExtensionWindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) {
- List<ExtensionDisplayFeature> displayFeatures = getDisplayFeatures(activity);
- return new ExtensionWindowLayoutInfo(displayFeatures);
+ private WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) {
+ List<androidx.window.extensions.layout.DisplayFeature> displayFeatures =
+ getDisplayFeatures(activity);
+ return new WindowLayoutInfo(displayFeatures);
}
- private List<ExtensionDisplayFeature> getDisplayFeatures(@NonNull Activity activity) {
- List<ExtensionDisplayFeature> features = new ArrayList<>();
+ private List<androidx.window.extensions.layout.DisplayFeature> getDisplayFeatures(
+ @NonNull Activity activity) {
+ List<androidx.window.extensions.layout.DisplayFeature> features = new ArrayList<>();
int displayId = activity.getDisplay().getDisplayId();
if (displayId != DEFAULT_DISPLAY) {
Log.w(TAG, "This sample doesn't support display features on secondary displays");
@@ -115,15 +163,14 @@
rotateRectToDisplayRotation(displayId, featureRect);
transformToWindowSpaceRect(activity, featureRect);
- features.add(new ExtensionFoldingFeature(featureRect, baseFeature.getType(),
+ features.add(new FoldingFeature(featureRect, baseFeature.getType(),
getFeatureState(baseFeature)));
}
}
return features;
}
- @Override
- protected void onListenersChanged() {
+ private void updateRegistrations() {
if (hasListeners()) {
mSettingsDevicePostureProducer.registerObserversIfNeeded();
mSettingsDisplayFeatureProducer.registerObserversIfNeeded();
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/EmbeddingExtensionImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/EmbeddingExtensionImpl.java
deleted file mode 100644
index 9a8961f..0000000
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/EmbeddingExtensionImpl.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.window.extensions.organizer;
-
-import androidx.annotation.NonNull;
-import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
-import androidx.window.extensions.embedding.EmbeddingRule;
-import androidx.window.extensions.embedding.SplitInfo;
-
-import java.util.List;
-import java.util.Set;
-import java.util.function.Consumer;
-
-/**
- * Reference implementation of the activity embedding interface defined in WM Jetpack.
- */
-public class EmbeddingExtensionImpl implements ActivityEmbeddingComponent {
-
- private final SplitController mSplitController;
-
- public EmbeddingExtensionImpl() {
- mSplitController = new SplitController();
- }
-
- @Override
- public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) {
- mSplitController.setEmbeddingRules(rules);
- }
-
- @Override
- public void setEmbeddingCallback(@NonNull Consumer<List<SplitInfo>> consumer) {
- mSplitController.setEmbeddingCallback(consumer);
- }
-}
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index 097febf..42e829e 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/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 9d0dd3c..d590ab1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -968,7 +968,7 @@
}
});
mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> {
- if (mExpandedBubble != null) {
+ if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView().setTaskViewAlpha(
(float) valueAnimator.getAnimatedValue());
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
index e6d6028..bde2b5f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
@@ -71,23 +71,13 @@
+ " topIsHome:" + topIsHome);
}
- final int visibleSplashScreenType = legacySplashScreen
- ? STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
- : STARTING_WINDOW_TYPE_SPLASH_SCREEN;
-
if (!topIsHome) {
- if (!processRunning) {
+ if (!processRunning || newTask || (taskSwitch && !activityCreated)) {
return useEmptySplashScreen
? STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN
- : visibleSplashScreenType;
- }
- if (newTask) {
- return useEmptySplashScreen
- ? STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN
- : visibleSplashScreenType;
- }
- if (taskSwitch && !activityCreated) {
- return visibleSplashScreenType;
+ : legacySplashScreen
+ ? STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
+ : STARTING_WINDOW_TYPE_SPLASH_SCREEN;
}
}
if (taskSwitch && allowTaskSnapshot) {
diff --git a/media/java/android/media/Spatializer.java b/media/java/android/media/Spatializer.java
index 3b835f7..8b1624b 100644
--- a/media/java/android/media/Spatializer.java
+++ b/media/java/android/media/Spatializer.java
@@ -129,6 +129,14 @@
*/
public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1;
+ /**
+ * @hide
+ * Constant indicating the {@code Spatializer} on this device supports the spatialization of
+ * multichannel bed plus objects.
+ * @see #getImmersiveAudioLevel()
+ */
+ public static final int SPATIALIZER_IMMERSIVE_LEVEL_MCHAN_BED_PLUS_OBJECTS = 2;
+
/** @hide */
@IntDef(flag = false, value = {
HEAD_TRACKING_MODE_UNSUPPORTED,
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
index b8039e1..e026012 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
@@ -37,8 +37,7 @@
const val GOOGLE_BLUE = 0xFF1b6ef3.toInt()
-const val MIN_CHROMA = 15
-const val MIN_LSTAR = 10
+const val MIN_CHROMA = 5
public class ColorScheme(@ColorInt seed: Int, val darkTheme: Boolean) {
@@ -75,7 +74,14 @@
get() = ColorUtils.setAlphaComponent(if (darkTheme) accent1[2] else accent1[6], 0xFF)
init {
- val seedArgb = if (seed == Color.TRANSPARENT) GOOGLE_BLUE else seed
+ val proposedSeedCam = Cam.fromInt(seed)
+ val seedArgb = if (seed == Color.TRANSPARENT) {
+ GOOGLE_BLUE
+ } else if (proposedSeedCam.chroma < 5) {
+ GOOGLE_BLUE
+ } else {
+ seed
+ }
val camSeed = Cam.fromInt(seedArgb)
val hue = camSeed.hue
val chroma = camSeed.chroma.coerceAtLeast(ACCENT1_CHROMA)
@@ -129,9 +135,7 @@
val distinctColors = wallpaperColors.mainColors.map {
it.toArgb()
}.distinct().filter {
- val cam = Cam.fromInt(it)
- val lstar = lstarFromInt(it)
- cam.chroma >= MIN_CHROMA && lstar >= MIN_LSTAR
+ Cam.fromInt(it).chroma >= MIN_CHROMA
}.toList()
if (distinctColors.isEmpty()) {
@@ -164,30 +168,38 @@
val cam = it.value
val lstar = lstarFromInt(it.key)
val proportion = intToHueProportion[it.key]!!
- cam.chroma >= MIN_CHROMA && lstar >= MIN_LSTAR &&
+ cam.chroma >= MIN_CHROMA &&
(totalPopulationMeaningless || proportion > 0.01)
}
// Sort the colors by score, from high to low.
- val seeds = mutableListOf<Int>()
val intToScoreIntermediate = filteredIntToCam.mapValues {
score(it.value, intToHueProportion[it.key]!!)
}
val intToScore = intToScoreIntermediate.entries.toMutableList()
intToScore.sortByDescending { it.value }
- // Go through the colors, from high score to low score. If there isn't already a seed
- // color with a hue close to color being examined, add the color being examined to the
- // seed colors.
- for (entry in intToScore) {
- val int = entry.key
- val existingSeedNearby = seeds.find {
- val hueA = intToCam[int]!!.hue
- val hueB = intToCam[it]!!.hue
- hueDiff(hueA, hueB) < 15 } != null
- if (existingSeedNearby) {
- continue
+ // Go through the colors, from high score to low score.
+ // If the color is distinct in hue from colors picked so far, pick the color.
+ // Iteratively decrease the amount of hue distinctness required, thus ensuring we
+ // maximize difference between colors.
+ val minimumHueDistance = 15
+ val seeds = mutableListOf<Int>()
+ maximizeHueDistance@ for (i in 90 downTo minimumHueDistance step 1) {
+ seeds.clear()
+ for (entry in intToScore) {
+ val int = entry.key
+ val existingSeedNearby = seeds.find {
+ val hueA = intToCam[int]!!.hue
+ val hueB = intToCam[it]!!.hue
+ hueDiff(hueA, hueB) < i } != null
+ if (existingSeedNearby) {
+ continue
+ }
+ seeds.add(int)
+ if (seeds.size >= 4) {
+ break@maximizeHueDistance
+ }
}
- seeds.add(int)
}
if (seeds.isEmpty()) {
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/Shades.java b/packages/SystemUI/monet/src/com/android/systemui/monet/Shades.java
index 498b7dd..aab3538 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/Shades.java
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/Shades.java
@@ -53,10 +53,15 @@
*/
public static @ColorInt int[] of(float hue, float chroma) {
int[] shades = new int[12];
- shades[0] = ColorUtils.CAMToColor(hue, chroma, 99);
- shades[1] = ColorUtils.CAMToColor(hue, chroma, 95);
+ // At tone 90 and above, blue and yellow hues can reach a much higher chroma.
+ // To preserve a consistent appearance across all hues, use a maximum chroma of 40.
+ shades[0] = ColorUtils.CAMToColor(hue, Math.min(40f, chroma), 99);
+ shades[1] = ColorUtils.CAMToColor(hue, Math.min(40f, chroma), 95);
for (int i = 2; i < 12; i++) {
float lStar = (i == 6) ? MIDDLE_LSTAR : 100 - 10 * (i - 1);
+ if (lStar >= 90) {
+ chroma = Math.min(40f, chroma);
+ }
shades[i] = ColorUtils.CAMToColor(hue, chroma, lStar);
}
return shades;
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index a2ae5023..e854b02 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -104,4 +104,11 @@
allow it to use the whole screen space, 0.6 will allow it to use just under half of the
screen. -->
<item name="half_opened_bouncer_height_ratio" type="dimen" format="float">0.0</item>
+
+ <!-- The actual amount of translation that is applied to the bouncer when it animates from one
+ side of the screen to the other in one-handed mode. Note that it will always translate from
+ the side of the screen to the other (it will "jump" closer to the destination while the
+ opacity is zero), but this controls how much motion will actually be applied to it while
+ animating. Larger values will cause it to move "faster" while fading out/in. -->
+ <dimen name="one_handed_bouncer_move_animation_translation">120dp</dimen>
</resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 64d214d..d80a408 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -21,8 +21,7 @@
import static java.lang.Integer.max;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
@@ -40,6 +39,8 @@
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import androidx.annotation.VisibleForTesting;
@@ -55,7 +56,6 @@
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.shared.system.SysUiStatsLog;
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
import java.util.ArrayList;
import java.util.List;
@@ -85,6 +85,13 @@
private static final long IME_DISAPPEAR_DURATION_MS = 125;
+ // The duration of the animation to switch bouncer sides.
+ private static final long BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS = 500;
+
+ // How much of the switch sides animation should be dedicated to fading the bouncer out. The
+ // remainder will fade it back in again.
+ private static final float BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION = 0.2f;
+
@VisibleForTesting
KeyguardSecurityViewFlipper mSecurityViewFlipper;
private AlertDialog mAlertDialog;
@@ -322,18 +329,87 @@
? 0 : (int) (getMeasuredWidth() - mSecurityViewFlipper.getWidth());
if (animate) {
- mRunningOneHandedAnimator =
- mSecurityViewFlipper.animate().translationX(targetTranslation);
- mRunningOneHandedAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
- mRunningOneHandedAnimator.setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mRunningOneHandedAnimator = null;
+ // This animation is a bit fun to implement. The bouncer needs to move, and fade in/out
+ // at the same time. The issue is, the bouncer should only move a short amount (120dp or
+ // so), but obviously needs to go from one side of the screen to the other. This needs a
+ // pretty custom animation.
+ //
+ // This works as follows. It uses a ValueAnimation to simply drive the animation
+ // progress. This animator is responsible for both the translation of the bouncer, and
+ // the current fade. It will fade the bouncer out while also moving it along the 120dp
+ // path. Once the bouncer is fully faded out though, it will "snap" the bouncer closer
+ // to its destination, then fade it back in again. The effect is that the bouncer will
+ // move from 0 -> X while fading out, then (destination - X) -> destination while fading
+ // back in again.
+ // TODO(b/195012405): Make this animation properly abortable.
+ Interpolator positionInterpolator = AnimationUtils.loadInterpolator(
+ mContext, android.R.interpolator.fast_out_extra_slow_in);
+ Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN;
+ Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+
+ ValueAnimator anim = ValueAnimator.ofFloat(0.0f, 1.0f);
+ anim.setDuration(BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS);
+ anim.setInterpolator(Interpolators.LINEAR);
+
+ int initialTranslation = (int) mSecurityViewFlipper.getTranslationX();
+ int totalTranslation = (int) getResources().getDimension(
+ R.dimen.one_handed_bouncer_move_animation_translation);
+
+ final boolean shouldRestoreLayerType = mSecurityViewFlipper.hasOverlappingRendering()
+ && mSecurityViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE;
+ if (shouldRestoreLayerType) {
+ mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null);
+ }
+
+ anim.addUpdateListener(animation -> {
+ float switchPoint = BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION;
+ boolean isFadingOut = animation.getAnimatedFraction() < switchPoint;
+
+ int currentTranslation = (int) (positionInterpolator.getInterpolation(
+ animation.getAnimatedFraction()) * totalTranslation);
+ int translationRemaining = totalTranslation - currentTranslation;
+
+ // Flip the sign if we're going from right to left.
+ if (mIsSecurityViewLeftAligned) {
+ currentTranslation = -currentTranslation;
+ translationRemaining = -translationRemaining;
+ }
+
+ if (isFadingOut) {
+ // The bouncer fades out over the first X%.
+ float fadeOutFraction = MathUtils.constrainedMap(
+ /* rangeMin= */0.0f,
+ /* rangeMax= */1.0f,
+ /* valueMin= */0.0f,
+ /* valueMax= */switchPoint,
+ animation.getAnimatedFraction());
+ float opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction);
+ mSecurityViewFlipper.setAlpha(1f - opacity);
+
+ // Animate away from the source.
+ mSecurityViewFlipper.setTranslationX(initialTranslation + currentTranslation);
+ } else {
+ // And in again over the remaining (100-X)%.
+ float fadeInFraction = MathUtils.constrainedMap(
+ /* rangeMin= */0.0f,
+ /* rangeMax= */1.0f,
+ /* valueMin= */switchPoint,
+ /* valueMax= */1.0f,
+ animation.getAnimatedFraction());
+
+ float opacity = fadeInInterpolator.getInterpolation(fadeInFraction);
+ mSecurityViewFlipper.setAlpha(opacity);
+
+ // Fading back in, animate towards the destination.
+ mSecurityViewFlipper.setTranslationX(targetTranslation - translationRemaining);
+ }
+
+ if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) {
+ mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null);
}
});
- mRunningOneHandedAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
- mRunningOneHandedAnimator.start();
+ anim.start();
} else {
mSecurityViewFlipper.setTranslationX(targetTranslation);
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
index 17818cd..59d9aff 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
@@ -19,8 +19,9 @@
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.util.MathUtils.constrain;
import static android.util.MathUtils.sq;
+import static android.view.WindowInsets.Type.displayCutout;
import static android.view.WindowInsets.Type.ime;
-import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.systemBars;
import static java.util.Objects.requireNonNull;
@@ -41,7 +42,6 @@
import android.graphics.drawable.LayerDrawable;
import android.os.Handler;
import android.os.Looper;
-import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
@@ -95,7 +95,6 @@
private boolean mIsShowing;
private boolean mIsDownInEnlargedTouchArea;
private boolean mIsDragging = false;
- private boolean mImeVisibility;
@Alignment
private int mAlignment;
@SizeType
@@ -108,8 +107,10 @@
private int mRadiusType;
private int mMargin;
private int mPadding;
- private int mScreenHeight;
- private int mScreenWidth;
+ // The display width excludes the window insets of the system bar and display cutout.
+ private int mDisplayHeight;
+ // The display Height excludes the window insets of the system bar and display cutout.
+ private int mDisplayWidth;
private int mIconWidth;
private int mIconHeight;
private int mInset;
@@ -118,6 +119,8 @@
private int mRelativeToPointerDownX;
private int mRelativeToPointerDownY;
private float mRadius;
+ private final Rect mDisplayInsetsRect = new Rect();
+ private final Rect mImeInsetsRect = new Rect();
private final Position mPosition;
private float mSquareScaledTouchSlop;
private final Configuration mLastConfiguration;
@@ -506,9 +509,21 @@
}
private WindowInsets onWindowInsetsApplied(WindowInsets insets) {
- final boolean currentImeVisibility = insets.isVisible(ime());
- if (currentImeVisibility != mImeVisibility) {
- mImeVisibility = currentImeVisibility;
+ final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
+ final Rect displayWindowInsetsRect = getDisplayInsets(windowMetrics).toRect();
+ if (!displayWindowInsetsRect.equals(mDisplayInsetsRect)) {
+ updateDisplaySizeWith(windowMetrics);
+ updateLocationWith(mPosition);
+ }
+
+ final Rect imeInsetsRect = windowMetrics.getWindowInsets().getInsets(ime()).toRect();
+ if (!imeInsetsRect.equals(mImeInsetsRect)) {
+ if (isImeVisible(imeInsetsRect)) {
+ mImeInsetsRect.set(imeInsetsRect);
+ } else {
+ mImeInsetsRect.setEmpty();
+ }
+
updateLocationWith(mPosition);
}
@@ -520,6 +535,11 @@
|| (side == Alignment.LEFT && downX > currentRawX);
}
+ private boolean isImeVisible(Rect imeInsetsRect) {
+ return imeInsetsRect.left != 0 || imeInsetsRect.top != 0 || imeInsetsRect.right != 0
+ || imeInsetsRect.bottom != 0;
+ }
+
private boolean hasExceededTouchSlop(int startX, int startY, int endX, int endY) {
return (sq(endX - startX) + sq(endY - startY)) > mSquareScaledTouchSlop;
}
@@ -546,9 +566,9 @@
private void updateDimensions() {
final Resources res = getResources();
- final DisplayMetrics dm = res.getDisplayMetrics();
- mScreenWidth = dm.widthPixels;
- mScreenHeight = dm.heightPixels;
+
+ updateDisplaySizeWith(mWindowManager.getCurrentWindowMetrics());
+
mMargin =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
mInset =
@@ -560,6 +580,15 @@
updateItemViewDimensionsWith(mSizeType);
}
+ private void updateDisplaySizeWith(WindowMetrics metrics) {
+ final Rect displayBounds = metrics.getBounds();
+ final Insets displayInsets = getDisplayInsets(metrics);
+ mDisplayInsetsRect.set(displayInsets.toRect());
+ displayBounds.inset(displayInsets);
+ mDisplayWidth = displayBounds.width();
+ mDisplayHeight = displayBounds.height();
+ }
+
private void updateItemViewDimensionsWith(@SizeType int sizeType) {
final Resources res = getResources();
final int paddingResId =
@@ -684,11 +713,11 @@
}
private int getMaxWindowX() {
- return mScreenWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth();
+ return mDisplayWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth();
}
private int getMaxWindowY() {
- return mScreenHeight - getWindowHeight();
+ return mDisplayHeight - getWindowHeight();
}
private InstantInsetLayerDrawable getMenuLayerDrawable() {
@@ -699,8 +728,13 @@
return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM);
}
+ private Insets getDisplayInsets(WindowMetrics metrics) {
+ return metrics.getWindowInsets().getInsetsIgnoringVisibility(
+ systemBars() | displayCutout());
+ }
+
/**
- * Updates the floating menu to be fixed at the side of the screen.
+ * Updates the floating menu to be fixed at the side of the display.
*/
private void updateLocationWith(Position position) {
final @Alignment int alignment = transformToAlignment(position.getPercentageX());
@@ -716,15 +750,9 @@
* @return the moving interval if they overlap each other, otherwise 0.
*/
private int getInterval() {
- if (!mImeVisibility) {
- return 0;
- }
-
- final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
- final Insets imeInsets = windowMetrics.getWindowInsets().getInsets(
- ime() | navigationBars());
- final int imeY = mScreenHeight - imeInsets.bottom;
- final int layoutBottomY = mCurrentLayoutParams.y + getWindowHeight();
+ final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY());
+ final int imeY = mDisplayHeight - mImeInsetsRect.bottom;
+ final int layoutBottomY = currentLayoutY + getWindowHeight();
return layoutBottomY > imeY ? (layoutBottomY - imeY) : 0;
}
@@ -855,11 +883,12 @@
@VisibleForTesting
Rect getAvailableBounds() {
- return new Rect(0, 0, mScreenWidth - getWindowWidth(), mScreenHeight - getWindowHeight());
+ return new Rect(0, 0, mDisplayWidth - getWindowWidth(),
+ mDisplayHeight - getWindowHeight());
}
private int getMaxLayoutHeight() {
- return mScreenHeight - mMargin * 2;
+ return mDisplayHeight - mMargin * 2;
}
private int getLayoutWidth() {
@@ -875,7 +904,7 @@
}
private int getWindowHeight() {
- return Math.min(mScreenHeight, mMargin * 2 + getLayoutHeight());
+ return Math.min(mDisplayHeight, mMargin * 2 + getLayoutHeight());
}
private void setSystemGestureExclusion() {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 5641868..594a642 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -244,7 +244,7 @@
final UdfpsEnrollHelper enrollHelper;
if (reason == BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
|| reason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING) {
- enrollHelper = new UdfpsEnrollHelper(mContext, reason);
+ enrollHelper = new UdfpsEnrollHelper(mContext, mFingerprintManager, reason);
} else {
enrollHelper = null;
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
index d407756..2034ff3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
@@ -16,9 +16,11 @@
package com.android.systemui.biometrics;
+import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
+import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
@@ -26,11 +28,17 @@
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.TypedValue;
import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.graphics.ColorUtils;
import com.android.systemui.R;
/**
@@ -39,10 +47,20 @@
public class UdfpsEnrollDrawable extends UdfpsDrawable {
private static final String TAG = "UdfpsAnimationEnroll";
- private static final long ANIM_DURATION = 800;
+ private static final long HINT_COLOR_ANIM_DELAY_MS = 233L;
+ private static final long HINT_COLOR_ANIM_DURATION_MS = 517L;
+ private static final long HINT_WIDTH_ANIM_DURATION_MS = 233L;
+ private static final long TARGET_ANIM_DURATION_LONG = 800L;
+ private static final long TARGET_ANIM_DURATION_SHORT = 600L;
// 1 + SCALE_MAX is the maximum that the moving target will animate to
private static final float SCALE_MAX = 0.25f;
+ private static final float HINT_PADDING_DP = 10f;
+ private static final float HINT_MAX_WIDTH_DP = 6f;
+ private static final float HINT_ANGLE = 40f;
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+
@NonNull private final Drawable mMovingTargetFpIcon;
@NonNull private final Paint mSensorOutlinePaint;
@NonNull private final Paint mBlueFill;
@@ -51,17 +69,41 @@
@Nullable private UdfpsEnrollHelper mEnrollHelper;
// Moving target animator set
- @Nullable AnimatorSet mAnimatorSet;
+ @Nullable AnimatorSet mTargetAnimatorSet;
// Moving target location
float mCurrentX;
float mCurrentY;
// Moving target size
float mCurrentScale = 1.f;
+ @ColorInt private final int mHintColorFaded;
+ @ColorInt private final int mHintColorHighlight;
+ private final float mHintMaxWidthPx;
+ private final float mHintPaddingPx;
+
+ @NonNull private final Animator.AnimatorListener mTargetAnimListener;
+
+ private boolean mShouldShowTipHint = false;
+ @NonNull private final Paint mTipHintPaint;
+ @Nullable private AnimatorSet mTipHintAnimatorSet;
+ @Nullable private ValueAnimator mTipHintColorAnimator;
+ @Nullable private ValueAnimator mTipHintWidthAnimator;
+ @NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintColorUpdateListener;
+ @NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintWidthUpdateListener;
+ @NonNull private final Animator.AnimatorListener mTipHintPulseListener;
+
+ private boolean mShouldShowEdgeHint = false;
+ @NonNull private final Paint mEdgeHintPaint;
+ @Nullable private AnimatorSet mEdgeHintAnimatorSet;
+ @Nullable private ValueAnimator mEdgeHintColorAnimator;
+ @Nullable private ValueAnimator mEdgeHintWidthAnimator;
+ @NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintColorUpdateListener;
+ @NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintWidthUpdateListener;
+ @NonNull private final Animator.AnimatorListener mEdgeHintPulseListener;
+
UdfpsEnrollDrawable(@NonNull Context context) {
super(context);
-
mSensorOutlinePaint = new Paint(0 /* flags */);
mSensorOutlinePaint.setAntiAlias(true);
mSensorOutlinePaint.setColor(mContext.getColor(R.color.udfps_enroll_icon));
@@ -78,6 +120,117 @@
mMovingTargetFpIcon.mutate();
mFingerprintDrawable.setTint(mContext.getColor(R.color.udfps_enroll_icon));
+
+ mHintColorFaded = getHintColorFaded(context);
+ mHintColorHighlight = context.getColor(R.color.udfps_enroll_progress);
+ mHintMaxWidthPx = Utils.dpToPixels(context, HINT_MAX_WIDTH_DP);
+ mHintPaddingPx = Utils.dpToPixels(context, HINT_PADDING_DP);
+
+ mTargetAnimListener = new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ updateTipHintVisibility();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+ };
+
+ mTipHintPaint = new Paint(0 /* flags */);
+ mTipHintPaint.setAntiAlias(true);
+ mTipHintPaint.setColor(mHintColorFaded);
+ mTipHintPaint.setStyle(Paint.Style.STROKE);
+ mTipHintPaint.setStrokeCap(Paint.Cap.ROUND);
+ mTipHintPaint.setStrokeWidth(0f);
+ mTipHintColorUpdateListener = animation -> {
+ mTipHintPaint.setColor((int) animation.getAnimatedValue());
+ invalidateSelf();
+ };
+ mTipHintWidthUpdateListener = animation -> {
+ mTipHintPaint.setStrokeWidth((float) animation.getAnimatedValue());
+ invalidateSelf();
+ };
+ mTipHintPulseListener = new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mHandler.postDelayed(() -> {
+ mTipHintColorAnimator =
+ ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorFaded);
+ mTipHintColorAnimator.setInterpolator(new LinearInterpolator());
+ mTipHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS);
+ mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener);
+ mTipHintColorAnimator.start();
+ }, HINT_COLOR_ANIM_DELAY_MS);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+ };
+
+ mEdgeHintPaint = new Paint(0 /* flags */);
+ mEdgeHintPaint.setAntiAlias(true);
+ mEdgeHintPaint.setColor(mHintColorFaded);
+ mEdgeHintPaint.setStyle(Paint.Style.STROKE);
+ mEdgeHintPaint.setStrokeCap(Paint.Cap.ROUND);
+ mEdgeHintPaint.setStrokeWidth(0f);
+ mEdgeHintColorUpdateListener = animation -> {
+ mEdgeHintPaint.setColor((int) animation.getAnimatedValue());
+ invalidateSelf();
+ };
+ mEdgeHintWidthUpdateListener = animation -> {
+ mEdgeHintPaint.setStrokeWidth((float) animation.getAnimatedValue());
+ invalidateSelf();
+ };
+ mEdgeHintPulseListener = new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mHandler.postDelayed(() -> {
+ mEdgeHintColorAnimator =
+ ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorFaded);
+ mEdgeHintColorAnimator.setInterpolator(new LinearInterpolator());
+ mEdgeHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS);
+ mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener);
+ mEdgeHintColorAnimator.start();
+ }, HINT_COLOR_ANIM_DELAY_MS);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+ };
+ }
+
+ @ColorInt
+ private static int getHintColorFaded(@NonNull Context context) {
+ final TypedValue tv = new TypedValue();
+ context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, tv, true);
+ final int alpha = (int) (tv.getFloat() * 255f);
+
+ final int[] attrs = new int[] {android.R.attr.colorControlNormal};
+ final TypedArray ta = context.obtainStyledAttributes(attrs);
+ try {
+ @ColorInt final int color = ta.getColor(0, context.getColor(R.color.white_disabled));
+ return ColorUtils.setAlphaComponent(color, alpha);
+ } finally {
+ ta.recycle();
+ }
}
void setEnrollHelper(@NonNull UdfpsEnrollHelper helper) {
@@ -98,41 +251,154 @@
}
void onEnrollmentProgress(int remaining, int totalSteps) {
- if (mEnrollHelper.isCenterEnrollmentComplete()) {
- if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
- mAnimatorSet.end();
+ if (mEnrollHelper == null) {
+ return;
+ }
+
+ if (!mEnrollHelper.isCenterEnrollmentStage()) {
+ if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) {
+ mTargetAnimatorSet.end();
}
final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint();
+ if (mCurrentX != point.x || mCurrentY != point.y) {
+ final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x);
+ x.addUpdateListener(animation -> {
+ mCurrentX = (float) animation.getAnimatedValue();
+ invalidateSelf();
+ });
- final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x);
- x.addUpdateListener(animation -> {
- mCurrentX = (float) animation.getAnimatedValue();
- invalidateSelf();
- });
+ final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y);
+ y.addUpdateListener(animation -> {
+ mCurrentY = (float) animation.getAnimatedValue();
+ invalidateSelf();
+ });
- final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y);
- y.addUpdateListener(animation -> {
- mCurrentY = (float) animation.getAnimatedValue();
- invalidateSelf();
- });
+ final boolean isMovingToCenter = point.x == 0f && point.y == 0f;
+ final long duration = isMovingToCenter
+ ? TARGET_ANIM_DURATION_SHORT
+ : TARGET_ANIM_DURATION_LONG;
- final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
- scale.setDuration(ANIM_DURATION);
- scale.addUpdateListener(animation -> {
- // Grow then shrink
- mCurrentScale = 1 +
- SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue());
- invalidateSelf();
- });
+ final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
+ scale.setDuration(duration);
+ scale.addUpdateListener(animation -> {
+ // Grow then shrink
+ mCurrentScale = 1
+ + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue());
+ invalidateSelf();
+ });
- mAnimatorSet = new AnimatorSet();
+ mTargetAnimatorSet = new AnimatorSet();
- mAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
- mAnimatorSet.setDuration(ANIM_DURATION);
- mAnimatorSet.playTogether(x, y, scale);
- mAnimatorSet.start();
+ mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
+ mTargetAnimatorSet.setDuration(duration);
+ mTargetAnimatorSet.addListener(mTargetAnimListener);
+ mTargetAnimatorSet.playTogether(x, y, scale);
+ mTargetAnimatorSet.start();
+ } else {
+ updateTipHintVisibility();
+ }
+ } else {
+ updateTipHintVisibility();
}
+
+ updateEdgeHintVisibility();
+ }
+
+ private void updateTipHintVisibility() {
+ final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isTipEnrollmentStage();
+ if (mShouldShowTipHint == shouldShow) {
+ return;
+ }
+ mShouldShowTipHint = shouldShow;
+
+ if (mTipHintWidthAnimator != null && mTipHintWidthAnimator.isRunning()) {
+ mTipHintWidthAnimator.cancel();
+ }
+
+ final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f;
+ mTipHintWidthAnimator = ValueAnimator.ofFloat(mTipHintPaint.getStrokeWidth(), targetWidth);
+ mTipHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
+ mTipHintWidthAnimator.addUpdateListener(mTipHintWidthUpdateListener);
+
+ if (shouldShow) {
+ startTipHintPulseAnimation();
+ } else {
+ mTipHintWidthAnimator.start();
+ }
+ }
+
+ private void updateEdgeHintVisibility() {
+ final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isEdgeEnrollmentStage();
+ if (mShouldShowEdgeHint == shouldShow) {
+ return;
+ }
+ mShouldShowEdgeHint = shouldShow;
+
+ if (mEdgeHintWidthAnimator != null && mEdgeHintWidthAnimator.isRunning()) {
+ mEdgeHintWidthAnimator.cancel();
+ }
+
+ final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f;
+ mEdgeHintWidthAnimator =
+ ValueAnimator.ofFloat(mEdgeHintPaint.getStrokeWidth(), targetWidth);
+ mEdgeHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
+ mEdgeHintWidthAnimator.addUpdateListener(mEdgeHintWidthUpdateListener);
+
+ if (shouldShow) {
+ startEdgeHintPulseAnimation();
+ } else {
+ mEdgeHintWidthAnimator.start();
+ }
+ }
+
+ private void startTipHintPulseAnimation() {
+ mHandler.removeCallbacksAndMessages(null);
+ if (mTipHintAnimatorSet != null && mTipHintAnimatorSet.isRunning()) {
+ mTipHintAnimatorSet.cancel();
+ }
+ if (mTipHintColorAnimator != null && mTipHintColorAnimator.isRunning()) {
+ mTipHintColorAnimator.cancel();
+ }
+
+ mTipHintColorAnimator = ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorHighlight);
+ mTipHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
+ mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener);
+ mTipHintColorAnimator.addListener(mTipHintPulseListener);
+
+ mTipHintAnimatorSet = new AnimatorSet();
+ mTipHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
+ mTipHintAnimatorSet.playTogether(mTipHintColorAnimator, mTipHintWidthAnimator);
+ mTipHintAnimatorSet.start();
+ }
+
+ private void startEdgeHintPulseAnimation() {
+ mHandler.removeCallbacksAndMessages(null);
+ if (mEdgeHintAnimatorSet != null && mEdgeHintAnimatorSet.isRunning()) {
+ mEdgeHintAnimatorSet.cancel();
+ }
+ if (mEdgeHintColorAnimator != null && mEdgeHintColorAnimator.isRunning()) {
+ mEdgeHintColorAnimator.cancel();
+ }
+
+ mEdgeHintColorAnimator =
+ ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorHighlight);
+ mEdgeHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
+ mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener);
+ mEdgeHintColorAnimator.addListener(mEdgeHintPulseListener);
+
+ mEdgeHintAnimatorSet = new AnimatorSet();
+ mEdgeHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
+ mEdgeHintAnimatorSet.playTogether(mEdgeHintColorAnimator, mEdgeHintWidthAnimator);
+ mEdgeHintAnimatorSet.start();
+ }
+
+ private boolean isTipHintVisible() {
+ return mTipHintPaint.getStrokeWidth() > 0f;
+ }
+
+ private boolean isEdgeHintVisible() {
+ return mEdgeHintPaint.getStrokeWidth() > 0f;
}
@Override
@@ -142,7 +408,7 @@
}
// Draw moving target
- if (mEnrollHelper.isCenterEnrollmentComplete()) {
+ if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) {
canvas.save();
canvas.translate(mCurrentX, mCurrentY);
@@ -162,6 +428,59 @@
mFingerprintDrawable.setAlpha(mAlpha);
mSensorOutlinePaint.setAlpha(mAlpha);
}
+
+ // Draw the finger tip or edges hint.
+ if (isTipHintVisible() || isEdgeHintVisible()) {
+ canvas.save();
+
+ // Make arcs start from the top, rather than the right.
+ canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY());
+
+ final float halfSensorHeight = Math.abs(mSensorRect.bottom - mSensorRect.top) / 2f;
+ final float halfSensorWidth = Math.abs(mSensorRect.right - mSensorRect.left) / 2f;
+ final float hintXOffset = halfSensorWidth + mHintPaddingPx;
+ final float hintYOffset = halfSensorHeight + mHintPaddingPx;
+
+ if (isTipHintVisible()) {
+ canvas.drawArc(
+ mSensorRect.centerX() - hintXOffset,
+ mSensorRect.centerY() - hintYOffset,
+ mSensorRect.centerX() + hintXOffset,
+ mSensorRect.centerY() + hintYOffset,
+ -HINT_ANGLE / 2f,
+ HINT_ANGLE,
+ false /* useCenter */,
+ mTipHintPaint);
+ }
+
+ if (isEdgeHintVisible()) {
+ // Draw right edge hint.
+ canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY());
+ canvas.drawArc(
+ mSensorRect.centerX() - hintXOffset,
+ mSensorRect.centerY() - hintYOffset,
+ mSensorRect.centerX() + hintXOffset,
+ mSensorRect.centerY() + hintYOffset,
+ -HINT_ANGLE / 2f,
+ HINT_ANGLE,
+ false /* useCenter */,
+ mEdgeHintPaint);
+
+ // Draw left edge hint.
+ canvas.rotate(180f, mSensorRect.centerX(), mSensorRect.centerY());
+ canvas.drawArc(
+ mSensorRect.centerX() - hintXOffset,
+ mSensorRect.centerY() - hintYOffset,
+ mSensorRect.centerX() + hintXOffset,
+ mSensorRect.centerY() + hintYOffset,
+ -HINT_ANGLE / 2f,
+ HINT_ANGLE,
+ false /* useCenter */,
+ mEdgeHintPaint);
+ }
+
+ canvas.restore();
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
index c6d2192..d5c763d3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.graphics.PointF;
import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.UserHandle;
import android.provider.Settings;
@@ -44,16 +45,14 @@
private static final String NEW_COORDS_OVERRIDE =
"com.android.systemui.biometrics.UdfpsNewCoords";
- // Enroll with two center touches before going to guided enrollment
- private static final int NUM_CENTER_TOUCHES = 2;
-
interface Listener {
void onEnrollmentProgress(int remaining, int totalSteps);
+ void onEnrollmentHelp(int remaining, int totalSteps);
void onLastStepAcquired();
- void onEnrollmentHelp();
}
@NonNull private final Context mContext;
+ @NonNull private final FingerprintManager mFingerprintManager;
// IUdfpsOverlayController reason
private final int mEnrollReason;
private final boolean mAccessibilityEnabled;
@@ -66,10 +65,15 @@
// interface makes no promises about monotonically increasing by one each time.
private int mLocationsEnrolled = 0;
+ private int mCenterTouchCount = 0;
+
@Nullable Listener mListener;
- public UdfpsEnrollHelper(@NonNull Context context, int reason) {
+ public UdfpsEnrollHelper(@NonNull Context context,
+ @NonNull FingerprintManager fingerprintManager, int reason) {
+
mContext = context;
+ mFingerprintManager = fingerprintManager;
mEnrollReason = reason;
final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
@@ -118,6 +122,14 @@
}
}
+ int getStageCount() {
+ return mFingerprintManager.getEnrollStageCount();
+ }
+
+ int getStageThresholdSteps(int totalSteps, int stageIndex) {
+ return Math.round(totalSteps * mFingerprintManager.getEnrollStageThreshold(stageIndex));
+ }
+
boolean shouldShowProgressBar() {
return mEnrollReason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING;
}
@@ -129,6 +141,9 @@
if (remaining != mRemainingSteps) {
mLocationsEnrolled++;
+ if (isCenterEnrollmentStage()) {
+ mCenterTouchCount++;
+ }
}
mRemainingSteps = remaining;
@@ -140,7 +155,7 @@
void onEnrollmentHelp() {
if (mListener != null) {
- mListener.onEnrollmentHelp();
+ mListener.onEnrollmentHelp(mRemainingSteps, mTotalSteps);
}
}
@@ -155,19 +170,41 @@
}
}
- boolean isCenterEnrollmentComplete() {
+ boolean isCenterEnrollmentStage() {
if (mTotalSteps == -1 || mRemainingSteps == -1) {
- return false;
- } else if (mAccessibilityEnabled) {
+ return true;
+ }
+ return mTotalSteps - mRemainingSteps < getStageThresholdSteps(mTotalSteps, 0);
+ }
+
+ boolean isGuidedEnrollmentStage() {
+ if (mAccessibilityEnabled || mTotalSteps == -1 || mRemainingSteps == -1) {
return false;
}
- final int stepsEnrolled = mTotalSteps - mRemainingSteps;
- return stepsEnrolled >= NUM_CENTER_TOUCHES;
+ final int progressSteps = mTotalSteps - mRemainingSteps;
+ return progressSteps >= getStageThresholdSteps(mTotalSteps, 0)
+ && progressSteps < getStageThresholdSteps(mTotalSteps, 1);
+ }
+
+ boolean isTipEnrollmentStage() {
+ if (mTotalSteps == -1 || mRemainingSteps == -1) {
+ return false;
+ }
+ final int progressSteps = mTotalSteps - mRemainingSteps;
+ return progressSteps >= getStageThresholdSteps(mTotalSteps, 1)
+ && progressSteps < getStageThresholdSteps(mTotalSteps, 2);
+ }
+
+ boolean isEdgeEnrollmentStage() {
+ if (mTotalSteps == -1 || mRemainingSteps == -1) {
+ return false;
+ }
+ return mTotalSteps - mRemainingSteps >= getStageThresholdSteps(mTotalSteps, 2);
}
@NonNull
PointF getNextGuidedEnrollmentPoint() {
- if (mAccessibilityEnabled) {
+ if (mAccessibilityEnabled || !isGuidedEnrollmentStage()) {
return new PointF(0f, 0f);
}
@@ -177,7 +214,7 @@
SCALE_OVERRIDE, SCALE,
UserHandle.USER_CURRENT);
}
- final int index = mLocationsEnrolled - NUM_CENTER_TOUCHES;
+ final int index = mLocationsEnrolled - mCenterTouchCount;
final PointF originalPoint = mGuidedEnrollmentPoints
.get(index % mGuidedEnrollmentPoints.size());
return new PointF(originalPoint.x * scale, originalPoint.y * scale);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
index 373d17c8..b2a5409 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
@@ -16,163 +16,129 @@
package com.android.systemui.biometrics;
-import android.animation.ArgbEvaluator;
-import android.animation.ValueAnimator;
-import android.annotation.ColorInt;
import android.content.Context;
-import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
-import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.Log;
-import android.util.TypedValue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.systemui.R;
+import java.util.ArrayList;
+import java.util.List;
/**
* UDFPS enrollment progress bar.
*/
public class UdfpsEnrollProgressBarDrawable extends Drawable {
+ private static final String TAG = "UdfpsProgressBar";
- private static final String TAG = "UdfpsEnrollProgressBarDrawable";
-
- private static final float PROGRESS_BAR_THICKNESS_DP = 12;
+ private static final float SEGMENT_GAP_ANGLE = 12f;
@NonNull private final Context mContext;
- @NonNull private final Paint mBackgroundCirclePaint;
- @NonNull private final Paint mProgressPaint;
- @Nullable private ValueAnimator mProgressAnimator;
- @Nullable private ValueAnimator mProgressShowingHelpAnimator;
- @Nullable private ValueAnimator mProgressHidingHelpAnimator;
- @ColorInt private final int mProgressColor;
- @ColorInt private final int mProgressHelpColor;
- private final int mShortAnimationDuration;
- private float mProgress;
- private int mRotation; // After last step, rotate the progress bar once
- private boolean mLastStepAcquired;
+ @Nullable private UdfpsEnrollHelper mEnrollHelper;
+ @NonNull private List<UdfpsEnrollProgressBarSegment> mSegments = new ArrayList<>();
+ private int mTotalSteps = 1;
+ private int mProgressSteps = 0;
+ private boolean mIsShowingHelp = false;
public UdfpsEnrollProgressBarDrawable(@NonNull Context context) {
mContext = context;
-
- mShortAnimationDuration = context.getResources()
- .getInteger(com.android.internal.R.integer.config_shortAnimTime);
- mProgressColor = context.getColor(R.color.udfps_enroll_progress);
- mProgressHelpColor = context.getColor(R.color.udfps_enroll_progress_help);
-
- mBackgroundCirclePaint = new Paint();
- mBackgroundCirclePaint.setStrokeWidth(Utils.dpToPixels(context, PROGRESS_BAR_THICKNESS_DP));
- mBackgroundCirclePaint.setColor(context.getColor(R.color.white_disabled));
- mBackgroundCirclePaint.setAntiAlias(true);
- mBackgroundCirclePaint.setStyle(Paint.Style.STROKE);
-
- // Background circle color + alpha
- TypedArray tc = context.obtainStyledAttributes(
- new int[] {android.R.attr.colorControlNormal});
- int tintColor = tc.getColor(0, mBackgroundCirclePaint.getColor());
- mBackgroundCirclePaint.setColor(tintColor);
- tc.recycle();
- TypedValue alpha = new TypedValue();
- context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, alpha, true);
- mBackgroundCirclePaint.setAlpha((int) (alpha.getFloat() * 255));
-
- // Progress should not be color extracted
- mProgressPaint = new Paint();
- mProgressPaint.setStrokeWidth(Utils.dpToPixels(context, PROGRESS_BAR_THICKNESS_DP));
- mProgressPaint.setColor(mProgressColor);
- mProgressPaint.setAntiAlias(true);
- mProgressPaint.setStyle(Paint.Style.STROKE);
- mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
}
- void setEnrollmentProgress(int remaining, int totalSteps) {
- // Add one so that the first steps actually changes progress, but also so that the last
- // step ends at 1.0
- final float progress = (totalSteps - remaining + 1) / (float) (totalSteps + 1);
- setEnrollmentProgress(progress);
- }
-
- private void setEnrollmentProgress(float progress) {
- if (mLastStepAcquired) {
- return;
- }
-
- long animationDuration = mShortAnimationDuration;
-
- hideEnrollmentHelp();
-
- if (progress == 1.f) {
- animationDuration = 400;
- final ValueAnimator rotationAnimator = ValueAnimator.ofInt(0, 400);
- rotationAnimator.setDuration(animationDuration);
- rotationAnimator.addUpdateListener(animation -> {
- Log.d(TAG, "Rotation: " + mRotation);
- mRotation = (int) animation.getAnimatedValue();
- invalidateSelf();
- });
- rotationAnimator.start();
- }
-
- if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
- mProgressAnimator.cancel();
- }
-
- mProgressAnimator = ValueAnimator.ofFloat(mProgress, progress);
- mProgressAnimator.setDuration(animationDuration);
- mProgressAnimator.addUpdateListener(animation -> {
- mProgress = (float) animation.getAnimatedValue();
+ void setEnrollHelper(@Nullable UdfpsEnrollHelper enrollHelper) {
+ mEnrollHelper = enrollHelper;
+ if (enrollHelper != null) {
+ final int stageCount = enrollHelper.getStageCount();
+ mSegments = new ArrayList<>(stageCount);
+ float startAngle = SEGMENT_GAP_ANGLE / 2f;
+ final float sweepAngle = (360f / stageCount) - SEGMENT_GAP_ANGLE;
+ final Runnable invalidateRunnable = this::invalidateSelf;
+ for (int index = 0; index < stageCount; index++) {
+ mSegments.add(new UdfpsEnrollProgressBarSegment(mContext, getBounds(), startAngle,
+ sweepAngle, SEGMENT_GAP_ANGLE, invalidateRunnable));
+ startAngle += sweepAngle + SEGMENT_GAP_ANGLE;
+ }
invalidateSelf();
- });
- mProgressAnimator.start();
+ }
+ }
+
+ void onEnrollmentProgress(int remaining, int totalSteps) {
+ mTotalSteps = totalSteps;
+ updateState(getProgressSteps(remaining, totalSteps), false /* isShowingHelp */);
+ }
+
+ void onEnrollmentHelp(int remaining, int totalSteps) {
+ updateState(getProgressSteps(remaining, totalSteps), true /* isShowingHelp */);
}
void onLastStepAcquired() {
- setEnrollmentProgress(1.f);
- mLastStepAcquired = true;
+ updateState(mTotalSteps, false /* isShowingHelp */);
}
- void onEnrollmentHelp() {
- if (mProgressShowingHelpAnimator != null || mProgressAnimator == null) {
- return; // already showing or at 0% (no progress bar visible)
- }
-
- if (mProgressHidingHelpAnimator != null && mProgressHidingHelpAnimator.isRunning()) {
- mProgressHidingHelpAnimator.cancel();
- }
- mProgressHidingHelpAnimator = null;
-
- mProgressShowingHelpAnimator = getProgressColorAnimator(
- mProgressPaint.getColor(), mProgressHelpColor);
- mProgressShowingHelpAnimator.start();
+ private static int getProgressSteps(int remaining, int totalSteps) {
+ // Show some progress for the initial touch.
+ return Math.max(1, totalSteps - remaining);
}
- private void hideEnrollmentHelp() {
- if (mProgressHidingHelpAnimator != null || mProgressShowingHelpAnimator == null) {
- return; // already hidden or help never shown
- }
-
- if (mProgressShowingHelpAnimator != null && mProgressShowingHelpAnimator.isRunning()) {
- mProgressShowingHelpAnimator.cancel();
- }
- mProgressShowingHelpAnimator = null;
-
- mProgressHidingHelpAnimator = getProgressColorAnimator(
- mProgressPaint.getColor(), mProgressColor);
- mProgressHidingHelpAnimator.start();
+ private void updateState(int progressSteps, boolean isShowingHelp) {
+ updateProgress(progressSteps);
+ updateFillColor(isShowingHelp);
}
- private ValueAnimator getProgressColorAnimator(@ColorInt int from, @ColorInt int to) {
- final ValueAnimator animator = ValueAnimator.ofObject(
- ArgbEvaluator.getInstance(), from, to);
- animator.setDuration(mShortAnimationDuration);
- animator.addUpdateListener(animation -> {
- mProgressPaint.setColor((int) animation.getAnimatedValue());
- });
- return animator;
+ private void updateProgress(int progressSteps) {
+ if (mProgressSteps == progressSteps) {
+ return;
+ }
+ mProgressSteps = progressSteps;
+
+ if (mEnrollHelper == null) {
+ Log.e(TAG, "updateState: UDFPS enroll helper was null");
+ return;
+ }
+
+ int index = 0;
+ int prevThreshold = 0;
+ while (index < mSegments.size()) {
+ final UdfpsEnrollProgressBarSegment segment = mSegments.get(index);
+ final int thresholdSteps = mEnrollHelper.getStageThresholdSteps(mTotalSteps, index);
+ if (progressSteps >= thresholdSteps && segment.getProgress() < 1f) {
+ segment.updateProgress(1f);
+ break;
+ } else if (progressSteps >= prevThreshold && progressSteps < thresholdSteps) {
+ final int relativeSteps = progressSteps - prevThreshold;
+ final int relativeThreshold = thresholdSteps - prevThreshold;
+ final float segmentProgress = (float) relativeSteps / (float) relativeThreshold;
+ segment.updateProgress(segmentProgress);
+ break;
+ }
+
+ index++;
+ prevThreshold = thresholdSteps;
+ }
+
+ if (progressSteps >= mTotalSteps) {
+ for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
+ segment.startCompletionAnimation();
+ }
+ } else {
+ for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
+ segment.cancelCompletionAnimation();
+ }
+ }
+ }
+
+ private void updateFillColor(boolean isShowingHelp) {
+ if (mIsShowingHelp == isShowingHelp) {
+ return;
+ }
+ mIsShowingHelp = isShowingHelp;
+
+ for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
+ segment.updateFillColor(isShowingHelp);
+ }
}
@Override
@@ -180,43 +146,22 @@
canvas.save();
// Progress starts from the top, instead of the right
- canvas.rotate(-90 + mRotation, getBounds().centerX(), getBounds().centerY());
+ canvas.rotate(-90f, getBounds().centerX(), getBounds().centerY());
- // Progress bar "background track"
- final float halfPaddingPx = Utils.dpToPixels(mContext, PROGRESS_BAR_THICKNESS_DP) / 2;
- canvas.drawArc(halfPaddingPx,
- halfPaddingPx,
- getBounds().right - halfPaddingPx,
- getBounds().bottom - halfPaddingPx,
- 0,
- 360,
- false,
- mBackgroundCirclePaint
- );
-
- final float progress = 360.f * mProgress;
- // Progress
- canvas.drawArc(halfPaddingPx,
- halfPaddingPx,
- getBounds().right - halfPaddingPx,
- getBounds().bottom - halfPaddingPx,
- 0,
- progress,
- false,
- mProgressPaint
- );
+ // Draw each of the enroll segments.
+ for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
+ segment.draw(canvas);
+ }
canvas.restore();
}
@Override
public void setAlpha(int alpha) {
-
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
-
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarSegment.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarSegment.java
new file mode 100644
index 0000000..bd6ab44
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarSegment.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.util.TypedValue;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.systemui.R;
+
+/**
+ * A single segment of the UDFPS enrollment progress bar.
+ */
+public class UdfpsEnrollProgressBarSegment {
+ private static final String TAG = "UdfpsProgressBarSegment";
+
+ private static final long FILL_COLOR_ANIMATION_DURATION_MS = 200L;
+ private static final long PROGRESS_ANIMATION_DURATION_MS = 400L;
+ private static final long OVER_SWEEP_ANIMATION_DELAY_MS = 200L;
+ private static final long OVER_SWEEP_ANIMATION_DURATION_MS = 200L;
+
+ private static final float STROKE_WIDTH_DP = 12f;
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+ @NonNull private final Rect mBounds;
+ @NonNull private final Runnable mInvalidateRunnable;
+ private final float mStartAngle;
+ private final float mSweepAngle;
+ private final float mMaxOverSweepAngle;
+ private final float mStrokeWidthPx;
+ @ColorInt private final int mProgressColor;
+ @ColorInt private final int mHelpColor;
+
+ @NonNull private final Paint mBackgroundPaint;
+ @NonNull private final Paint mProgressPaint;
+
+ private float mProgress = 0f;
+ private float mAnimatedProgress = 0f;
+ @Nullable private ValueAnimator mProgressAnimator;
+ @NonNull private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener;
+
+ private boolean mIsShowingHelp = false;
+ @Nullable private ValueAnimator mFillColorAnimator;
+ @NonNull private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener;
+
+ private float mOverSweepAngle = 0f;
+ @Nullable private ValueAnimator mOverSweepAnimator;
+ @Nullable private ValueAnimator mOverSweepReverseAnimator;
+ @NonNull private final ValueAnimator.AnimatorUpdateListener mOverSweepUpdateListener;
+ @NonNull private final Runnable mOverSweepAnimationRunnable;
+
+ public UdfpsEnrollProgressBarSegment(@NonNull Context context, @NonNull Rect bounds,
+ float startAngle, float sweepAngle, float maxOverSweepAngle,
+ @NonNull Runnable invalidateRunnable) {
+
+ mBounds = bounds;
+ mInvalidateRunnable = invalidateRunnable;
+ mStartAngle = startAngle;
+ mSweepAngle = sweepAngle;
+ mMaxOverSweepAngle = maxOverSweepAngle;
+ mStrokeWidthPx = Utils.dpToPixels(context, STROKE_WIDTH_DP);
+ mProgressColor = context.getColor(R.color.udfps_enroll_progress);
+ mHelpColor = context.getColor(R.color.udfps_enroll_progress_help);
+
+ mBackgroundPaint = new Paint();
+ mBackgroundPaint.setStrokeWidth(mStrokeWidthPx);
+ mBackgroundPaint.setColor(context.getColor(R.color.white_disabled));
+ mBackgroundPaint.setAntiAlias(true);
+ mBackgroundPaint.setStyle(Paint.Style.STROKE);
+ mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ // Background paint color + alpha
+ final int[] attrs = new int[] {android.R.attr.colorControlNormal};
+ final TypedArray ta = context.obtainStyledAttributes(attrs);
+ @ColorInt final int tintColor = ta.getColor(0, mBackgroundPaint.getColor());
+ mBackgroundPaint.setColor(tintColor);
+ ta.recycle();
+ TypedValue alpha = new TypedValue();
+ context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, alpha, true);
+ mBackgroundPaint.setAlpha((int) (alpha.getFloat() * 255f));
+
+ // Progress should not be color extracted
+ mProgressPaint = new Paint();
+ mProgressPaint.setStrokeWidth(mStrokeWidthPx);
+ mProgressPaint.setColor(mProgressColor);
+ mProgressPaint.setAntiAlias(true);
+ mProgressPaint.setStyle(Paint.Style.STROKE);
+ mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ mProgressUpdateListener = animation -> {
+ mAnimatedProgress = (float) animation.getAnimatedValue();
+ mInvalidateRunnable.run();
+ };
+
+ mFillColorUpdateListener = animation -> {
+ mProgressPaint.setColor((int) animation.getAnimatedValue());
+ mInvalidateRunnable.run();
+ };
+
+ mOverSweepUpdateListener = animation -> {
+ mOverSweepAngle = (float) animation.getAnimatedValue();
+ mInvalidateRunnable.run();
+ };
+ mOverSweepAnimationRunnable = () -> {
+ if (mOverSweepAnimator != null && mOverSweepAnimator.isRunning()) {
+ mOverSweepAnimator.cancel();
+ }
+ mOverSweepAnimator = ValueAnimator.ofFloat(mOverSweepAngle, mMaxOverSweepAngle);
+ mOverSweepAnimator.setDuration(OVER_SWEEP_ANIMATION_DURATION_MS);
+ mOverSweepAnimator.addUpdateListener(mOverSweepUpdateListener);
+ mOverSweepAnimator.start();
+ };
+ }
+
+ /**
+ * Draws this segment to the given canvas.
+ */
+ public void draw(@NonNull Canvas canvas) {
+ final float halfPaddingPx = mStrokeWidthPx / 2f;
+
+ if (mAnimatedProgress < 1f) {
+ // Draw the unfilled background color of the segment.
+ canvas.drawArc(
+ halfPaddingPx,
+ halfPaddingPx,
+ mBounds.right - halfPaddingPx,
+ mBounds.bottom - halfPaddingPx,
+ mStartAngle,
+ mSweepAngle,
+ false /* useCenter */,
+ mBackgroundPaint);
+ }
+
+ if (mAnimatedProgress > 0f) {
+ // Draw the filled progress portion of the segment.
+ canvas.drawArc(
+ halfPaddingPx,
+ halfPaddingPx,
+ mBounds.right - halfPaddingPx,
+ mBounds.bottom - halfPaddingPx,
+ mStartAngle,
+ mSweepAngle * mAnimatedProgress + mOverSweepAngle,
+ false /* useCenter */,
+ mProgressPaint);
+ }
+ }
+
+ /**
+ * @return The fill progress of this segment, in the range [0, 1]. If fill progress is being
+ * animated, returns the value it is animating to.
+ */
+ public float getProgress() {
+ return mProgress;
+ }
+
+ /**
+ * Updates the fill progress of this segment, animating if necessary.
+ *
+ * @param progress The new fill progress, in the range [0, 1].
+ */
+ public void updateProgress(float progress) {
+ updateProgress(progress, PROGRESS_ANIMATION_DURATION_MS);
+ }
+
+ private void updateProgress(float progress, long animationDurationMs) {
+ if (mProgress == progress) {
+ return;
+ }
+ mProgress = progress;
+
+ if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
+ mProgressAnimator.cancel();
+ }
+
+ mProgressAnimator = ValueAnimator.ofFloat(mAnimatedProgress, progress);
+ mProgressAnimator.setDuration(animationDurationMs);
+ mProgressAnimator.addUpdateListener(mProgressUpdateListener);
+ mProgressAnimator.start();
+ }
+
+ /**
+ * Updates the fill color of this segment, animating if necessary.
+ *
+ * @param isShowingHelp Whether fill color should indicate that a help message is being shown.
+ */
+ public void updateFillColor(boolean isShowingHelp) {
+ if (mIsShowingHelp == isShowingHelp) {
+ return;
+ }
+ mIsShowingHelp = isShowingHelp;
+
+ if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) {
+ mFillColorAnimator.cancel();
+ }
+
+ @ColorInt final int targetColor = isShowingHelp ? mHelpColor : mProgressColor;
+ mFillColorAnimator = ValueAnimator.ofArgb(mProgressPaint.getColor(), targetColor);
+ mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
+ mFillColorAnimator.addUpdateListener(mFillColorUpdateListener);
+ mFillColorAnimator.start();
+ }
+
+ /**
+ * Queues and runs the completion animation for this segment.
+ */
+ public void startCompletionAnimation() {
+ final boolean hasCallback = mHandler.hasCallbacks(mOverSweepAnimationRunnable);
+ if (hasCallback || mOverSweepAngle >= mMaxOverSweepAngle) {
+ Log.d(TAG, "startCompletionAnimation skipped: hasCallback = " + hasCallback
+ + ", mOverSweepAngle = " + mOverSweepAngle);
+ return;
+ }
+
+ // Reset sweep angle back to zero if the animation is being rolled back.
+ if (mOverSweepReverseAnimator != null && mOverSweepReverseAnimator.isRunning()) {
+ mOverSweepReverseAnimator.cancel();
+ mOverSweepAngle = 0f;
+ }
+
+ // Clear help color and start filling the segment if it isn't already.
+ if (mAnimatedProgress < 1f) {
+ updateProgress(1f, OVER_SWEEP_ANIMATION_DELAY_MS);
+ updateFillColor(false /* isShowingHelp */);
+ }
+
+ // Queue the animation to run after fill completes.
+ mHandler.postDelayed(mOverSweepAnimationRunnable, OVER_SWEEP_ANIMATION_DELAY_MS);
+ }
+
+ /**
+ * Cancels (and reverses, if necessary) a queued or running completion animation.
+ */
+ public void cancelCompletionAnimation() {
+ // Cancel the animation if it's queued or running.
+ mHandler.removeCallbacks(mOverSweepAnimationRunnable);
+ if (mOverSweepAnimator != null && mOverSweepAnimator.isRunning()) {
+ mOverSweepAnimator.cancel();
+ }
+
+ // Roll back the animation if it has at least partially run.
+ if (mOverSweepAngle > 0f) {
+ if (mOverSweepReverseAnimator != null && mOverSweepReverseAnimator.isRunning()) {
+ mOverSweepReverseAnimator.cancel();
+ }
+
+ final float completion = mOverSweepAngle / mMaxOverSweepAngle;
+ final long proratedDuration = (long) (OVER_SWEEP_ANIMATION_DURATION_MS * completion);
+ mOverSweepReverseAnimator = ValueAnimator.ofFloat(mOverSweepAngle, 0f);
+ mOverSweepReverseAnimator.setDuration(proratedDuration);
+ mOverSweepReverseAnimator.addUpdateListener(mOverSweepUpdateListener);
+ mOverSweepReverseAnimator.start();
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
index c83006d..729838e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
@@ -73,23 +73,22 @@
}
void setEnrollHelper(UdfpsEnrollHelper enrollHelper) {
+ mFingerprintProgressDrawable.setEnrollHelper(enrollHelper);
mFingerprintDrawable.setEnrollHelper(enrollHelper);
}
void onEnrollmentProgress(int remaining, int totalSteps) {
mHandler.post(() -> {
- mFingerprintProgressDrawable.setEnrollmentProgress(remaining, totalSteps);
+ mFingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps);
mFingerprintDrawable.onEnrollmentProgress(remaining, totalSteps);
});
}
- void onLastStepAcquired() {
- mHandler.post(() -> {
- mFingerprintProgressDrawable.onLastStepAcquired();
- });
+ void onEnrollmentHelp(int remaining, int totalSteps) {
+ mHandler.post(() -> mFingerprintProgressDrawable.onEnrollmentHelp(remaining, totalSteps));
}
- void onEnrollmentHelp() {
- mHandler.post(mFingerprintProgressDrawable::onEnrollmentHelp);
+ void onLastStepAcquired() {
+ mHandler.post(mFingerprintProgressDrawable::onLastStepAcquired);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
index 33fbe7b..af7c352 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
@@ -35,21 +35,21 @@
@NonNull private final UdfpsEnrollHelper mEnrollHelper;
@NonNull private final UdfpsEnrollHelper.Listener mEnrollHelperListener =
new UdfpsEnrollHelper.Listener() {
- @Override
- public void onEnrollmentProgress(int remaining, int totalSteps) {
- mView.onEnrollmentProgress(remaining, totalSteps);
- }
+ @Override
+ public void onEnrollmentProgress(int remaining, int totalSteps) {
+ mView.onEnrollmentProgress(remaining, totalSteps);
+ }
- @Override
- public void onLastStepAcquired() {
- mView.onLastStepAcquired();
- }
+ @Override
+ public void onEnrollmentHelp(int remaining, int totalSteps) {
+ mView.onEnrollmentHelp(remaining, totalSteps);
+ }
- @Override
- public void onEnrollmentHelp() {
- mView.onEnrollmentHelp();
- }
- };
+ @Override
+ public void onLastStepAcquired() {
+ mView.onLastStepAcquired();
+ }
+ };
protected UdfpsEnrollViewController(
@NonNull UdfpsEnrollView view,
@@ -81,7 +81,7 @@
@NonNull
@Override
public PointF getTouchTranslation() {
- if (!mEnrollHelper.isCenterEnrollmentComplete()) {
+ if (!mEnrollHelper.isGuidedEnrollmentStage()) {
return new PointF(0, 0);
} else {
return mEnrollHelper.getNextGuidedEnrollmentPoint();
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 43315f6..8576a28 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -712,6 +712,7 @@
mHandler.removeCallbacks(mAutoDim);
mHandler.removeCallbacks(mOnVariableDurationHomeLongClick);
mHandler.removeCallbacks(mEnableLayoutTransitions);
+ mNavigationBarA11yHelper.removeA11yEventListener(mAccessibilityListener);
mFrame = null;
mNavigationBarView = null;
mOrientationHandle = null;
@@ -1391,10 +1392,11 @@
void updateAccessibilityServicesState() {
int a11yFlags = mNavigationBarA11yHelper.getA11yButtonState();
- boolean clickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
- boolean longClickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;
- mNavigationBarView.setAccessibilityButtonState(clickable, longClickable);
-
+ if (mNavigationBarView != null) {
+ boolean clickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
+ boolean longClickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;
+ mNavigationBarView.setAccessibilityButtonState(clickable, longClickable);
+ }
updateSystemUiStateFlags(a11yFlags);
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index 0e32e9d..71d2a73 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -71,7 +71,6 @@
private int mDisabledFlags;
private @WindowVisibleState int mTaskBarWindowState = WINDOW_STATE_SHOWING;
private @Behavior int mBehavior;
- private boolean mTaskbarVisible = false;
@Inject
public TaskbarDelegate(Context context) {
@@ -98,7 +97,6 @@
mNavigationModeController.removeListener(this);
mNavigationBarA11yHelper.removeA11yEventListener(mNavA11yEventListener);
mEdgeBackGestureHandler.onNavBarDetached();
- mTaskbarVisible = false;
}
public void init(int displayId) {
@@ -109,7 +107,6 @@
mNavigationModeController.addListener(this));
mNavigationBarA11yHelper.registerA11yEventListener(mNavA11yEventListener);
mEdgeBackGestureHandler.onNavBarAttached();
- mTaskbarVisible = true;
// Set initial state for any listeners
updateSysuiFlags();
}
@@ -185,19 +182,6 @@
}
@Override
- public void onTaskbarStatusUpdated(boolean visible, boolean stashed) {
- if (mTaskbarVisible == visible) {
- return;
- }
- mTaskbarVisible = visible;
- if (visible) {
- mEdgeBackGestureHandler.onNavBarAttached();
- } else {
- mEdgeBackGestureHandler.onNavBarDetached();
- }
- }
-
- @Override
public void onNavigationModeChanged(int mode) {
mEdgeBackGestureHandler.onNavigationModeChanged(mode);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index 23c9408f..bfb63ea 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -409,21 +409,8 @@
}
if (mAllowFancy) {
- // Make brightness appear static position and alpha in through second half.
- View brightness = mQsPanelController.getBrightnessView();
- if (brightness != null) {
- firstPageBuilder.addFloat(brightness, "translationY",
- brightness.getMeasuredHeight() * 0.5f, 0);
- mBrightnessAnimator = new TouchAnimator.Builder()
- .addFloat(brightness, "alpha", 0, 1)
- .addFloat(brightness, "sliderScaleY", 0.3f, 1)
- .setInterpolator(Interpolators.ALPHA_IN)
- .setStartDelay(0.3f)
- .build();
- mAllViews.add(brightness);
- } else {
- mBrightnessAnimator = null;
- }
+ animateBrightnessSlider(firstPageBuilder);
+
mFirstPageAnimator = firstPageBuilder
.setListener(this)
.build();
@@ -474,20 +461,53 @@
.addFloat(tileLayout, "alpha", 0, 1).build();
}
+ private void animateBrightnessSlider(Builder firstPageBuilder) {
+ View qsBrightness = mQsPanelController.getBrightnessView();
+ View qqsBrightness = mQuickQSPanelController.getBrightnessView();
+ if (qqsBrightness != null && qqsBrightness.getVisibility() == View.VISIBLE) {
+ // animating in split shade mode
+ mAnimatedQsViews.add(qsBrightness);
+ mAllViews.add(qqsBrightness);
+ int translationY = getRelativeTranslationY(qsBrightness, qqsBrightness);
+ mBrightnessAnimator = new Builder()
+ // we need to animate qs brightness even if animation will not be visible,
+ // as we might start from sliderScaleY set to 0.3 if device was in collapsed QS
+ // portrait orientation before
+ .addFloat(qsBrightness, "sliderScaleY", 0.3f, 1)
+ .addFloat(qqsBrightness, "translationY", 0, translationY)
+ .build();
+ } else if (qsBrightness != null) {
+ firstPageBuilder.addFloat(qsBrightness, "translationY",
+ qsBrightness.getMeasuredHeight() * 0.5f, 0);
+ mBrightnessAnimator = new Builder()
+ .addFloat(qsBrightness, "alpha", 0, 1)
+ .addFloat(qsBrightness, "sliderScaleY", 0.3f, 1)
+ .setInterpolator(Interpolators.ALPHA_IN)
+ .setStartDelay(0.3f)
+ .build();
+ mAllViews.add(qsBrightness);
+ } else {
+ mBrightnessAnimator = null;
+ }
+ }
+
private void updateQQSFooterAnimation() {
- int[] qsPosition = new int[2];
- int[] qqsPosition = new int[2];
- View commonView = mQs.getView();
- getRelativePositionInt(qsPosition, mQSFooterActions, commonView);
- getRelativePositionInt(qqsPosition, mQQSFooterActions, commonView);
- int translationY = (qsPosition[1] - qqsPosition[1])
- - mQuickStatusBarHeader.getOffsetTranslation();
+ int translationY = getRelativeTranslationY(mQSFooterActions, mQQSFooterActions);
mQQSFooterActionsAnimator = new TouchAnimator.Builder()
.addFloat(mQQSFooterActions, "translationY", 0, translationY)
.build();
mAnimatedQsViews.add(mQSFooterActions);
}
+ private int getRelativeTranslationY(View view1, View view2) {
+ int[] qsPosition = new int[2];
+ int[] qqsPosition = new int[2];
+ View commonView = mQs.getView();
+ getRelativePositionInt(qsPosition, view1, commonView);
+ getRelativePositionInt(qqsPosition, view2, commonView);
+ return (qsPosition[1] - qqsPosition[1]) - mQuickStatusBarHeader.getOffsetTranslation();
+ }
+
private boolean isIconInAnimatedRow(int count) {
if (mPagedLayout == null) {
return false;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 9025427..70892a7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -258,10 +258,6 @@
return mView.isLayoutRtl();
}
- public View getBrightnessView() {
- return mView.getBrightnessView();
- }
-
/** */
public void setPageListener(PagedTileLayout.PageListener listener) {
mView.setPageListener(listener);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 42323e3..97568f9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -24,6 +24,7 @@
import android.content.ComponentName;
import android.content.res.Configuration;
import android.metrics.LogMaker;
+import android.view.View;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
@@ -405,6 +406,10 @@
mUsingHorizontalLayoutChangedListener = listener;
}
+ public View getBrightnessView() {
+ return mView.getBrightnessView();
+ }
+
/** */
public static final class TileRecord extends QSPanel.Record {
public QSTile tile;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
index f09d7b7..7e9f84c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
@@ -19,8 +19,9 @@
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.View.OVER_SCROLL_ALWAYS;
import static android.view.View.OVER_SCROLL_NEVER;
+import static android.view.WindowInsets.Type.displayCutout;
import static android.view.WindowInsets.Type.ime;
-import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.systemBars;
import static com.google.common.truth.Truth.assertThat;
@@ -39,6 +40,7 @@
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Insets;
+import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.LayerDrawable;
import android.testing.AndroidTestingRunner;
@@ -98,14 +100,15 @@
private AccessibilityFloatingMenuView mMenuView;
private RecyclerView mListView = new RecyclerView(mContext);
- private int mScreenHeight;
private int mMenuWindowHeight;
private int mMenuHalfWidth;
private int mMenuHalfHeight;
- private int mScreenHalfWidth;
- private int mScreenHalfHeight;
+ private int mDisplayHalfWidth;
+ private int mDisplayHalfHeight;
private int mMaxWindowX;
private int mMaxWindowY;
+ private final int mDisplayWindowWidth = 1080;
+ private final int mDisplayWindowHeight = 2340;
@Before
public void initMenuView() {
@@ -113,7 +116,10 @@
doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
mWindowManager).getMaximumWindowMetrics();
mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
-
+ when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+ when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, mDisplayWindowWidth,
+ mDisplayWindowHeight));
+ when(mWindowMetrics.getWindowInsets()).thenReturn(fakeDisplayInsets());
mMenuView = spy(
new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, mListView));
}
@@ -129,18 +135,16 @@
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_width_height);
final int menuWidth = padding * 2 + iconWidthHeight;
final int menuHeight = (padding + iconWidthHeight) * mTargets.size() + padding;
- final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
- mScreenHeight = mContext.getResources().getDisplayMetrics().heightPixels;
mMenuHalfWidth = menuWidth / 2;
mMenuHalfHeight = menuHeight / 2;
- mScreenHalfWidth = screenWidth / 2;
- mScreenHalfHeight = mScreenHeight / 2;
+ mDisplayHalfWidth = mDisplayWindowWidth / 2;
+ mDisplayHalfHeight = mDisplayWindowHeight / 2;
int marginStartEnd =
mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT
? margin : 0;
- mMaxWindowX = screenWidth - marginStartEnd - menuWidth;
+ mMaxWindowX = mDisplayWindowWidth - marginStartEnd - menuWidth;
mMenuWindowHeight = menuHeight + margin * 2;
- mMaxWindowY = mScreenHeight - mMenuWindowHeight;
+ mMaxWindowY = mDisplayWindowHeight - mMenuWindowHeight;
}
@Test
@@ -279,15 +283,15 @@
final MotionEvent moveEvent =
mMotionEventHelper.obtainMotionEvent(2, 3,
MotionEvent.ACTION_MOVE,
- /* screenCenterX */mScreenHalfWidth
- - /* offsetXToScreenLeftHalfRegion */ 10,
- /* screenCenterY */ mScreenHalfHeight);
+ /* displayCenterX */mDisplayHalfWidth
+ - /* offsetXToDisplayLeftHalfRegion */ 10,
+ /* displayCenterY */ mDisplayHalfHeight);
final MotionEvent upEvent =
mMotionEventHelper.obtainMotionEvent(4, 5,
MotionEvent.ACTION_UP,
- /* screenCenterX */ mScreenHalfWidth
- - /* offsetXToScreenLeftHalfRegion */ 10,
- /* screenCenterY */ mScreenHalfHeight);
+ /* displayCenterX */ mDisplayHalfWidth
+ - /* offsetXToDisplayLeftHalfRegion */ 10,
+ /* displayCenterY */ mDisplayHalfHeight);
listView.dispatchTouchEvent(downEvent);
listView.dispatchTouchEvent(moveEvent);
@@ -315,15 +319,15 @@
final MotionEvent moveEvent =
mMotionEventHelper.obtainMotionEvent(2, 3,
MotionEvent.ACTION_MOVE,
- /* screenCenterX */mScreenHalfWidth
- + /* offsetXToScreenRightHalfRegion */ 10,
- /* screenCenterY */ mScreenHalfHeight);
+ /* displayCenterX */mDisplayHalfWidth
+ + /* offsetXToDisplayRightHalfRegion */ 10,
+ /* displayCenterY */ mDisplayHalfHeight);
final MotionEvent upEvent =
mMotionEventHelper.obtainMotionEvent(4, 5,
MotionEvent.ACTION_UP,
- /* screenCenterX */ mScreenHalfWidth
- + /* offsetXToScreenRightHalfRegion */ 10,
- /* screenCenterY */ mScreenHalfHeight);
+ /* displayCenterX */ mDisplayHalfWidth
+ + /* offsetXToDisplayRightHalfRegion */ 10,
+ /* displayCenterY */ mDisplayHalfHeight);
listView.dispatchTouchEvent(downEvent);
listView.dispatchTouchEvent(moveEvent);
@@ -332,12 +336,12 @@
assertThat((float) menuView.mCurrentLayoutParams.x).isWithin(1.0f).of(mMaxWindowX);
assertThat((float) menuView.mCurrentLayoutParams.y).isWithin(1.0f).of(
- /* newWindowY = screenCenterY - offsetY */ mScreenHalfHeight - mMenuHalfHeight);
+ /* newWindowY = displayCenterY - offsetY */ mDisplayHalfHeight - mMenuHalfHeight);
}
@Test
- public void tapOnAndDragMenuToScreenSide_transformShapeHalfOval() {
+ public void tapOnAndDragMenuToDisplaySide_transformShapeHalfOval() {
final Position alignRightPosition = new Position(1.0f, 0.8f);
final RecyclerView listView = new RecyclerView(mContext);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
@@ -355,13 +359,13 @@
mMotionEventHelper.obtainMotionEvent(2, 3,
MotionEvent.ACTION_MOVE,
/* downX */(currentWindowX + mMenuHalfWidth)
- + /* offsetXToScreenRightSide */ mMenuHalfWidth,
+ + /* offsetXToDisplayRightSide */ mMenuHalfWidth,
/* downY */ (currentWindowY + mMenuHalfHeight));
final MotionEvent upEvent =
mMotionEventHelper.obtainMotionEvent(4, 5,
MotionEvent.ACTION_UP,
/* downX */(currentWindowX + mMenuHalfWidth)
- + /* offsetXToScreenRightSide */ mMenuHalfWidth,
+ + /* offsetXToDisplayRightSide */ mMenuHalfWidth,
/* downY */ (currentWindowY + mMenuHalfHeight));
listView.dispatchTouchEvent(downEvent);
@@ -423,7 +427,7 @@
}
@Test
- public void showMenuAndIme_withHigherIme_alignScreenTopEdge() {
+ public void showMenuAndIme_withHigherIme_alignDisplayTopEdge() {
final int offset = 99999;
setupBasicMenuView(mMenuView);
@@ -475,10 +479,21 @@
private WindowInsets fakeImeInsetWith(AccessibilityFloatingMenuView menuView, int offset) {
// Ensure the keyboard has overlapped on the menu view.
final int fakeImeHeight =
- mScreenHeight - (menuView.mCurrentLayoutParams.y + mMenuWindowHeight) + offset;
+ mDisplayWindowHeight - (menuView.mCurrentLayoutParams.y + mMenuWindowHeight)
+ + offset;
return new WindowInsets.Builder()
- .setVisible(ime() | navigationBars(), true)
- .setInsets(ime() | navigationBars(), Insets.of(0, 0, 0, fakeImeHeight))
+ .setVisible(ime(), true)
+ .setInsets(ime(), Insets.of(0, 0, 0, fakeImeHeight))
+ .build();
+ }
+
+ private WindowInsets fakeDisplayInsets() {
+ final int fakeStatusBarHeight = 75;
+ final int fakeNavigationBarHeight = 125;
+ return new WindowInsets.Builder()
+ .setVisible(systemBars() | displayCutout(), true)
+ .setInsets(systemBars() | displayCutout(),
+ Insets.of(0, fakeStatusBarHeight, 0, fakeNavigationBarHeight))
.build();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
index eb1f15b..3553a0a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
@@ -23,12 +23,16 @@
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.Context;
+import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.MotionEvent;
+import android.view.WindowInsets;
import android.view.WindowManager;
+import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.test.filters.SmallTest;
@@ -52,6 +56,9 @@
@Mock
private WindowManager mWindowManager;
+ @Mock
+ private WindowMetrics mWindowMetrics;
+
private AccessibilityFloatingMenuView mMenuView;
private BaseTooltipView mToolTipView;
@@ -66,6 +73,9 @@
doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
mWindowManager).getMaximumWindowMetrics();
mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+ when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+ when(mWindowMetrics.getBounds()).thenReturn(new Rect());
+ when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
mMenuView = new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition);
mToolTipView = new BaseTooltipView(mContext, mMenuView);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
index ca4e3e9..9eba49d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
@@ -21,12 +21,16 @@
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.Context;
+import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.MotionEvent;
+import android.view.WindowInsets;
import android.view.WindowManager;
+import android.view.WindowMetrics;
import androidx.test.filters.SmallTest;
@@ -49,6 +53,9 @@
@Mock
private WindowManager mWindowManager;
+ @Mock
+ private WindowMetrics mWindowMetrics;
+
private AccessibilityFloatingMenuView mMenuView;
private DockTooltipView mDockTooltipView;
private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
@@ -62,6 +69,9 @@
doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
mWindowManager).getMaximumWindowMetrics();
mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+ when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+ when(mWindowMetrics.getBounds()).thenReturn(new Rect());
+ when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
mMenuView = spy(new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition));
mDockTooltipView = new DockTooltipView(mContext, mMenuView);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java
index dae4364..ea104a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java
@@ -22,12 +22,15 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.Context;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.view.WindowInsets;
import android.view.WindowManager;
+import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
@@ -57,6 +60,8 @@
@Mock
private WindowManager mWindowManager;
+ @Mock
+ private WindowMetrics mWindowMetrics;
private RecyclerView mListView;
private AccessibilityFloatingMenuView mMenuView;
private ItemDelegateCompat mItemDelegateCompat;
@@ -69,6 +74,9 @@
doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
mWindowManager).getMaximumWindowMetrics();
mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+ when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+ when(mWindowMetrics.getBounds()).thenReturn(new Rect());
+ when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
mListView = new RecyclerView(mContext);
mMenuView =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index ed3c473..36228dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -56,6 +56,7 @@
import android.view.Display;
import android.view.DisplayInfo;
import android.view.MotionEvent;
+import android.view.View;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityManager;
@@ -122,6 +123,8 @@
EdgeBackGestureHandler.Factory mEdgeBackGestureHandlerFactory;
@Mock
EdgeBackGestureHandler mEdgeBackGestureHandler;
+ @Mock
+ NavigationBarA11yHelper mNavigationBarA11yHelper;
@Rule
public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck();
@@ -256,6 +259,20 @@
assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
}
+ @Test
+ public void testA11yEventAfterDetach() {
+ View v = mNavigationBar.createView(null);
+ mNavigationBar.onViewAttachedToWindow(v);
+ verify(mNavigationBarA11yHelper).registerA11yEventListener(any(
+ NavigationBarA11yHelper.NavA11yEventListener.class));
+ mNavigationBar.onViewDetachedFromWindow(v);
+ verify(mNavigationBarA11yHelper).removeA11yEventListener(any(
+ NavigationBarA11yHelper.NavA11yEventListener.class));
+
+ // Should be safe even though the internal view is now null.
+ mNavigationBar.updateAccessibilityServicesState();
+ }
+
private NavigationBar createNavBar(Context context) {
DeviceProvisionedController deviceProvisionedController =
mock(DeviceProvisionedController.class);
@@ -287,7 +304,7 @@
mHandler,
mock(NavigationBarOverlayController.class),
mUiEventLogger,
- mock(NavigationBarA11yHelper.class),
+ mNavigationBarA11yHelper,
mock(UserTracker.class)));
}
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java
index 9f3045e..3a90a95 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java
@@ -87,7 +87,7 @@
mId = sessionId;
mUid = uid;
mContentCaptureContext = new ContentCaptureContext(/* clientContext= */ null,
- activityId, appComponentName, displayId, flags);
+ activityId, appComponentName, displayId, activityToken, flags);
mSessionStateReceiver = sessionStateReceiver;
try {
sessionStateReceiver.asBinder().linkToDeath(() -> onClientDeath(), 0);
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 55b982b..e5716ee 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -149,7 +149,7 @@
"android.hardware.biometrics.fingerprint-V2.3-java",
"android.hardware.biometrics.fingerprint-V1-java",
"android.hardware.oemlock-V1.0-java",
- "android.hardware.configstore-V1.0-java",
+ "android.hardware.configstore-V1.1-java",
"android.hardware.contexthub-V1.0-java",
"android.hardware.rebootescrow-V1-java",
"android.hardware.soundtrigger-V2.3-java",
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 36c0de9..ad0485b 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -1033,6 +1033,7 @@
mService.setProcessTrackerStateLOSP(app, trackerMemFactor, now);
state.setProcStateChanged(false);
}
+ trimMemoryUiHiddenIfNecessaryLSP(app);
if (curProcState >= ActivityManager.PROCESS_STATE_HOME && !app.isKilledByAm()) {
if (trimMemoryLevel < curLevel[0] && (thread = app.getThread()) != null) {
try {
@@ -1075,24 +1076,6 @@
}
profile.setTrimMemoryLevel(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND);
} else {
- if ((curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
- || state.isSystemNoUi()) && profile.hasPendingUiClean()) {
- // If this application is now in the background and it
- // had done UI, then give it the special trim level to
- // have it free UI resources.
- final int level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN;
- if (trimMemoryLevel < level && (thread = app.getThread()) != null) {
- try {
- if (DEBUG_SWITCH || DEBUG_OOM_ADJ) {
- Slog.v(TAG_OOM_ADJ, "Trimming memory of bg-ui "
- + app.processName + " to " + level);
- }
- thread.scheduleTrimMemory(level);
- } catch (RemoteException e) {
- }
- }
- profile.setPendingUiClean(false);
- }
if (trimMemoryLevel < fgTrimLevel && (thread = app.getThread()) != null) {
try {
if (DEBUG_SWITCH || DEBUG_OOM_ADJ) {
@@ -1119,28 +1102,36 @@
mService.setProcessTrackerStateLOSP(app, trackerMemFactor, now);
state.setProcStateChanged(false);
}
- if ((state.getCurProcState() >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
- || state.isSystemNoUi()) && profile.hasPendingUiClean()) {
- if (profile.getTrimMemoryLevel() < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
- && (thread = app.getThread()) != null) {
- try {
- if (DEBUG_SWITCH || DEBUG_OOM_ADJ) {
- Slog.v(TAG_OOM_ADJ,
- "Trimming memory of ui hidden " + app.processName
- + " to " + ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
- }
- thread.scheduleTrimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
- } catch (RemoteException e) {
- }
- }
- profile.setPendingUiClean(false);
- }
+ trimMemoryUiHiddenIfNecessaryLSP(app);
profile.setTrimMemoryLevel(0);
});
}
return allChanged;
}
+ @GuardedBy({"mService", "mProcLock"})
+ private void trimMemoryUiHiddenIfNecessaryLSP(ProcessRecord app) {
+ if ((app.mState.getCurProcState() >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
+ || app.mState.isSystemNoUi()) && app.mProfile.hasPendingUiClean()) {
+ // If this application is now in the background and it
+ // had done UI, then give it the special trim level to
+ // have it free UI resources.
+ final int level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN;
+ IApplicationThread thread;
+ if (app.mProfile.getTrimMemoryLevel() < level && (thread = app.getThread()) != null) {
+ try {
+ if (DEBUG_SWITCH || DEBUG_OOM_ADJ) {
+ Slog.v(TAG_OOM_ADJ, "Trimming memory of bg-ui "
+ + app.processName + " to " + level);
+ }
+ thread.scheduleTrimMemory(level);
+ } catch (RemoteException e) {
+ }
+ }
+ app.mProfile.setPendingUiClean(false);
+ }
+ }
+
@GuardedBy("mProcLock")
long getLowRamTimeSinceIdleLPr(long now) {
return mLowRamTimeSinceLastIdle + (mLowRamStartTime > 0 ? (now - mLowRamStartTime) : 0);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index e383c56..8c19284 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -243,7 +243,7 @@
*/
private static final int FLAG_ADJUST_VOLUME = 1;
- private final Context mContext;
+ final Context mContext;
private final ContentResolver mContentResolver;
private final AppOpsManager mAppOps;
@@ -318,6 +318,8 @@
private static final int MSG_A2DP_DEV_CONFIG_CHANGE = 39;
private static final int MSG_DISPATCH_AUDIO_MODE = 40;
private static final int MSG_ROUTING_UPDATED = 41;
+ private static final int MSG_INIT_HEADTRACKING_SENSORS = 42;
+ private static final int MSG_PERSIST_SPATIAL_AUDIO_ENABLED = 43;
// start of messages handled under wakelock
// these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(),
@@ -1037,12 +1039,13 @@
mMonitorRotation = SystemProperties.getBoolean("ro.audio.monitorRotation", false);
+ mHasSpatializerEffect = SystemProperties.getBoolean("ro.audio.spatializer_enabled", false);
+
// done with service initialization, continue additional work in our Handler thread
queueMsgUnderWakeLock(mAudioHandler, MSG_INIT_STREAMS_VOLUMES,
0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */);
queueMsgUnderWakeLock(mAudioHandler, MSG_INIT_SPATIALIZER,
- 0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */);
-
+ 0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */);
}
/**
@@ -1236,6 +1239,9 @@
// routing monitoring from AudioSystemAdapter
@Override
public void onRoutingUpdatedFromNative() {
+ if (!mHasSpatializerEffect) {
+ return;
+ }
sendMsg(mAudioHandler,
MSG_ROUTING_UPDATED,
SENDMSG_REPLACE, 0, 0, null,
@@ -1432,8 +1438,10 @@
}
}
- // TODO check property if feature enabled
- mSpatializerHelper.reset(/* featureEnabled */ SPATIALIZER_FEATURE_ENABLED_DEFAULT);
+ if (mHasSpatializerEffect) {
+ mSpatializerHelper.reset(/* featureEnabled */ isSpatialAudioEnabled());
+ monitorRoutingChanges(true);
+ }
onIndicateSystemReady();
// indicate the end of reconfiguration phase to audio HAL
@@ -7569,12 +7577,18 @@
break;
case MSG_INIT_SPATIALIZER:
- mSpatializerHelper.init();
- // TODO read property to see if enabled
- mSpatializerHelper.setFeatureEnabled(SPATIALIZER_FEATURE_ENABLED_DEFAULT);
+ mSpatializerHelper.init(/*effectExpected*/ mHasSpatializerEffect);
+ if (mHasSpatializerEffect) {
+ mSpatializerHelper.setFeatureEnabled(isSpatialAudioEnabled());
+ monitorRoutingChanges(true);
+ }
mAudioEventWakeLock.release();
break;
+ case MSG_INIT_HEADTRACKING_SENSORS:
+ mSpatializerHelper.onInitSensors(/*init*/ msg.arg1 == 1);
+ break;
+
case MSG_CHECK_MUSIC_ACTIVE:
onCheckMusicActive((String) msg.obj);
break;
@@ -7711,6 +7725,10 @@
case MSG_ROUTING_UPDATED:
mSpatializerHelper.onRoutingUpdated();
break;
+
+ case MSG_PERSIST_SPATIAL_AUDIO_ENABLED:
+ onPersistSpatialAudioEnabled(msg.arg1 == 1);
+ break;
}
}
}
@@ -8280,7 +8298,40 @@
//==========================================================================================
private final @NonNull SpatializerHelper mSpatializerHelper;
- private static final boolean SPATIALIZER_FEATURE_ENABLED_DEFAULT = false;
+ /**
+ * Initialized from property ro.audio.spatializer_enabled
+ * Should only be 1 when the device ships with a Spatializer effect
+ */
+ private final boolean mHasSpatializerEffect;
+ /**
+ * Default value for the spatial audio feature
+ */
+ private static final boolean SPATIAL_AUDIO_ENABLED_DEFAULT = true;
+
+ /**
+ * persist in user settings whether the feature is enabled.
+ * Can change when {@link Spatializer#setEnabled(boolean)} is called and successfully
+ * changes the state of the feature
+ * @param featureEnabled
+ */
+ void persistSpatialAudioEnabled(boolean featureEnabled) {
+ sendMsg(mAudioHandler,
+ MSG_PERSIST_SPATIAL_AUDIO_ENABLED,
+ SENDMSG_REPLACE, featureEnabled ? 1 : 0, 0, null,
+ /*delay ms*/ 100);
+ }
+
+ void onPersistSpatialAudioEnabled(boolean enabled) {
+ Settings.Secure.putIntForUser(mContentResolver,
+ Settings.Secure.SPATIAL_AUDIO_ENABLED, enabled ? 1 : 0,
+ UserHandle.USER_CURRENT);
+ }
+
+ boolean isSpatialAudioEnabled() {
+ return Settings.Secure.getIntForUser(mContentResolver,
+ Settings.Secure.SPATIAL_AUDIO_ENABLED, SPATIAL_AUDIO_ENABLED_DEFAULT ? 1 : 0,
+ UserHandle.USER_CURRENT) == 1;
+ }
private void enforceModifyDefaultAudioEffectsPermission() {
if (mContext.checkCallingOrSelfPermission(
@@ -8447,6 +8498,18 @@
mSpatializerHelper.getEffectParameter(key, value);
}
+ /**
+ * post a message to schedule init/release of head tracking sensors
+ * @param init initialization if true, release if false
+ */
+ void postInitSpatializerHeadTrackingSensors(boolean init) {
+ sendMsg(mAudioHandler,
+ MSG_INIT_HEADTRACKING_SENSORS,
+ SENDMSG_REPLACE,
+ /*arg1*/ init ? 1 : 0,
+ 0, TAG, /*delay*/ 0);
+ }
+
//==========================================================================================
private boolean readCameraSoundForced() {
return SystemProperties.getBoolean("audio.camerasound.force", false) ||
@@ -9027,6 +9090,12 @@
sVolumeLogger.dump(pw);
pw.println("\n");
dumpSupportedSystemUsage(pw);
+
+ pw.println("\n");
+ pw.println("\nSpatial audio:");
+ pw.println("mHasSpatializerEffect:" + mHasSpatializerEffect);
+ pw.println("isSpatializerEnabled:" + isSpatializerEnabled());
+ pw.println("isSpatialAudioEnabled:" + isSpatialAudioEnabled());
}
private void dumpSupportedSystemUsage(PrintWriter pw) {
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index ccaa96d..b2fa86b 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -18,6 +18,9 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioFormat;
@@ -28,6 +31,7 @@
import android.media.ISpatializerHeadToSoundStagePoseCallback;
import android.media.ISpatializerHeadTrackingCallback;
import android.media.ISpatializerHeadTrackingModeCallback;
+import android.media.SpatializationLevel;
import android.media.Spatializer;
import android.media.SpatializerHeadTrackingMode;
import android.os.RemoteCallbackList;
@@ -45,6 +49,7 @@
private static final String TAG = "AS.SpatializerHelper";
private static final boolean DEBUG = true;
+ private static final boolean DEBUG_MORE = false;
private static void logd(String s) {
if (DEBUG) {
@@ -54,6 +59,7 @@
private final @NonNull AudioSystemAdapter mASA;
private final @NonNull AudioService mAudioService;
+ private @Nullable SensorManager mSensorManager;
//------------------------------------------------------------
// Spatializer state machine
@@ -100,8 +106,13 @@
mASA = asa;
}
- synchronized void init() {
+ synchronized void init(boolean effectExpected) {
Log.i(TAG, "Initializing");
+ if (!effectExpected) {
+ Log.i(TAG, "Setting state to STATE_NOT_SUPPORTED due to effect not expected");
+ mState = STATE_NOT_SUPPORTED;
+ return;
+ }
if (mState != STATE_UNINITIALIZED) {
throw new IllegalStateException(("init() called in state:" + mState));
}
@@ -127,7 +138,7 @@
for (byte level : levels) {
logd("found support for level: " + level);
if (level == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL) {
- logd("Setting Spatializer to LEVEL_MULTICHANNEL");
+ logd("Setting capable level to LEVEL_MULTICHANNEL");
mCapableSpatLevel = level;
break;
}
@@ -159,7 +170,7 @@
mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
- init();
+ init(true);
setFeatureEnabled(featureEnabled);
}
@@ -191,9 +202,16 @@
public void onLevelChanged(byte level) {
logd("SpatializerCallback.onLevelChanged level:" + level);
synchronized (SpatializerHelper.this) {
- mSpatLevel = level;
+ mSpatLevel = spatializationLevelToSpatializerInt(level);
}
// TODO use reported spat level to change state
+
+ // init sensors
+ if (level == SpatializationLevel.NONE) {
+ initSensors(/*init*/false);
+ } else {
+ postInitSensors(true);
+ }
}
};
@@ -224,7 +242,7 @@
+ " invalid transform length" + headToStage.length);
return;
}
- if (DEBUG) {
+ if (DEBUG_MORE) {
// 6 values * (4 digits + 1 dot + 2 brackets) = 42 characters
StringBuilder t = new StringBuilder(42);
for (float val : headToStage) {
@@ -378,7 +396,7 @@
}
}
mStateCallbacks.finishBroadcast();
- // TODO persist enabled state
+ mAudioService.persistSpatialAudioEnabled(featureEnabled);
}
private synchronized void setDispatchAvailableState(boolean available) {
@@ -641,36 +659,6 @@
}
}
- private int headTrackingModeTypeToSpatializerInt(byte mode) {
- switch (mode) {
- case SpatializerHeadTrackingMode.OTHER:
- return Spatializer.HEAD_TRACKING_MODE_OTHER;
- case SpatializerHeadTrackingMode.DISABLED:
- return Spatializer.HEAD_TRACKING_MODE_DISABLED;
- case SpatializerHeadTrackingMode.RELATIVE_WORLD:
- return Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD;
- case SpatializerHeadTrackingMode.RELATIVE_SCREEN:
- return Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE;
- default:
- throw(new IllegalArgumentException("Unexpected head tracking mode:" + mode));
- }
- }
-
- private byte spatializerIntToHeadTrackingModeType(int sdkMode) {
- switch (sdkMode) {
- case Spatializer.HEAD_TRACKING_MODE_OTHER:
- return SpatializerHeadTrackingMode.OTHER;
- case Spatializer.HEAD_TRACKING_MODE_DISABLED:
- return SpatializerHeadTrackingMode.DISABLED;
- case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD:
- return SpatializerHeadTrackingMode.RELATIVE_WORLD;
- case Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE:
- return SpatializerHeadTrackingMode.RELATIVE_SCREEN;
- default:
- throw(new IllegalArgumentException("Unexpected head tracking mode:" + sdkMode));
- }
- }
-
private boolean checkSpatForHeadTracking(String funcName) {
switch (mState) {
case STATE_UNINITIALIZED:
@@ -792,4 +780,101 @@
Log.e(TAG, "Error in getParameter for key:" + key, e);
}
}
+
+ //------------------------------------------------------
+ // sensors
+ private void initSensors(boolean init) {
+ if (mSensorManager == null) {
+ mSensorManager = (SensorManager)
+ mAudioService.mContext.getSystemService(Context.SENSOR_SERVICE);
+ }
+ final int headHandle;
+ final int screenHandle;
+ if (init) {
+ if (mSensorManager == null) {
+ Log.e(TAG, "Null SensorManager, can't init sensors");
+ return;
+ }
+ // TODO replace with dynamic association of sensor for headtracker
+ Sensor headSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR);
+ headHandle = headSensor.getHandle();
+ //Sensor screenSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
+ //screenHandle = deviceSensor.getHandle();
+ screenHandle = -1;
+ } else {
+ // -1 is disable value
+ screenHandle = -1;
+ headHandle = -1;
+ }
+ try {
+ Log.i(TAG, "setScreenSensor:" + screenHandle);
+ mSpat.setScreenSensor(screenHandle);
+ } catch (Exception e) {
+ Log.e(TAG, "Error calling setScreenSensor:" + screenHandle, e);
+ }
+ try {
+ Log.i(TAG, "setHeadSensor:" + headHandle);
+ mSpat.setHeadSensor(headHandle);
+ } catch (Exception e) {
+ Log.e(TAG, "Error calling setHeadSensor:" + headHandle, e);
+ }
+ }
+
+ private void postInitSensors(boolean init) {
+ mAudioService.postInitSpatializerHeadTrackingSensors(init);
+ }
+
+ synchronized void onInitSensors(boolean init) {
+ final int[] modes = getSupportedHeadTrackingModes();
+ if (modes.length == 0) {
+ Log.i(TAG, "not initializing sensors, no headtracking supported");
+ return;
+ }
+ initSensors(init);
+ }
+
+ //------------------------------------------------------
+ // SDK <-> AIDL converters
+ private static int headTrackingModeTypeToSpatializerInt(byte mode) {
+ switch (mode) {
+ case SpatializerHeadTrackingMode.OTHER:
+ return Spatializer.HEAD_TRACKING_MODE_OTHER;
+ case SpatializerHeadTrackingMode.DISABLED:
+ return Spatializer.HEAD_TRACKING_MODE_DISABLED;
+ case SpatializerHeadTrackingMode.RELATIVE_WORLD:
+ return Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD;
+ case SpatializerHeadTrackingMode.RELATIVE_SCREEN:
+ return Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE;
+ default:
+ throw(new IllegalArgumentException("Unexpected head tracking mode:" + mode));
+ }
+ }
+
+ private static byte spatializerIntToHeadTrackingModeType(int sdkMode) {
+ switch (sdkMode) {
+ case Spatializer.HEAD_TRACKING_MODE_OTHER:
+ return SpatializerHeadTrackingMode.OTHER;
+ case Spatializer.HEAD_TRACKING_MODE_DISABLED:
+ return SpatializerHeadTrackingMode.DISABLED;
+ case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD:
+ return SpatializerHeadTrackingMode.RELATIVE_WORLD;
+ case Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE:
+ return SpatializerHeadTrackingMode.RELATIVE_SCREEN;
+ default:
+ throw(new IllegalArgumentException("Unexpected head tracking mode:" + sdkMode));
+ }
+ }
+
+ private static int spatializationLevelToSpatializerInt(byte level) {
+ switch (level) {
+ case SpatializationLevel.NONE:
+ return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+ case SpatializationLevel.SPATIALIZER_MULTICHANNEL:
+ return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL;
+ case SpatializationLevel.SPATIALIZER_MCHAN_BED_PLUS_OBJECTS:
+ return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MCHAN_BED_PLUS_OBJECTS;
+ default:
+ throw(new IllegalArgumentException("Unexpected spatializer level:" + level));
+ }
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 8a11e8d..ca051e9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -167,14 +167,17 @@
@Override
protected void stopHalOperation() {
mSensorOverlays.hide(getSensorId());
-
- try {
- mCancellationSignal.cancel();
- } catch (RemoteException e) {
- Slog.e(TAG, "Remote exception", e);
- onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
- 0 /* vendorCode */);
- mCallback.onClientFinished(this, false /* success */);
+ if (mCancellationSignal != null) {
+ try {
+ mCancellationSignal.cancel();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote exception", e);
+ onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
+ 0 /* vendorCode */);
+ mCallback.onClientFinished(this, false /* success */);
+ }
+ } else {
+ Slog.e(TAG, "cancellation signal was null");
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 71ac730..bab5a61 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -16,7 +16,6 @@
package com.android.server.wm;
-import static android.Manifest.permission.ACTIVITY_EMBEDDING;
import static android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND;
import static android.app.Activity.RESULT_CANCELED;
import static android.app.ActivityManager.START_ABORTED;
@@ -1953,38 +1952,43 @@
}
}
- if (mInTaskFragment != null && mInTaskFragment.getTask() != null) {
- final int hostUid = mInTaskFragment.getTask().effectiveUid;
- final int embeddingUid = targetTask != null ? targetTask.effectiveUid : r.getUid();
- if (!canTaskBeEmbedded(hostUid, embeddingUid)) {
- Slog.e(TAG, "Cannot embed activity to a task owned by " + hostUid + " targetTask= "
- + targetTask);
- return START_PERMISSION_DENIED;
- }
+ if (mInTaskFragment != null && !canEmbedActivity(mInTaskFragment, r, newTask, targetTask)) {
+ Slog.e(TAG, "Permission denied: Cannot embed " + r + " to " + mInTaskFragment.getTask()
+ + " targetTask= " + targetTask);
+ return START_PERMISSION_DENIED;
}
return START_SUCCESS;
}
/**
- * Return {@code true} if the {@param task} can embed another task.
- * @param hostUid the uid of the host task
- * @param embeddedUid the uid of the task the are going to be embedded
+ * Return {@code true} if an activity can be embedded to the TaskFragment.
+ * @param taskFragment the TaskFragment for embedding.
+ * @param starting the starting activity.
+ * @param newTask whether the starting activity is going to be launched on a new task.
+ * @param targetTask the target task for launching activity, which could be different from
+ * the one who hosting the embedding.
*/
- private boolean canTaskBeEmbedded(int hostUid, int embeddedUid) {
+ private boolean canEmbedActivity(@NonNull TaskFragment taskFragment, ActivityRecord starting,
+ boolean newTask, Task targetTask) {
+ final Task hostTask = taskFragment.getTask();
+ if (hostTask == null) {
+ return false;
+ }
+
// Allowing the embedding if the task is owned by system.
+ final int hostUid = hostTask.effectiveUid;
if (hostUid == Process.SYSTEM_UID) {
return true;
}
- // Allowing embedding if the host task is owned by an app that has the ACTIVITY_EMBEDDING
- // permission
- if (mService.checkPermission(ACTIVITY_EMBEDDING, -1, hostUid) == PERMISSION_GRANTED) {
- return true;
+ // Not allowed embedding an activity of another app.
+ if (hostUid != starting.getUid()) {
+ return false;
}
- // Allowing embedding if it is from the same app that owned the task
- return hostUid == embeddedUid;
+ // Not allowed embedding task.
+ return !newTask && (targetTask == null || targetTask == hostTask);
}
/**
@@ -2801,10 +2805,15 @@
newParent = mInTaskFragment;
}
} else {
- // Use the child TaskFragment (if any) as the new parent if the activity can be embedded
final ActivityRecord top = task.topRunningActivity(false /* focusableOnly */,
false /* includingEmbeddedTask */);
- newParent = top != null ? top.getTaskFragment() : task;
+ final TaskFragment taskFragment = top != null ? top.getTaskFragment() : null;
+ if (taskFragment != null && taskFragment.isEmbedded()
+ && task.effectiveUid == mStartActivity.getUid()) {
+ // Use the embedded TaskFragment of the top activity as the new parent if the
+ // activity can be embedded.
+ newParent = top.getTaskFragment();
+ }
}
if (mStartActivity.getTaskFragment() == null
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index fed4f62..852a658 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -107,8 +107,9 @@
final TaskFragment childTaskFragment = child.asTaskFragment();
childTaskFragment.updateActivityVisibilities(starting, configChanges,
preserveWindows, notifyClients);
- mBehindFullyOccludedContainer |= childTaskFragment.getBounds().equals(
- mTaskFragment.getBounds());
+ mBehindFullyOccludedContainer |=
+ childTaskFragment.topRunningActivity() != null
+ && childTaskFragment.getBounds().equals(mTaskFragment.getBounds());
if (mAboveTop && mTop.getTaskFragment() == childTaskFragment) {
mAboveTop = false;
}
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 584f7bf..a902ca9 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2046,9 +2046,7 @@
if (shouldStartChangeTransition(mTmpPrevBounds)) {
initializeChangeTransition(mTmpPrevBounds);
- }
-
- if (mTaskFragmentOrganizer != null) {
+ } else if (mTaskFragmentOrganizer != null) {
// Update the surface position here instead of in the organizer so that we can make sure
// it can be synced with the surface freezer.
updateSurfacePosition(getSyncTransaction());
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 63951d9..5ce9938 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -168,6 +168,9 @@
*/
@VisibleForTesting
void addSkipClosingAppSnapshotTasks(ArraySet<Task> tasks) {
+ if (shouldDisableSnapshots()) {
+ return;
+ }
mSkipClosingAppSnapshotTasks.addAll(tasks);
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index c5d7179..b9c6e1b 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -172,8 +172,10 @@
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
-import android.hardware.configstore.V1_0.ISurfaceFlingerConfigs;
import android.hardware.configstore.V1_0.OptionalBool;
+import android.hardware.configstore.V1_1.DisplayOrientation;
+import android.hardware.configstore.V1_1.ISurfaceFlingerConfigs;
+import android.hardware.configstore.V1_1.OptionalDisplayOrientation;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerInternal;
import android.hardware.input.InputManager;
@@ -225,6 +227,7 @@
import android.util.proto.ProtoOutputStream;
import android.view.Choreographer;
import android.view.Display;
+import android.view.DisplayAddress;
import android.view.DisplayInfo;
import android.view.Gravity;
import android.view.IAppTransitionAnimationSpecsFuture;
@@ -450,6 +453,8 @@
*/
static final boolean ENABLE_FIXED_ROTATION_TRANSFORM =
SystemProperties.getBoolean("persist.wm.fixed_rotation_transform", true);
+ private @Surface.Rotation int mPrimaryDisplayOrientation = Surface.ROTATION_0;
+ private DisplayAddress mPrimaryDisplayPhysicalAddress;
// Enums for animation scale update types.
@Retention(RetentionPolicy.SOURCE)
@@ -2459,16 +2464,21 @@
configChanged = displayContent.updateOrientation();
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- final DisplayInfo rotatedDisplayInfo =
- win.mToken.getFixedRotationTransformDisplayInfo();
- if (rotatedDisplayInfo != null) {
- outSurfaceControl.setTransformHint(rotatedDisplayInfo.rotation);
- } else {
- // We have to update the transform hint of display here, but we need to get if from
- // SurfaceFlinger, so set it as rotation of display for most cases, then
- // SurfaceFlinger would still update the transform hint of display in next frame.
- outSurfaceControl.setTransformHint(displayContent.getDisplayInfo().rotation);
+ final DisplayInfo displayInfo = win.getDisplayInfo();
+ int transformHint = displayInfo.rotation;
+ // If the window is on the primary display, use the panel orientation to adjust the
+ // transform hint
+ final boolean isPrimaryDisplay = displayInfo.address != null &&
+ displayInfo.address.equals(mPrimaryDisplayPhysicalAddress);
+ if (isPrimaryDisplay) {
+ transformHint = (transformHint + mPrimaryDisplayOrientation) % 4;
}
+ outSurfaceControl.setTransformHint(transformHint);
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Passing transform hint %d for window %s%s",
+ transformHint, win,
+ isPrimaryDisplay ? " on primary display with orientation "
+ + mPrimaryDisplayOrientation : "");
if (toBeDisplayed && win.mIsWallpaper) {
displayContent.mWallpaperController.updateWallpaperOffset(win, false /* sync */);
@@ -4874,6 +4884,9 @@
mTaskSnapshotController.systemReady();
mHasWideColorGamutSupport = queryWideColorGamutSupport();
mHasHdrSupport = queryHdrSupport();
+ mPrimaryDisplayOrientation = queryPrimaryDisplayOrientation();
+ mPrimaryDisplayPhysicalAddress =
+ DisplayAddress.fromPhysicalDisplayId(SurfaceControl.getPrimaryPhysicalDisplayId());
UiThread.getHandler().post(mSettingsObserver::loadSettings);
IVrManager vrManager = IVrManager.Stub.asInterface(
ServiceManager.getService(Context.VR_SERVICE));
@@ -4893,6 +4906,9 @@
}
}
+
+ // Keep logic in sync with SurfaceFlingerProperties.cpp
+ // Consider exposing properties via ISurfaceComposer instead.
private static boolean queryWideColorGamutSupport() {
boolean defaultValue = false;
Optional<Boolean> hasWideColorProp = SurfaceFlingerProperties.has_wide_color_display();
@@ -4933,6 +4949,39 @@
return false;
}
+ private static @Surface.Rotation int queryPrimaryDisplayOrientation() {
+ Optional<SurfaceFlingerProperties.primary_display_orientation_values> prop =
+ SurfaceFlingerProperties.primary_display_orientation();
+ if (prop.isPresent()) {
+ switch (prop.get()) {
+ case ORIENTATION_90: return Surface.ROTATION_90;
+ case ORIENTATION_180: return Surface.ROTATION_180;
+ case ORIENTATION_270: return Surface.ROTATION_270;
+ case ORIENTATION_0:
+ default:
+ return Surface.ROTATION_0;
+ }
+ }
+ try {
+ ISurfaceFlingerConfigs surfaceFlinger = ISurfaceFlingerConfigs.getService();
+ OptionalDisplayOrientation primaryDisplayOrientation =
+ surfaceFlinger.primaryDisplayOrientation();
+ if (primaryDisplayOrientation != null && primaryDisplayOrientation.specified) {
+ switch (primaryDisplayOrientation.value) {
+ case DisplayOrientation.ORIENTATION_90: return Surface.ROTATION_90;
+ case DisplayOrientation.ORIENTATION_180: return Surface.ROTATION_180;
+ case DisplayOrientation.ORIENTATION_270: return Surface.ROTATION_270;
+ case DisplayOrientation.ORIENTATION_0:
+ default:
+ return Surface.ROTATION_0;
+ }
+ }
+ } catch (Exception e) {
+ // Use default value if we can't talk to config store.
+ }
+ return Surface.ROTATION_0;
+ }
+
void reportFocusChanged(IBinder oldToken, IBinder newToken) {
WindowState lastFocus;
WindowState newFocus;
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index dd70a14..d4d8971 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -250,10 +250,10 @@
// away.
if (mLastHidden && mDrawState != NO_SURFACE && !forceApplyNow) {
mPostDrawTransaction.merge(postDrawTransaction);
- layoutNeeded = true;
} else {
mWin.getSyncTransaction().merge(postDrawTransaction);
}
+ layoutNeeded = true;
}
return layoutNeeded;
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java
index b4fbf5f..184ea52 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java
@@ -110,18 +110,20 @@
@Override
public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) {
- t.setVisibility(leash, true /* visible */).apply();
+ t.show(leash).apply();
}
int cookieIndex = -1;
if (trampoline.equals(taskInfo.baseActivity)) {
cookieIndex = 0;
} else if (main.equals(taskInfo.baseActivity)) {
cookieIndex = 1;
- mainLatch.countDown();
}
if (cookieIndex >= 0) {
appearedCookies[cookieIndex] = taskInfo.launchCookies.isEmpty()
? null : taskInfo.launchCookies.get(0);
+ if (cookieIndex == 1) {
+ mainLatch.countDown();
+ }
}
}
};
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt
index fe1b1cd..b37c404 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt
@@ -18,6 +18,7 @@
import android.app.Instrumentation
import android.content.ComponentName
+import android.os.SystemProperties
import android.platform.test.annotations.Presubmit
import android.view.Surface
import android.view.WindowManagerPolicyConstants
@@ -43,6 +44,7 @@
import com.android.server.wm.flicker.statusBarLayerRotatesScales
import com.android.server.wm.flicker.statusBarWindowIsVisible
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -61,6 +63,8 @@
class ReOpenImeWindowTest(private val testSpec: FlickerTestParameter) {
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.config.startRotation)
+ private val isShellTransitionsEnabled =
+ SystemProperties.getBoolean("persist.debug.shell_transit", false)
@FlickerBuilderProvider
fun buildFlicker(): FlickerBuilder {
@@ -122,15 +126,17 @@
@Presubmit
@Test
- fun imeWindowIsAlwaysVisible() = testSpec.imeWindowIsAlwaysVisible(true)
+ fun imeWindowIsAlwaysVisible() = testSpec.imeWindowIsAlwaysVisible(!isShellTransitionsEnabled)
@Presubmit
@Test
- fun imeAppWindowVisibility() {
- // the app starts visible in live tile, then becomes invisible during animation and
- // is again launched. Since we log 1x per frame, sometimes the activity visibility and
- // the app visibility are updated together, sometimes not, thus ignore activity check
- // at the start
+ fun imeAppWindowVisibilityLegacy() {
+ Assume.assumeFalse(isShellTransitionsEnabled)
+ // the app starts visible in live tile, and stays visible for the duration of entering
+ // and exiting overview. However, legacy transitions seem to have a bug which causes
+ // everything to restart during the test, so expect the app to disappear and come back.
+ // Since we log 1x per frame, sometimes the activity visibility and the app visibility
+ // are updated together, sometimes not, thus ignore activity check at the start
testSpec.assertWm {
this.isAppWindowVisible(testApp.component, ignoreActivity = true)
.then()
@@ -142,6 +148,19 @@
@Presubmit
@Test
+ fun imeAppWindowVisibility() {
+ Assume.assumeTrue(isShellTransitionsEnabled)
+ // the app starts visible in live tile, and stays visible for the duration of entering
+ // and exiting overview. Since we log 1x per frame, sometimes the activity visibility
+ // and the app visibility are updated together, sometimes not, thus ignore activity
+ // check at the start
+ testSpec.assertWm {
+ this.isAppWindowVisible(testApp.component, ignoreActivity = true)
+ }
+ }
+
+ @Presubmit
+ @Test
// During testing the launcher is always in portrait mode
fun entireScreenCovered() = testSpec.entireScreenCovered()
@@ -155,7 +174,8 @@
@Presubmit
@Test
- fun imeLayerIsBecomesVisible() {
+ fun imeLayerIsBecomesVisibleLegacy() {
+ Assume.assumeFalse(isShellTransitionsEnabled)
testSpec.assertLayers {
this.isVisible(WindowManagerStateHelper.IME_COMPONENT)
.then()
@@ -167,6 +187,15 @@
@Presubmit
@Test
+ fun imeLayerIsBecomesVisible() {
+ Assume.assumeTrue(isShellTransitionsEnabled)
+ testSpec.assertLayers {
+ this.isVisible(WindowManagerStateHelper.IME_COMPONENT)
+ }
+ }
+
+ @Presubmit
+ @Test
fun appLayerReplacesLauncher() {
testSpec.assertLayers {
this.isVisible(LAUNCHER_COMPONENT)