Merge "Navigate directly to second page from pinned entry" into main
diff --git a/Android.bp b/Android.bp
index 13b1703..f6a9328 100644
--- a/Android.bp
+++ b/Android.bp
@@ -220,6 +220,7 @@
"updatable-driver-protos",
"ota_metadata_proto_java",
"android.hidl.base-V1.0-java",
+ "android.hidl.manager-V1.2-java",
"android.hardware.cas-V1-java", // AIDL
"android.hardware.cas-V1.0-java",
"android.hardware.cas-V1.1-java",
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index c65e506..de6f023 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -12,4 +12,11 @@
namespace: "backstage_power"
description: "Throw an exception if an unsupported app uses JobInfo.setBias"
bug: "300477393"
-}
\ No newline at end of file
+}
+
+flag {
+ name: "batch_jobs_on_network_activation"
+ namespace: "backstage_power"
+ description: "Have JobScheduler attempt to delay the start of some connectivity jobs until the network is actually active"
+ bug: "318394184"
+}
diff --git a/core/api/current.txt b/core/api/current.txt
index 7ab234a..95af71c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4369,7 +4369,7 @@
method public final android.media.session.MediaController getMediaController();
method @NonNull public android.view.MenuInflater getMenuInflater();
method @NonNull public android.window.OnBackInvokedDispatcher getOnBackInvokedDispatcher();
- method public final android.app.Activity getParent();
+ method @Deprecated public final android.app.Activity getParent();
method @Nullable public android.content.Intent getParentActivityIntent();
method public android.content.SharedPreferences getPreferences(int);
method @Nullable public android.net.Uri getReferrer();
@@ -4387,7 +4387,7 @@
method public void invalidateOptionsMenu();
method public boolean isActivityTransitionRunning();
method public boolean isChangingConfigurations();
- method public final boolean isChild();
+ method @Deprecated public final boolean isChild();
method public boolean isDestroyed();
method public boolean isFinishing();
method public boolean isImmersive();
diff --git a/core/java/android/accounts/AbstractAccountAuthenticator.java b/core/java/android/accounts/AbstractAccountAuthenticator.java
index 45515dd..c1c5c0e 100644
--- a/core/java/android/accounts/AbstractAccountAuthenticator.java
+++ b/core/java/android/accounts/AbstractAccountAuthenticator.java
@@ -142,10 +142,7 @@
private static final String KEY_ACCOUNT =
"android.accounts.AbstractAccountAuthenticator.KEY_ACCOUNT";
- private final Context mContext;
-
public AbstractAccountAuthenticator(Context context) {
- mContext = context;
}
private class Transport extends IAccountAuthenticator.Stub {
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 5674a10..5d4d5e2 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1174,12 +1174,23 @@
return mApplication;
}
- /** Is this activity embedded inside of another activity? */
+ /**
+ * Whether this is a child {@link Activity} of an {@link ActivityGroup}.
+ *
+ * @deprecated {@link ActivityGroup} is deprecated.
+ */
+ @Deprecated
public final boolean isChild() {
return mParent != null;
}
- /** Return the parent activity if this view is an embedded child. */
+ /**
+ * Returns the parent {@link Activity} if this is a child {@link Activity} of an
+ * {@link ActivityGroup}.
+ *
+ * @deprecated {@link ActivityGroup} is deprecated.
+ */
+ @Deprecated
public final Activity getParent() {
return mParent;
}
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index 343348b..f9ab55e 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -70,6 +70,8 @@
public static final int TYPE_SCHEDULE_CALENDAR = 2;
/**
* The type for rules triggered by bedtime/sleeping, like time of day, or snore detection.
+ *
+ * <p>Only the 'Wellbeing' app may own rules of this type.
*/
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int TYPE_BEDTIME = 3;
@@ -95,6 +97,8 @@
/**
* The type for rules created and managed by a device owner. These rules may not be fully
* editable by the device user.
+ *
+ * <p>Only a 'Device Owner' app may own rules of this type.
*/
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int TYPE_MANAGED = 7;
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 4a6349b1..5c42b0e 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -2598,8 +2598,8 @@
* There can be at most one app that has this delegation.
* If another app already had delegated certificate selection access,
* it will lose the delegation when a new app is delegated.
- * <p> The delegaetd app can also call {@link #grantKeyPairToApp} and
- * {@link #revokeKeyPairFromApp} to directly grant KeyCain keys to other apps.
+ * <p> The delegated app can also call {@link #grantKeyPairToApp} and
+ * {@link #revokeKeyPairFromApp} to directly grant KeyChain keys to other apps.
* <p> Can be granted by Device Owner or Profile Owner.
*/
public static final String DELEGATION_CERT_SELECTION = "delegation-cert-selection";
diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig
index ec2e5fe..084cba3 100644
--- a/core/java/android/appwidget/flags.aconfig
+++ b/core/java/android/appwidget/flags.aconfig
@@ -20,3 +20,10 @@
description: "Move state file IO to non-critical path"
bug: "312949280"
}
+
+flag {
+ name: "draw_data_parcel"
+ namespace: "app_widgets"
+ description: "Enable support for transporting draw instructions as data parcel"
+ bug: "286130467"
+}
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 18d3e5e..71698e4 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -127,6 +127,7 @@
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.inputmethod.InlineSuggestionsResponse;
@@ -388,6 +389,9 @@
private long mStylusHwSessionsTimeout = STYLUS_HANDWRITING_IDLE_TIMEOUT_MS;
private Runnable mStylusWindowIdleTimeoutRunnable;
private long mStylusWindowIdleTimeoutForTest;
+ /** Tracks last {@link MotionEvent#getToolType(int)} used for {@link MotionEvent#ACTION_DOWN}.
+ **/
+ private int mLastUsedToolType;
/**
* Returns whether {@link InputMethodService} is responsible for rendering the back button and
@@ -1005,7 +1009,7 @@
*/
@Override
public void updateEditorToolType(@ToolType int toolType) {
- onUpdateEditorToolType(toolType);
+ updateEditorToolTypeInternal(toolType);
}
/**
@@ -1249,6 +1253,14 @@
rootView.setSystemGestureExclusionRects(exclusionRects);
}
+ private void updateEditorToolTypeInternal(int toolType) {
+ if (Flags.useHandwritingListenerForTooltype()) {
+ mLastUsedToolType = toolType;
+ mInputEditorInfo.setInitialToolType(toolType);
+ }
+ onUpdateEditorToolType(toolType);
+ }
+
/**
* Concrete implementation of
* {@link AbstractInputMethodService.AbstractInputMethodSessionImpl} that provides
@@ -3110,6 +3122,9 @@
null /* icProto */);
mInputStarted = true;
mStartedInputConnection = ic;
+ if (Flags.useHandwritingListenerForTooltype()) {
+ editorInfo.setInitialToolType(mLastUsedToolType);
+ }
mInputEditorInfo = editorInfo;
initialize();
mInlineSuggestionSessionController.notifyOnStartInput(
@@ -3354,6 +3369,10 @@
* had not seen the event at all.
*/
public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (Flags.useHandwritingListenerForTooltype()) {
+ // any KeyEvent keyDown should reset last toolType.
+ updateEditorToolTypeInternal(MotionEvent.TOOL_TYPE_UNKNOWN);
+ }
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
final ExtractEditText eet = getExtractEditTextIfVisible();
if (eet != null && eet.handleBackInTextActionModeIfNeeded(event)) {
diff --git a/core/java/android/os/HwNoService.java b/core/java/android/os/HwNoService.java
index 117c3ad..0840314 100644
--- a/core/java/android/os/HwNoService.java
+++ b/core/java/android/os/HwNoService.java
@@ -16,37 +16,127 @@
package android.os;
+import android.hidl.manager.V1_2.IServiceManager;
+import android.util.Log;
+
+import java.util.ArrayList;
+
/**
* A fake hwservicemanager that is used locally when HIDL isn't supported on the device.
*
* @hide
*/
-final class HwNoService implements IHwBinder, IHwInterface {
- /** @hide */
- @Override
- public void transact(int code, HwParcel request, HwParcel reply, int flags) {}
+final class HwNoService extends IServiceManager.Stub implements IHwBinder, IHwInterface {
+ private static final String TAG = "HwNoService";
/** @hide */
@Override
- public IHwInterface queryLocalInterface(String descriptor) {
- return new HwNoService();
+ public String toString() {
+ return "[HwNoService]";
}
- /** @hide */
@Override
- public boolean linkToDeath(DeathRecipient recipient, long cookie) {
+ public android.hidl.base.V1_0.IBase get(String fqName, String name)
+ throws android.os.RemoteException {
+ Log.i(TAG, "get " + fqName + "/" + name + " with no hwservicemanager");
+ return null;
+ }
+
+ @Override
+ public boolean add(String name, android.hidl.base.V1_0.IBase service)
+ throws android.os.RemoteException {
+ Log.i(TAG, "get " + name + " with no hwservicemanager");
+ return false;
+ }
+
+ @Override
+ public byte getTransport(String fqName, String name) throws android.os.RemoteException {
+ Log.i(TAG, "getTransoport " + fqName + "/" + name + " with no hwservicemanager");
+ return 0x0;
+ }
+
+ @Override
+ public java.util.ArrayList<String> list() throws android.os.RemoteException {
+ Log.i(TAG, "list with no hwservicemanager");
+ return new ArrayList<String>();
+ }
+
+ @Override
+ public java.util.ArrayList<String> listByInterface(String fqName)
+ throws android.os.RemoteException {
+ Log.i(TAG, "listByInterface with no hwservicemanager");
+ return new ArrayList<String>();
+ }
+
+ @Override
+ public boolean registerForNotifications(
+ String fqName, String name, android.hidl.manager.V1_0.IServiceNotification callback)
+ throws android.os.RemoteException {
+ Log.i(TAG, "registerForNotifications with no hwservicemanager");
return true;
}
- /** @hide */
@Override
- public boolean unlinkToDeath(DeathRecipient recipient) {
+ public ArrayList<android.hidl.manager.V1_0.IServiceManager.InstanceDebugInfo> debugDump()
+ throws android.os.RemoteException {
+ Log.i(TAG, "debugDump with no hwservicemanager");
+ return new ArrayList<android.hidl.manager.V1_0.IServiceManager.InstanceDebugInfo>();
+ }
+
+ @Override
+ public void registerPassthroughClient(String fqName, String name)
+ throws android.os.RemoteException {
+ Log.i(TAG, "registerPassthroughClient with no hwservicemanager");
+ }
+
+ @Override
+ public boolean unregisterForNotifications(
+ String fqName, String name, android.hidl.manager.V1_0.IServiceNotification callback)
+ throws android.os.RemoteException {
+ Log.i(TAG, "unregisterForNotifications with no hwservicemanager");
return true;
}
- /** @hide */
@Override
- public IHwBinder asBinder() {
- return this;
+ public boolean registerClientCallback(
+ String fqName,
+ String name,
+ android.hidl.base.V1_0.IBase server,
+ android.hidl.manager.V1_2.IClientCallback cb)
+ throws android.os.RemoteException {
+ Log.i(
+ TAG,
+ "registerClientCallback for " + fqName + "/" + name + " with no hwservicemanager");
+ return true;
+ }
+
+ @Override
+ public boolean unregisterClientCallback(
+ android.hidl.base.V1_0.IBase server, android.hidl.manager.V1_2.IClientCallback cb)
+ throws android.os.RemoteException {
+ Log.i(TAG, "unregisterClientCallback with no hwservicemanager");
+ return true;
+ }
+
+ @Override
+ public boolean addWithChain(
+ String name, android.hidl.base.V1_0.IBase service, java.util.ArrayList<String> chain)
+ throws android.os.RemoteException {
+ Log.i(TAG, "addWithChain with no hwservicemanager");
+ return true;
+ }
+
+ @Override
+ public java.util.ArrayList<String> listManifestByInterface(String fqName)
+ throws android.os.RemoteException {
+ Log.i(TAG, "listManifestByInterface for " + fqName + " with no hwservicemanager");
+ return new ArrayList<String>();
+ }
+
+ @Override
+ public boolean tryUnregister(String fqName, String name, android.hidl.base.V1_0.IBase service)
+ throws android.os.RemoteException {
+ Log.i(TAG, "tryUnregister for " + fqName + "/" + name + " with no hwservicemanager");
+ return true;
}
}
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 4e5588c..fe6c4a4 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -20,3 +20,10 @@
description: "Enables toasts when ASM restrictions are triggered"
bug: "230590090"
}
+
+flag {
+ name: "content_uri_permission_apis"
+ namespace: "responsible_apis"
+ description: "Enables the content URI permission APIs"
+ bug: "293467489"
+}
diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java
index ad0bf7c..7850554 100644
--- a/core/java/android/view/Surface.java
+++ b/core/java/android/view/Surface.java
@@ -274,7 +274,8 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"FRAME_RATE_CATEGORY_"},
value = {FRAME_RATE_CATEGORY_DEFAULT, FRAME_RATE_CATEGORY_NO_PREFERENCE,
- FRAME_RATE_CATEGORY_LOW, FRAME_RATE_CATEGORY_NORMAL, FRAME_RATE_CATEGORY_HIGH})
+ FRAME_RATE_CATEGORY_LOW, FRAME_RATE_CATEGORY_NORMAL,
+ FRAME_RATE_CATEGORY_HIGH_HINT, FRAME_RATE_CATEGORY_HIGH})
public @interface FrameRateCategory {}
// From native_window.h or window.h. Keep these in sync.
@@ -308,11 +309,21 @@
public static final int FRAME_RATE_CATEGORY_NORMAL = 3;
/**
+ * Hints that, as a result of a user interaction, an animation is likely to start.
+ * This category is a signal that a user interaction heuristic determined the need of a
+ * high refresh rate, and is not an explicit request from the app.
+ * As opposed to {@link #FRAME_RATE_CATEGORY_HIGH}, this vote may be ignored in favor of
+ * more explicit votes.
+ * @hide
+ */
+ public static final int FRAME_RATE_CATEGORY_HIGH_HINT = 4;
+
+ /**
* Indicates a frame rate suitable for animations that require a high frame rate, which may
* increase smoothness but may also increase power usage.
* @hide
*/
- public static final int FRAME_RATE_CATEGORY_HIGH = 4;
+ public static final int FRAME_RATE_CATEGORY_HIGH = 5;
/**
* Create an empty surface, which will later be filled in by readFromParcel().
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index a38092a..49d2ceb 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -2067,10 +2067,10 @@
}
/**
- * Start sequence (infinite) type of flash notification. Use
- * {@code Context.getOpPackageName()} as the identifier of this flash notification.
+ * Start sequence (infinite) type of flash notification. Use {@code Context} to retrieve the
+ * package name as the identifier of this flash notification.
* The notification can be cancelled later by calling {@link #stopFlashNotificationSequence}
- * with same {@code Context.getOpPackageName()}.
+ * with same {@code Context}.
* If the binder associated with this {@link AccessibilityManager} instance dies then the
* sequence will stop automatically. It is strongly recommended to call
* {@link #stopFlashNotificationSequence} within a reasonable amount of time after calling
@@ -2104,8 +2104,8 @@
}
/**
- * Stop sequence (infinite) type of flash notification. The flash notification with
- * {@code Context.getOpPackageName()} as identifier will be stopped if exist.
+ * Stop sequence (infinite) type of flash notification. The flash notification with the
+ * package name retrieved from {@code Context} as identifier will be stopped if exist.
* It is strongly recommended to call this method within a reasonable amount of time after
* calling {@link #startFlashNotificationSequence} method.
*
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index dc6aa6c..bb7677d6 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -38,3 +38,12 @@
description: "Feature flag for supporting stylus handwriting delegation from RemoteViews on the home screen"
bug: "279959705"
}
+
+flag {
+ name: "use_handwriting_listener_for_tooltype"
+ namespace: "input_method"
+ description: "Feature flag for using handwriting spy for determining pointer toolType."
+ bug: "309554999"
+ is_fixed_read_only: true
+}
+
diff --git a/core/java/android/window/ActivityWindowInfo.aidl b/core/java/android/window/ActivityWindowInfo.aidl
new file mode 100644
index 0000000..d0526bc
--- /dev/null
+++ b/core/java/android/window/ActivityWindowInfo.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+/**
+ * Stores information about a particular Activity Window.
+ * @hide
+ */
+parcelable ActivityWindowInfo;
diff --git a/core/java/android/window/ActivityWindowInfo.java b/core/java/android/window/ActivityWindowInfo.java
new file mode 100644
index 0000000..946bb82
--- /dev/null
+++ b/core/java/android/window/ActivityWindowInfo.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Stores the window information about a particular Activity.
+ * It contains the info that is not part of {@link android.content.res.Configuration}.
+ * @hide
+ */
+public final class ActivityWindowInfo implements Parcelable {
+
+ private boolean mIsEmbedded;
+
+ @NonNull
+ private final Rect mTaskBounds = new Rect();
+
+ @NonNull
+ private final Rect mTaskFragmentBounds = new Rect();
+
+ public ActivityWindowInfo() {}
+
+ public ActivityWindowInfo(@NonNull ActivityWindowInfo info) {
+ set(info);
+ }
+
+ /** Copies fields from {@code info}. */
+ public void set(@NonNull ActivityWindowInfo info) {
+ set(info.mIsEmbedded, info.mTaskBounds, info.mTaskFragmentBounds);
+ }
+
+ /** Sets to the given values. */
+ public void set(boolean isEmbedded, @NonNull Rect taskBounds,
+ @NonNull Rect taskFragmentBounds) {
+ mIsEmbedded = isEmbedded;
+ mTaskBounds.set(taskBounds);
+ mTaskFragmentBounds.set(taskFragmentBounds);
+ }
+
+ /**
+ * Whether this activity is embedded, which means it is a TaskFragment that doesn't fill the
+ * leaf Task.
+ */
+ public boolean isEmbedded() {
+ return mIsEmbedded;
+ }
+
+ /**
+ * The bounds of the leaf Task window in display space.
+ */
+ @NonNull
+ public Rect getTaskBounds() {
+ return mTaskBounds;
+ }
+
+ /**
+ * The bounds of the leaf TaskFragment window in display space.
+ * This can be referring to the bounds of the same window as {@link #getTaskBounds()} when
+ * the activity is not embedded.
+ */
+ @NonNull
+ public Rect getTaskFragmentBounds() {
+ return mTaskFragmentBounds;
+ }
+
+ private ActivityWindowInfo(@NonNull Parcel in) {
+ mIsEmbedded = in.readBoolean();
+ mTaskBounds.readFromParcel(in);
+ mTaskFragmentBounds.readFromParcel(in);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mIsEmbedded);
+ mTaskBounds.writeToParcel(dest, flags);
+ mTaskFragmentBounds.writeToParcel(dest, flags);
+ }
+
+ @NonNull
+ public static final Creator<ActivityWindowInfo> CREATOR =
+ new Creator<>() {
+ @Override
+ public ActivityWindowInfo createFromParcel(@NonNull Parcel in) {
+ return new ActivityWindowInfo(in);
+ }
+
+ @Override
+ public ActivityWindowInfo[] newArray(int size) {
+ return new ActivityWindowInfo[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final ActivityWindowInfo other = (ActivityWindowInfo) o;
+ return mIsEmbedded == other.mIsEmbedded
+ && mTaskBounds.equals(other.mTaskBounds)
+ && mTaskFragmentBounds.equals(other.mTaskFragmentBounds);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (mIsEmbedded ? 1 : 0);
+ result = 31 * result + mTaskBounds.hashCode();
+ result = 31 * result + mTaskFragmentBounds.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "ActivityWindowInfo{isEmbedded=" + mIsEmbedded
+ + ", taskBounds=" + mTaskBounds
+ + ", taskFragmentBounds=" + mTaskFragmentBounds
+ + "}";
+ }
+}
diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java
index 0ec9ffe..acc6a74 100644
--- a/core/java/android/window/TaskFragmentOperation.java
+++ b/core/java/android/window/TaskFragmentOperation.java
@@ -120,6 +120,11 @@
*/
public static final int OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE = 15;
+ /**
+ * Applies dimming on the parent Task which could cross two TaskFragments.
+ */
+ public static final int OP_TYPE_SET_DIM_ON_TASK = 16;
+
@IntDef(prefix = { "OP_TYPE_" }, value = {
OP_TYPE_UNKNOWN,
OP_TYPE_CREATE_TASK_FRAGMENT,
@@ -138,6 +143,7 @@
OP_TYPE_REORDER_TO_TOP_OF_TASK,
OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE,
OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE,
+ OP_TYPE_SET_DIM_ON_TASK,
})
@Retention(RetentionPolicy.SOURCE)
public @interface OperationType {}
@@ -165,12 +171,14 @@
private final boolean mIsolatedNav;
+ private final boolean mDimOnTask;
+
private TaskFragmentOperation(@OperationType int opType,
@Nullable TaskFragmentCreationParams taskFragmentCreationParams,
@Nullable IBinder activityToken, @Nullable Intent activityIntent,
@Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken,
@Nullable TaskFragmentAnimationParams animationParams,
- boolean isolatedNav) {
+ boolean isolatedNav, boolean dimOnTask) {
mOpType = opType;
mTaskFragmentCreationParams = taskFragmentCreationParams;
mActivityToken = activityToken;
@@ -179,6 +187,7 @@
mSecondaryFragmentToken = secondaryFragmentToken;
mAnimationParams = animationParams;
mIsolatedNav = isolatedNav;
+ mDimOnTask = dimOnTask;
}
private TaskFragmentOperation(Parcel in) {
@@ -190,6 +199,7 @@
mSecondaryFragmentToken = in.readStrongBinder();
mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR);
mIsolatedNav = in.readBoolean();
+ mDimOnTask = in.readBoolean();
}
@Override
@@ -202,6 +212,7 @@
dest.writeStrongBinder(mSecondaryFragmentToken);
dest.writeTypedObject(mAnimationParams, flags);
dest.writeBoolean(mIsolatedNav);
+ dest.writeBoolean(mDimOnTask);
}
@NonNull
@@ -282,6 +293,13 @@
return mIsolatedNav;
}
+ /**
+ * Returns whether the dim layer should apply on the parent Task.
+ */
+ public boolean isDimOnTask() {
+ return mDimOnTask;
+ }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
@@ -305,6 +323,7 @@
sb.append(", animationParams=").append(mAnimationParams);
}
sb.append(", isolatedNav=").append(mIsolatedNav);
+ sb.append(", dimOnTask=").append(mDimOnTask);
sb.append('}');
return sb.toString();
@@ -313,7 +332,7 @@
@Override
public int hashCode() {
return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent,
- mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav);
+ mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav, mDimOnTask);
}
@Override
@@ -329,7 +348,8 @@
&& Objects.equals(mBundle, other.mBundle)
&& Objects.equals(mSecondaryFragmentToken, other.mSecondaryFragmentToken)
&& Objects.equals(mAnimationParams, other.mAnimationParams)
- && mIsolatedNav == other.mIsolatedNav;
+ && mIsolatedNav == other.mIsolatedNav
+ && mDimOnTask == other.mDimOnTask;
}
@Override
@@ -363,6 +383,8 @@
private boolean mIsolatedNav;
+ private boolean mDimOnTask;
+
/**
* @param opType the {@link OperationType} of this {@link TaskFragmentOperation}.
*/
@@ -435,13 +457,22 @@
}
/**
+ * Sets the dimming to apply on the parent Task if any.
+ */
+ @NonNull
+ public Builder setDimOnTask(boolean dimOnTask) {
+ mDimOnTask = dimOnTask;
+ return this;
+ }
+
+ /**
* Constructs the {@link TaskFragmentOperation}.
*/
@NonNull
public TaskFragmentOperation build() {
return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken,
mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams,
- mIsolatedNav);
+ mIsolatedNav, mDimOnTask);
}
}
}
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 59d7b0e..f743ab7 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -38,4 +38,12 @@
name: "activity_embedding_interactive_divider_flag"
description: "Whether the interactive divider feature is enabled"
bug: "293654166"
+}
+
+flag {
+ namespace: "windowing_sdk"
+ name: "activity_window_info_flag"
+ description: "To dispatch ActivityWindowInfo through ClientTransaction"
+ bug: "287582673"
+ is_fixed_read_only: true
}
\ No newline at end of file
diff --git a/core/java/com/android/internal/foldables/FoldGracePeriodProvider.java b/core/java/com/android/internal/foldables/FoldGracePeriodProvider.java
new file mode 100644
index 0000000..53164f3
--- /dev/null
+++ b/core/java/com/android/internal/foldables/FoldGracePeriodProvider.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.foldables;
+
+import android.os.Build;
+import android.sysprop.FoldLockBehaviorProperties;
+import android.util.Slog;
+
+import com.android.internal.foldables.flags.Flags;
+
+import java.util.function.Supplier;
+
+/**
+ * Wrapper class to access {@link FoldLockBehaviorProperties}.
+ */
+public class FoldGracePeriodProvider {
+
+ private static final String TAG = "FoldGracePeriodProvider";
+ private final Supplier<Boolean> mFoldGracePeriodEnabled = Flags::foldGracePeriodEnabled;
+
+ /**
+ * Whether the fold grace period feature is enabled.
+ */
+ public boolean isEnabled() {
+ if ((Build.IS_ENG || Build.IS_USERDEBUG)
+ && FoldLockBehaviorProperties.fold_grace_period_enabled().orElse(false)) {
+ return true;
+ }
+ try {
+ return mFoldGracePeriodEnabled.get();
+ } catch (Throwable ex) {
+ Slog.i(TAG,
+ "Flags not ready yet. Return false for "
+ + Flags.FLAG_FOLD_GRACE_PERIOD_ENABLED,
+ ex);
+ return false;
+ }
+ }
+}
diff --git a/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig b/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig
index 44f436ea..d73e623 100644
--- a/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig
+++ b/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig
@@ -7,3 +7,11 @@
bug: "274447767"
is_fixed_read_only: true
}
+
+flag {
+ name: "fold_grace_period_enabled"
+ namespace: "display_manager"
+ description: "Feature flag for Folding Grace Period"
+ bug: "308417021"
+ is_fixed_read_only: true
+}
diff --git a/core/jni/android_os_HwBinder.cpp b/core/jni/android_os_HwBinder.cpp
index 781895e..477bd09 100644
--- a/core/jni/android_os_HwBinder.cpp
+++ b/core/jni/android_os_HwBinder.cpp
@@ -258,14 +258,59 @@
JHwBinder::SetNativeContext(env, thiz, context);
}
-static void JHwBinder_native_transact(
- JNIEnv * /* env */,
- jobject /* thiz */,
- jint /* code */,
- jobject /* requestObj */,
- jobject /* replyObj */,
- jint /* flags */) {
- CHECK(!"Should not be here");
+static void JHwBinder_native_transact(JNIEnv *env, jobject thiz, jint code, jobject requestObj,
+ jobject replyObj, jint flags) {
+ if (requestObj == NULL) {
+ jniThrowException(env, "java/lang/NullPointerException", NULL);
+ return;
+ }
+ sp<hardware::IBinder> binder = JHwBinder::GetNativeBinder(env, thiz);
+ sp<android::hidl::base::V1_0::IBase> base = new android::hidl::base::V1_0::BpHwBase(binder);
+ hidl_string desc;
+ auto ret = base->interfaceDescriptor(
+ [&desc](const hidl_string &descriptor) { desc = descriptor; });
+ ret.assertOk();
+ // Only the fake hwservicemanager is allowed to be used locally like this.
+ if (desc != "android.hidl.manager@1.2::IServiceManager" &&
+ desc != "android.hidl.manager@1.1::IServiceManager" &&
+ desc != "android.hidl.manager@1.0::IServiceManager") {
+ LOG(FATAL) << "Local binders are not supported!";
+ }
+ if (replyObj == nullptr) {
+ LOG(FATAL) << "Unexpected null replyObj. code: " << code;
+ return;
+ }
+ const hardware::Parcel *request = JHwParcel::GetNativeContext(env, requestObj)->getParcel();
+ sp<JHwParcel> replyContext = JHwParcel::GetNativeContext(env, replyObj);
+ hardware::Parcel *reply = replyContext->getParcel();
+
+ request->setDataPosition(0);
+
+ bool isOneway = (flags & IBinder::FLAG_ONEWAY) != 0;
+ if (!isOneway) {
+ replyContext->setTransactCallback([](auto &replyParcel) {});
+ }
+
+ env->CallVoidMethod(thiz, gFields.onTransactID, code, requestObj, replyObj, flags);
+
+ if (env->ExceptionCheck()) {
+ jthrowable excep = env->ExceptionOccurred();
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+
+ binder_report_exception(env, excep, "Uncaught error or exception in hwbinder!");
+
+ env->DeleteLocalRef(excep);
+ }
+
+ if (!isOneway) {
+ if (!replyContext->wasSent()) {
+ // The implementation never finished the transaction.
+ LOG(ERROR) << "The reply failed to send!";
+ }
+ }
+
+ reply->setDataPosition(0);
}
static void JHwBinder_native_registerService(
diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto
index 382a82c..2a0feee 100644
--- a/core/proto/android/server/windowmanagerservice.proto
+++ b/core/proto/android/server/windowmanagerservice.proto
@@ -404,7 +404,7 @@
optional WindowContainerProto window_container = 1;
optional int32 hash_code = 2;
repeated WindowStateProto windows = 3 [deprecated=true];
- optional bool waiting_to_show = 5;
+ optional bool waiting_to_show = 5 [deprecated=true];
optional bool paused = 6;
}
diff --git a/core/sysprop/FoldLockBehaviorProperties.sysprop b/core/sysprop/FoldLockBehaviorProperties.sysprop
index d337954..120e4bb 100644
--- a/core/sysprop/FoldLockBehaviorProperties.sysprop
+++ b/core/sysprop/FoldLockBehaviorProperties.sysprop
@@ -22,3 +22,11 @@
scope: Internal
access: Readonly
}
+
+prop {
+ api_name: "fold_grace_period_enabled"
+ type: Boolean
+ prop_name: "persist.fold_grace_period_enabled"
+ scope: Internal
+ access: Readonly
+}
diff --git a/data/etc/preinstalled-packages-platform.xml b/data/etc/preinstalled-packages-platform.xml
index 421bc25..bf60944 100644
--- a/data/etc/preinstalled-packages-platform.xml
+++ b/data/etc/preinstalled-packages-platform.xml
@@ -128,4 +128,9 @@
<install-in-user-type package="com.android.wallpaperbackup">
<install-in user-type="FULL" />
</install-in-user-type>
+
+ <!-- AvatarPicker (AvatarPicker app)-->
+ <install-in-user-type package="com.android.avatarpicker">
+ <install-in user-type="FULL" />
+ </install-in-user-type>
</config>
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 742d5a2..917a300 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -4453,6 +4453,12 @@
"group": "WM_DEBUG_BACK_PREVIEW",
"at": "com\/android\/server\/wm\/BackNavigationController.java"
},
+ "1946983717": {
+ "message": "Waiting for screen on due to %s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_STATES",
+ "at": "com\/android\/server\/wm\/TaskFragment.java"
+ },
"1947239194": {
"message": "Deferring rotation, still finishing previous rotation",
"level": "VERBOSE",
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index ca3d8d1..592f9a5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -19,6 +19,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION;
import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior;
@@ -356,6 +357,13 @@
wct.addTaskFragmentOperation(fragmentToken, operation);
}
+ void setTaskFragmentDimOnTask(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken, boolean dimOnTask) {
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_SET_DIM_ON_TASK).setDimOnTask(dimOnTask).build();
+ wct.addTaskFragmentOperation(fragmentToken, operation);
+ }
+
void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) {
mFragmentInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 543570c..6f356fa 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -20,6 +20,8 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.content.pm.PackageManager.MATCH_ALL;
+import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
+
import android.app.Activity;
import android.app.ActivityThread;
import android.app.WindowConfiguration;
@@ -56,6 +58,7 @@
import androidx.window.extensions.layout.WindowLayoutInfo;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
import java.util.ArrayList;
import java.util.List;
@@ -384,6 +387,13 @@
setCompanionTaskFragment(wct, primaryContainer.getTaskFragmentToken(),
secondaryContainer.getTaskFragmentToken(), splitRule, isStacked);
+ // Sets the dim area when the two TaskFragments are adjacent.
+ final boolean dimOnTask = !isStacked
+ && splitAttributes.getWindowAttributes().getDimArea() == DIM_AREA_ON_TASK
+ && Flags.fullscreenDimFlag();
+ setTaskFragmentDimOnTask(wct, primaryContainer.getTaskFragmentToken(), dimOnTask);
+ setTaskFragmentDimOnTask(wct, secondaryContainer.getTaskFragmentToken(), dimOnTask);
+
// Setting isolated navigation and clear non-sticky pinned container if needed.
final SplitPinRule splitPinRule =
splitRule instanceof SplitPinRule ? (SplitPinRule) splitRule : null;
@@ -578,6 +588,23 @@
bounds.isEmpty() ? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_MULTI_WINDOW);
}
+ @Override
+ void setTaskFragmentDimOnTask(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken, boolean dimOnTask) {
+ final TaskFragmentContainer container = mController.getContainer(fragmentToken);
+ if (container == null) {
+ throw new IllegalStateException("setTaskFragmentDimOnTask on TaskFragment that is"
+ + " not registered with controller.");
+ }
+
+ if (container.isLastDimOnTask() == dimOnTask) {
+ return;
+ }
+
+ container.setLastDimOnTask(dimOnTask);
+ super.setTaskFragmentDimOnTask(wct, fragmentToken, dimOnTask);
+ }
+
/**
* Expands the split container if the current split bounds are smaller than the Activity or
* Intent that is added to the container.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 810bded..b52971a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -172,6 +172,11 @@
private boolean mIsIsolatedNavigationEnabled;
/**
+ * Whether to apply dimming on the parent Task that was requested last.
+ */
+ private boolean mLastDimOnTask;
+
+ /**
* @see #TaskFragmentContainer(Activity, Intent, TaskContainer, SplitController,
* TaskFragmentContainer, String, Bundle)
*/
@@ -836,6 +841,16 @@
mIsIsolatedNavigationEnabled = isolatedNavigationEnabled;
}
+ /** Sets whether to apply dim on the parent Task. */
+ void setLastDimOnTask(boolean lastDimOnTask) {
+ mLastDimOnTask = lastDimOnTask;
+ }
+
+ /** Returns whether to apply dim on the parent Task. */
+ boolean isLastDimOnTask() {
+ return mLastDimOnTask;
+ }
+
/**
* Adds the pending appeared activity that has requested to be launched in this task fragment.
* @see android.app.ActivityClient#isRequestedToLaunchInTaskFragment
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index 6981d9d..941b4e1 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -235,6 +235,19 @@
}
@Test
+ public void testSetTaskFragmentDimOnTask() {
+ final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID);
+
+ mPresenter.setTaskFragmentDimOnTask(mTransaction, container.getTaskFragmentToken(), true);
+ verify(mTransaction).addTaskFragmentOperation(eq(container.getTaskFragmentToken()), any());
+
+ // No request to set the same adjacent TaskFragments.
+ clearInvocations(mTransaction);
+ mPresenter.setTaskFragmentDimOnTask(mTransaction, container.getTaskFragmentToken(), true);
+ verify(mTransaction, never()).addTaskFragmentOperation(any(), any());
+ }
+
+ @Test
public void testUpdateAnimationParams() {
final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 71bf487..0ef047f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -235,7 +235,8 @@
mainChoreographer,
taskOrganizer,
displayController,
- syncQueue);
+ syncQueue,
+ transitions);
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 48a0a46..3b0e7c1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -19,6 +19,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_PIP;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
@@ -54,6 +55,8 @@
@Nullable
private WindowContainerToken mPipTaskToken;
@Nullable
+ private IBinder mEnterTransition;
+ @Nullable
private IBinder mAutoEnterButtonNavTransition;
@Nullable
private IBinder mExitViaExpandTransition;
@@ -98,11 +101,8 @@
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
- if (isAutoEnterInButtonNavigation(request)) {
- mAutoEnterButtonNavTransition = transition;
- return getEnterPipTransaction(transition, request);
- } else if (isLegacyEnter(request)) {
- mLegacyEnterTransition = transition;
+ if (isAutoEnterInButtonNavigation(request) || isEnterPictureInPictureModeRequest(request)) {
+ mEnterTransition = transition;
return getEnterPipTransaction(transition, request);
}
return null;
@@ -111,12 +111,9 @@
@Override
public void augmentRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request,
@NonNull WindowContainerTransaction outWct) {
- if (isAutoEnterInButtonNavigation(request)) {
+ if (isAutoEnterInButtonNavigation(request) || isEnterPictureInPictureModeRequest(request)) {
outWct.merge(getEnterPipTransaction(transition, request), true /* transfer */);
- mAutoEnterButtonNavTransition = transition;
- } else if (isLegacyEnter(request)) {
- outWct.merge(getEnterPipTransaction(transition, request), true /* transfer */);
- mLegacyEnterTransition = transition;
+ mEnterTransition = transition;
}
}
@@ -162,7 +159,7 @@
&& pipTask.pictureInPictureParams.isAutoEnterEnabled();
}
- private boolean isLegacyEnter(@NonNull TransitionRequestInfo requestInfo) {
+ private boolean isEnterPictureInPictureModeRequest(@NonNull TransitionRequestInfo requestInfo) {
return requestInfo.getType() == TRANSIT_PIP;
}
@@ -172,13 +169,15 @@
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- if (transition == mAutoEnterButtonNavTransition) {
- mAutoEnterButtonNavTransition = null;
- return startAutoEnterButtonNavAnimation(info, startTransaction, finishTransaction,
- finishCallback);
- } else if (transition == mLegacyEnterTransition) {
- mLegacyEnterTransition = null;
- return startLegacyEnterAnimation(info, startTransaction, finishTransaction,
+ if (transition == mEnterTransition) {
+ mEnterTransition = null;
+ if (isLegacyEnter(info)) {
+ // If this is a legacy-enter-pip (auto-enter is off and PiP activity went to pause),
+ // then we should run an ALPHA type (cross-fade) animation.
+ return startAlphaTypeEnterAnimation(info, startTransaction, finishTransaction,
+ finishCallback);
+ }
+ return startBoundsTypeEnterAnimation(info, startTransaction, finishTransaction,
finishCallback);
} else if (transition == mExitViaExpandTransition) {
mExitViaExpandTransition = null;
@@ -187,7 +186,15 @@
return false;
}
- private boolean startAutoEnterButtonNavAnimation(@NonNull TransitionInfo info,
+ private boolean isLegacyEnter(@NonNull TransitionInfo info) {
+ TransitionInfo.Change pipChange = getPipChange(info);
+ // If the only change in the changes list is a TO_FRONT mode PiP task,
+ // then this is legacy-enter PiP.
+ return pipChange != null && pipChange.getMode() == TRANSIT_TO_FRONT
+ && info.getChanges().size() == 1;
+ }
+
+ private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
@@ -205,7 +212,7 @@
return true;
}
- private boolean startLegacyEnterAnimation(@NonNull TransitionInfo info,
+ private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index cf16920..cebc400 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -54,6 +54,7 @@
private final Choreographer mMainChoreographer;
private final DisplayController mDisplayController;
private final SyncTransactionQueue mSyncQueue;
+ private final Transitions mTransitions;
private TaskOperations mTaskOperations;
private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>();
@@ -64,13 +65,15 @@
Choreographer mainChoreographer,
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
- SyncTransactionQueue syncQueue) {
+ SyncTransactionQueue syncQueue,
+ Transitions transitions) {
mContext = context;
mMainHandler = mainHandler;
mMainChoreographer = mainChoreographer;
mTaskOrganizer = taskOrganizer;
mDisplayController = displayController;
mSyncQueue = syncQueue;
+ mTransitions = transitions;
if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
mTaskOperations = new TaskOperations(null, mContext, mSyncQueue);
}
@@ -133,7 +136,8 @@
if (decoration == null) {
createWindowDecoration(taskInfo, taskSurface, startT, finishT);
} else {
- decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */);
+ decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
+ false /* setTaskCropAndPosition */);
}
}
@@ -145,7 +149,8 @@
final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
if (decoration == null) return;
- decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */);
+ decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
+ false /* setTaskCropAndPosition */);
}
@Override
@@ -191,16 +196,17 @@
mSyncQueue);
mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
- final DragPositioningCallback dragPositioningCallback =
- new FluidResizeTaskPositioner(mTaskOrganizer, windowDecoration, mDisplayController,
- 0 /* disallowedAreaForEndBoundsHeight */);
+ final FluidResizeTaskPositioner taskPositioner =
+ new FluidResizeTaskPositioner(mTaskOrganizer, mTransitions, windowDecoration,
+ mDisplayController, 0 /* disallowedAreaForEndBoundsHeight */);
final CaptionTouchEventListener touchEventListener =
- new CaptionTouchEventListener(taskInfo, dragPositioningCallback);
+ new CaptionTouchEventListener(taskInfo, taskPositioner);
windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
- windowDecoration.setDragPositioningCallback(dragPositioningCallback);
+ windowDecoration.setDragPositioningCallback(taskPositioner);
windowDecoration.setDragDetector(touchEventListener.mDragDetector);
+ windowDecoration.setTaskDragResizer(taskPositioner);
windowDecoration.relayout(taskInfo, startT, finishT,
- false /* applyStartTransactionOnDraw */);
+ false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */);
setupCaptionColor(taskInfo, windowDecoration);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 6e7d11d..1debb02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -157,15 +157,21 @@
@Override
void relayout(RunningTaskInfo taskInfo) {
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ // The crop and position of the task should only be set when a task is fluid resizing. In
+ // all other cases, it is expected that the transition handler positions and crops the task
+ // in order to allow the handler time to animate before the task before the final
+ // position and crop are set.
+ final boolean shouldSetTaskPositionAndCrop = mTaskDragResizer.isResizingOrAnimating();
// Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is
// synced with the buffer transaction (that draws the View). Both will be shown on screen
// at the same, whereas applying them independently causes flickering. See b/270202228.
- relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */);
+ relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */,
+ shouldSetTaskPositionAndCrop);
}
void relayout(RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- boolean applyStartTransactionOnDraw) {
+ boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) {
final int shadowRadiusID = taskInfo.isFocused
? R.dimen.freeform_decor_shadow_focused_thickness
: R.dimen.freeform_decor_shadow_unfocused_thickness;
@@ -183,6 +189,7 @@
mRelayoutParams.mCaptionHeightId = getCaptionHeightId(taskInfo.getWindowingMode());
mRelayoutParams.mShadowRadiusId = shadowRadiusID;
mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
+ mRelayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition;
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index ab29df1..4fd3625 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -335,7 +335,8 @@
if (decoration == null) {
createWindowDecoration(taskInfo, taskSurface, startT, finishT);
} else {
- decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */);
+ decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
+ false /* shouldSetTaskPositionAndCrop */);
}
}
@@ -347,7 +348,8 @@
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
if (decoration == null) return;
- decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */);
+ decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
+ false /* shouldSetTaskPositionAndCrop */);
}
@Override
@@ -1010,8 +1012,23 @@
mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
windowDecoration.createResizeVeil();
- final DragPositioningCallback dragPositioningCallback = createDragPositioningCallback(
- windowDecoration);
+ final DragPositioningCallback dragPositioningCallback;
+ final int transitionAreaHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.desktop_mode_transition_area_height);
+ if (!DesktopModeStatus.isVeiledResizeEnabled()) {
+ dragPositioningCallback = new FluidResizeTaskPositioner(
+ mTaskOrganizer, mTransitions, windowDecoration, mDisplayController,
+ mDragStartListener, mTransactionFactory, transitionAreaHeight);
+ windowDecoration.setTaskDragResizer(
+ (FluidResizeTaskPositioner) dragPositioningCallback);
+ } else {
+ dragPositioningCallback = new VeiledResizeTaskPositioner(
+ mTaskOrganizer, windowDecoration, mDisplayController,
+ mDragStartListener, mTransitions, transitionAreaHeight);
+ windowDecoration.setTaskDragResizer(
+ (VeiledResizeTaskPositioner) dragPositioningCallback);
+ }
+
final DesktopModeTouchEventListener touchEventListener =
new DesktopModeTouchEventListener(taskInfo, dragPositioningCallback);
@@ -1021,23 +1038,9 @@
windowDecoration.setDragPositioningCallback(dragPositioningCallback);
windowDecoration.setDragDetector(touchEventListener.mDragDetector);
windowDecoration.relayout(taskInfo, startT, finishT,
- false /* applyStartTransactionOnDraw */);
+ false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */);
incrementEventReceiverTasks(taskInfo.displayId);
}
- private DragPositioningCallback createDragPositioningCallback(
- @NonNull DesktopModeWindowDecoration windowDecoration) {
- final int transitionAreaHeight = mContext.getResources().getDimensionPixelSize(
- R.dimen.desktop_mode_transition_area_height);
- if (!DesktopModeStatus.isVeiledResizeEnabled()) {
- return new FluidResizeTaskPositioner(mTaskOrganizer, windowDecoration,
- mDisplayController, mDragStartListener, mTransactionFactory,
- transitionAreaHeight);
- } else {
- return new VeiledResizeTaskPositioner(mTaskOrganizer, windowDecoration,
- mDisplayController, mDragStartListener, mTransitions,
- transitionAreaHeight);
- }
- }
private RunningTaskInfo getOtherSplitTask(int taskId) {
@SplitPosition int remainingTaskPosition = mSplitScreenController
@@ -1138,7 +1141,6 @@
}
}
}
-
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 2f51278..0c8e93b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -187,20 +187,28 @@
}
final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
+ // The crop and position of the task should only be set when a task is fluid resizing. In
+ // all other cases, it is expected that the transition handler positions and crops the task
+ // in order to allow the handler time to animate before the task before the final
+ // position and crop are set.
+ final boolean shouldSetTaskPositionAndCrop = !DesktopModeStatus.isVeiledResizeEnabled()
+ && mTaskDragResizer.isResizingOrAnimating();
// Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is
// synced with the buffer transaction (that draws the View). Both will be shown on screen
// at the same, whereas applying them independently causes flickering. See b/270202228.
- relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */);
+ relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */,
+ shouldSetTaskPositionAndCrop);
}
void relayout(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- boolean applyStartTransactionOnDraw) {
+ boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
if (isHandleMenuActive()) {
mHandleMenu.relayout(startT);
}
- updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw);
+ updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
+ shouldSetTaskPositionAndCrop);
final WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
@@ -302,7 +310,8 @@
RelayoutParams relayoutParams,
Context context,
ActivityManager.RunningTaskInfo taskInfo,
- boolean applyStartTransactionOnDraw) {
+ boolean applyStartTransactionOnDraw,
+ boolean shouldSetTaskPositionAndCrop) {
relayoutParams.reset();
relayoutParams.mRunningTaskInfo = taskInfo;
relayoutParams.mLayoutResId =
@@ -314,6 +323,7 @@
: R.dimen.freeform_decor_shadow_unfocused_thickness;
}
relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
+ relayoutParams.mSetTaskPositionAndCrop = shouldSetTaskPositionAndCrop;
// The configuration used to lay out the window decoration. The system context's config is
// used when the task density has been overridden to a custom density so that the resources
// and views of the decoration aren't affected and match the rest of the System UI, if not
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index 677c7f1..5afbd54 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -26,9 +26,7 @@
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.view.SurfaceControl;
-import android.window.WindowContainerTransaction;
-import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
/**
@@ -130,8 +128,7 @@
Rect taskBoundsAtDragStart, PointF repositionStartPoint, SurfaceControl.Transaction t,
float x, float y) {
updateTaskBounds(repositionTaskBounds, taskBoundsAtDragStart, repositionStartPoint, x, y);
- t.setPosition(decoration.mTaskSurface, repositionTaskBounds.left,
- repositionTaskBounds.top);
+ t.setPosition(decoration.mTaskSurface, repositionTaskBounds.left, repositionTaskBounds.top);
}
private static void updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart,
@@ -188,18 +185,6 @@
}
}
- /**
- * Apply a bounds change to a task.
- * @param windowDecoration decor of task we are changing bounds for
- * @param taskBounds new bounds of this task
- * @param taskOrganizer applies the provided WindowContainerTransaction
- */
- static void applyTaskBoundsChange(WindowContainerTransaction wct,
- WindowDecoration windowDecoration, Rect taskBounds, ShellTaskOrganizer taskOrganizer) {
- wct.setBounds(windowDecoration.mTaskInfo.token, taskBounds);
- taskOrganizer.applyTransaction(wct);
- }
-
private static float getMinWidth(DisplayController displayController,
WindowDecoration windowDecoration) {
return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinSize(displayController,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index 5d006fb..6bfc7cd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -16,23 +16,42 @@
package com.android.wm.shell.windowdecor;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+
import android.graphics.PointF;
import android.graphics.Rect;
+import android.os.IBinder;
import android.view.Surface;
import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.transition.Transitions;
import java.util.function.Supplier;
/**
* A task positioner that resizes/relocates task contents as it is dragged.
* Utilizes {@link DragPositioningCallbackUtility} to determine new task bounds.
+ *
+ * This positioner applies the final bounds after a resize or drag using a shell transition in order
+ * to utilize the startAnimation callback to set the final task position and crop. In most cases,
+ * the transition will be aborted since the final bounds are usually the same bounds set in the
+ * final {@link #onDragPositioningMove} call. In this case, the cropping and positioning would be
+ * set by {@link WindowDecoration#relayout} due to the final bounds change; however, it is important
+ * that we send the final shell transition since we still utilize the {@link #onTransitionConsumed}
+ * callback.
*/
-class FluidResizeTaskPositioner implements DragPositioningCallback {
+class FluidResizeTaskPositioner implements DragPositioningCallback,
+ TaskDragResizer, Transitions.TransitionHandler {
private final ShellTaskOrganizer mTaskOrganizer;
+ private final Transitions mTransitions;
private final WindowDecoration mWindowDecoration;
private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
private DisplayController mDisplayController;
@@ -45,21 +64,28 @@
// finalize the bounds there using WCT#setBounds
private final int mDisallowedAreaForEndBoundsHeight;
private boolean mHasDragResized;
+ private boolean mIsResizingOrAnimatingResize;
private int mCtrlType;
+ private IBinder mDragResizeEndTransition;
@Surface.Rotation private int mRotation;
- FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
- DisplayController displayController, int disallowedAreaForEndBoundsHeight) {
- this(taskOrganizer, windowDecoration, displayController, dragStartListener -> {},
- SurfaceControl.Transaction::new, disallowedAreaForEndBoundsHeight);
+ FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, Transitions transitions,
+ WindowDecoration windowDecoration, DisplayController displayController,
+ int disallowedAreaForEndBoundsHeight) {
+ this(taskOrganizer, transitions, windowDecoration, displayController,
+ dragStartListener -> {}, SurfaceControl.Transaction::new,
+ disallowedAreaForEndBoundsHeight);
}
- FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
+ FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
+ Transitions transitions,
+ WindowDecoration windowDecoration,
DisplayController displayController,
DragPositioningCallbackUtility.DragStartListener dragStartListener,
Supplier<SurfaceControl.Transaction> supplier,
int disallowedAreaForEndBoundsHeight) {
mTaskOrganizer = taskOrganizer;
+ mTransitions = transitions;
mWindowDecoration = windowDecoration;
mDisplayController = displayController;
mDragStartListener = dragStartListener;
@@ -103,9 +129,10 @@
// This is the first bounds change since drag resize operation started.
wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */);
}
- DragPositioningCallbackUtility.applyTaskBoundsChange(wct, mWindowDecoration,
- mRepositionTaskBounds, mTaskOrganizer);
+ wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
+ mTaskOrganizer.applyTransaction(wct);
mHasDragResized = true;
+ mIsResizingOrAnimatingResize = true;
} else if (mCtrlType == CTRL_TYPE_UNDEFINED) {
final SurfaceControl.Transaction t = mTransactionSupplier.get();
DragPositioningCallbackUtility.setPositionOnDrag(mWindowDecoration,
@@ -129,7 +156,7 @@
mWindowDecoration)) {
wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
}
- mTaskOrganizer.applyTransaction(wct);
+ mDragResizeEndTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
} else if (mCtrlType == CTRL_TYPE_UNDEFINED
&& DragPositioningCallbackUtility.isBelowDisallowedArea(
mDisallowedAreaForEndBoundsHeight, mTaskBoundsAtDragStart, mRepositionStartPoint,
@@ -139,7 +166,7 @@
mTaskBoundsAtDragStart, mRepositionStartPoint, x, y,
mWindowDecoration.calculateValidDragArea());
wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
- mTaskOrganizer.applyTransaction(wct);
+ mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
}
mTaskBoundsAtDragStart.setEmpty();
@@ -154,4 +181,51 @@
|| (mCtrlType & CTRL_TYPE_LEFT) != 0 || (mCtrlType & CTRL_TYPE_RIGHT) != 0;
}
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ for (TransitionInfo.Change change: info.getChanges()) {
+ final SurfaceControl sc = change.getLeash();
+ final Rect endBounds = change.getEndAbsBounds();
+ startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
+ .setPosition(sc, endBounds.left, endBounds.top);
+ finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
+ .setPosition(sc, endBounds.left, endBounds.top);
+ }
+
+ startTransaction.apply();
+ if (transition.equals(mDragResizeEndTransition)) {
+ mIsResizingOrAnimatingResize = false;
+ mDragResizeEndTransition = null;
+ }
+ finishCallback.onTransitionFinished(null);
+ return true;
+ }
+
+ /**
+ * We should never reach this as this handler's transitions are only started from shell
+ * explicitly.
+ */
+ @Nullable
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ return null;
+ }
+
+ @Override
+ public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @Nullable SurfaceControl.Transaction finishTransaction) {
+ if (transition.equals(mDragResizeEndTransition)) {
+ mIsResizingOrAnimatingResize = false;
+ mDragResizeEndTransition = null;
+ }
+ }
+
+ @Override
+ public boolean isResizingOrAnimating() {
+ return mIsResizingOrAnimatingResize;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java
new file mode 100644
index 0000000..40421b5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor;
+
+/**
+ * Holds the state of a drag resize.
+ */
+interface TaskDragResizer {
+
+ /**
+ * Returns true if task is currently being resized or animating the final transition after
+ * a resize is complete.
+ */
+ boolean isResizingOrAnimating();
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index 4363558..c1b18f9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -43,7 +43,7 @@
* If the drag is repositioning, we update in the typical manner.
*/
public class VeiledResizeTaskPositioner implements DragPositioningCallback,
- Transitions.TransitionHandler {
+ TaskDragResizer, Transitions.TransitionHandler {
private DesktopModeWindowDecoration mDesktopWindowDecoration;
private ShellTaskOrganizer mTaskOrganizer;
@@ -59,10 +59,12 @@
private final int mDisallowedAreaForEndBoundsHeight;
private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
private int mCtrlType;
+ private boolean mIsResizingOrAnimatingResize;
@Surface.Rotation private int mRotation;
public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
- DesktopModeWindowDecoration windowDecoration, DisplayController displayController,
+ DesktopModeWindowDecoration windowDecoration,
+ DisplayController displayController,
DragPositioningCallbackUtility.DragStartListener dragStartListener,
Transitions transitions,
int disallowedAreaForEndBoundsHeight) {
@@ -71,12 +73,13 @@
}
public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
- DesktopModeWindowDecoration windowDecoration, DisplayController displayController,
+ DesktopModeWindowDecoration windowDecoration,
+ DisplayController displayController,
DragPositioningCallbackUtility.DragStartListener dragStartListener,
Supplier<SurfaceControl.Transaction> supplier, Transitions transitions,
int disallowedAreaForEndBoundsHeight) {
- mTaskOrganizer = taskOrganizer;
mDesktopWindowDecoration = windowDecoration;
+ mTaskOrganizer = taskOrganizer;
mDisplayController = displayController;
mDragStartListener = dragStartListener;
mTransactionSupplier = supplier;
@@ -117,6 +120,7 @@
mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta,
mDisplayController, mDesktopWindowDecoration)) {
mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds);
+ mIsResizingOrAnimatingResize = true;
} else if (mCtrlType == CTRL_TYPE_UNDEFINED) {
final SurfaceControl.Transaction t = mTransactionSupplier.get();
DragPositioningCallbackUtility.setPositionOnDrag(mDesktopWindowDecoration,
@@ -138,24 +142,22 @@
mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds);
final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
- } else {
- mTaskOrganizer.applyTransaction(wct);
- }
+ mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
} else {
// If bounds haven't changed, perform necessary veil reset here as startAnimation
// won't be called.
mDesktopWindowDecoration.hideResizeVeil();
+ mIsResizingOrAnimatingResize = false;
}
} else if (DragPositioningCallbackUtility.isBelowDisallowedArea(
mDisallowedAreaForEndBoundsHeight, mTaskBoundsAtDragStart, mRepositionStartPoint,
y)) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
DragPositioningCallbackUtility.onDragEnd(mRepositionTaskBounds,
mTaskBoundsAtDragStart, mRepositionStartPoint, x, y,
mDesktopWindowDecoration.calculateValidDragArea());
- DragPositioningCallbackUtility.applyTaskBoundsChange(new WindowContainerTransaction(),
- mDesktopWindowDecoration, mRepositionTaskBounds, mTaskOrganizer);
+ wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
+ mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
}
mCtrlType = CTRL_TYPE_UNDEFINED;
@@ -174,10 +176,20 @@
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
+ for (TransitionInfo.Change change: info.getChanges()) {
+ final SurfaceControl sc = change.getLeash();
+ final Rect endBounds = change.getEndAbsBounds();
+ startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
+ .setPosition(sc, endBounds.left, endBounds.top);
+ finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
+ .setPosition(sc, endBounds.left, endBounds.top);
+ }
+
startTransaction.apply();
mDesktopWindowDecoration.hideResizeVeil();
mCtrlType = CTRL_TYPE_UNDEFINED;
finishCallback.onTransitionFinished(null);
+ mIsResizingOrAnimatingResize = false;
return true;
}
@@ -191,4 +203,9 @@
@NonNull TransitionRequestInfo request) {
return null;
}
+
+ @Override
+ public boolean isResizingOrAnimating() {
+ return mIsResizingOrAnimatingResize;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index ee0e31e..b5373c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -124,6 +124,7 @@
private WindowlessWindowManager mCaptionWindowManager;
private SurfaceControlViewHost mViewHost;
private Configuration mWindowDecorConfig;
+ TaskDragResizer mTaskDragResizer;
private boolean mIsCaptionVisible;
private final Binder mOwner = new Binder();
@@ -311,25 +312,21 @@
float shadowRadius;
final Point taskPosition = mTaskInfo.positionInParent;
if (isFullscreen) {
- // Setting the task crop to the width/height stops input events from being sent to
- // some regions of the app window. See b/300324920
- // TODO(b/296921174): investigate whether crop/position needs to be set by window
- // decorations at all when transition handlers are already taking ownership of the task
- // surface placement/crop, especially when in fullscreen where tasks cannot be
- // drag-resized by the window decoration.
- startT.setWindowCrop(mTaskSurface, null);
- finishT.setWindowCrop(mTaskSurface, null);
// Shadow is not needed for fullscreen tasks
shadowRadius = 0;
} else {
- startT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight);
- finishT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight);
shadowRadius = loadDimension(resources, params.mShadowRadiusId);
}
+
+ if (params.mSetTaskPositionAndCrop) {
+ startT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight);
+ finishT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight)
+ .setPosition(mTaskSurface, taskPosition.x, taskPosition.y);
+ }
+
startT.setShadowRadius(mTaskSurface, shadowRadius)
.show(mTaskSurface);
- finishT.setPosition(mTaskSurface, taskPosition.x, taskPosition.y)
- .setShadowRadius(mTaskSurface, shadowRadius);
+ finishT.setShadowRadius(mTaskSurface, shadowRadius);
if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
if (!DesktopModeStatus.isVeiledResizeEnabled()) {
// When fluid resize is enabled, add a background to freeform tasks
@@ -394,6 +391,10 @@
}
}
+ void setTaskDragResizer(TaskDragResizer taskDragResizer) {
+ mTaskDragResizer = taskDragResizer;
+ }
+
private void setCaptionVisibility(View rootView, boolean visible) {
if (rootView == null) {
return;
@@ -559,6 +560,7 @@
Configuration mWindowDecorConfig;
boolean mApplyStartTransactionOnDraw;
+ boolean mSetTaskPositionAndCrop;
void reset() {
mLayoutResId = Resources.ID_NULL;
@@ -572,6 +574,7 @@
mCaptionY = 0;
mApplyStartTransactionOnDraw = false;
+ mSetTaskPositionAndCrop = false;
mWindowDecorConfig = null;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 77667ca..193f16d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -144,7 +144,8 @@
RelayoutParams relayoutParams = new RelayoutParams();
DesktopModeWindowDecoration.updateRelayoutParams(
- relayoutParams, mContext, taskInfo, /* applyStartTransactionOnDraw= */ true);
+ relayoutParams, mContext, taskInfo, /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false);
assertThat(relayoutParams.mShadowRadiusId).isNotEqualTo(Resources.ID_NULL);
}
@@ -159,7 +160,8 @@
relayoutParams,
mTestableContext,
taskInfo,
- /* applyStartTransactionOnDraw= */ true);
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false);
assertThat(relayoutParams.mCornerRadius).isGreaterThan(0);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index 2ce49cf..de6903d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -10,6 +10,7 @@
import android.view.Surface.ROTATION_270
import android.view.Surface.ROTATION_90
import android.view.SurfaceControl
+import android.view.WindowManager
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING
@@ -18,13 +19,17 @@
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito
@@ -34,6 +39,7 @@
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doReturn
import java.util.function.Supplier
import org.mockito.Mockito.`when` as whenever
@@ -50,6 +56,8 @@
@Mock
private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer
@Mock
+ private lateinit var mockTransitions: Transitions
+ @Mock
private lateinit var mockWindowDecoration: WindowDecoration<*>
@Mock
private lateinit var mockDragStartListener: DragPositioningCallbackUtility.DragStartListener
@@ -69,6 +77,8 @@
private lateinit var mockTransactionFactory: Supplier<SurfaceControl.Transaction>
@Mock
private lateinit var mockTransaction: SurfaceControl.Transaction
+ @Mock
+ private lateinit var mockTransitionBinder: IBinder
private lateinit var taskPositioner: FluidResizeTaskPositioner
@@ -106,9 +116,12 @@
`when`(mockWindowDecoration.calculateValidDragArea()).thenReturn(VALID_DRAG_AREA)
mockWindowDecoration.mDisplay = mockDisplay
whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
+ whenever(mockTransitions.startTransition(anyInt(), any(), any()))
+ .doReturn(mockTransitionBinder)
taskPositioner = FluidResizeTaskPositioner(
mockShellTaskOrganizer,
+ mockTransitions,
mockWindowDecoration,
mockDisplayController,
mockDragStartListener,
@@ -118,7 +131,7 @@
}
@Test
- fun testDragResize_notMove_skipsTransactionOnEnd() {
+ fun testDragResize_notMove_skipsTransitionOnEnd() {
taskPositioner.onDragPositioningStart(
CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
STARTING_BOUNDS.left.toFloat(),
@@ -130,16 +143,16 @@
STARTING_BOUNDS.top.toFloat() + 10
)
- verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+ verify(mockTransitions, never()).startTransition(
+ eq(WindowManager.TRANSIT_CHANGE), argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
token == taskBinder &&
((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
- }
- })
+ }}, eq(taskPositioner))
}
@Test
- fun testDragResize_noEffectiveMove_skipsTransactionOnMoveAndEnd() {
+ fun testDragResize_noEffectiveMove_skipsTransitionOnMoveAndEnd() {
taskPositioner.onDragPositioningStart(
CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
STARTING_BOUNDS.left.toFloat(),
@@ -151,21 +164,28 @@
STARTING_BOUNDS.top.toFloat()
)
- taskPositioner.onDragPositioningEnd(
- STARTING_BOUNDS.left.toFloat() + 10,
- STARTING_BOUNDS.top.toFloat() + 10
- )
-
verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
token == taskBinder &&
((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
}
})
+
+ taskPositioner.onDragPositioningEnd(
+ STARTING_BOUNDS.left.toFloat() + 10,
+ STARTING_BOUNDS.top.toFloat() + 10
+ )
+
+ verify(mockTransitions, never()).startTransition(
+ eq(WindowManager.TRANSIT_CHANGE), argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
+ }}, eq(taskPositioner))
}
@Test
- fun testDragResize_hasEffectiveMove_issuesTransactionOnMoveAndEnd() {
+ fun testDragResize_hasEffectiveMove_issuesTransitionOnMoveAndEnd() {
taskPositioner.onDragPositioningStart(
CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
STARTING_BOUNDS.left.toFloat(),
@@ -192,13 +212,13 @@
)
val rectAfterEnd = Rect(rectAfterMove)
rectAfterEnd.top += 10
- verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
- return@argThat wct.changes.any { (token, change) ->
- token == taskBinder &&
- (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
- change.configuration.windowConfiguration.bounds == rectAfterEnd
- }
- })
+ verify(mockTransitions).startTransition(
+ eq(WindowManager.TRANSIT_CHANGE), argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+ change.configuration.windowConfiguration.bounds == rectAfterEnd
+ }}, eq(taskPositioner))
}
@Test
@@ -226,6 +246,13 @@
change.dragResizing
}
})
+ verify(mockTransitions, never()).startTransition(
+ eq(WindowManager.TRANSIT_CHANGE), argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) &&
+ change.dragResizing
+ }}, eq(taskPositioner))
}
@Test
@@ -253,13 +280,13 @@
change.dragResizing
}
})
- verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ verify(mockTransitions).startTransition(
+ eq(WindowManager.TRANSIT_CHANGE), argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
token == taskBinder &&
((change.changeMask and CHANGE_DRAG_RESIZING) != 0) &&
!change.dragResizing
- }
- })
+ }}, eq(taskPositioner))
}
@Test
@@ -270,7 +297,7 @@
STARTING_BOUNDS.top.toFloat()
)
- // Resize to width of 95px and height of 5px with min width of 10px
+ // Resize to width of 95px and height of 5px with min height of 10px
val newX = STARTING_BOUNDS.right.toFloat() - 5
val newY = STARTING_BOUNDS.top.toFloat() + 95
taskPositioner.onDragPositioningMove(
@@ -566,12 +593,12 @@
taskPositioner.onDragPositioningEnd(newX, newY)
- verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+ verify(mockTransitions, never()).startTransition(
+ eq(WindowManager.TRANSIT_CHANGE), argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
token == taskBinder &&
((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
- }
- })
+ }}, eq(taskPositioner))
}
private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean {
@@ -650,14 +677,14 @@
)
// Verify task's top bound is set to stable bounds top since dragged outside stable bounds
// but not in disallowed end bounds area.
- verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ verify(mockTransitions).startTransition(
+ eq(WindowManager.TRANSIT_CHANGE), argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
token == taskBinder &&
(change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
change.configuration.windowConfiguration.bounds.top ==
STABLE_BOUNDS_LANDSCAPE.top
- }
- })
+ }}, eq(taskPositioner))
}
@Test
@@ -680,7 +707,8 @@
newX,
newY
)
- verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ verify(mockTransitions).startTransition(
+ eq(WindowManager.TRANSIT_CHANGE), argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
token == taskBinder &&
(change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
@@ -688,8 +716,7 @@
VALID_DRAG_AREA.bottom &&
change.configuration.windowConfiguration.bounds.left ==
VALID_DRAG_AREA.left
- }
- })
+ }}, eq(taskPositioner))
}
@Test
@@ -741,6 +768,59 @@
verify(mockDisplayLayout, Mockito.times(2)).getStableBounds(any())
}
+ @Test
+ fun testIsResizingOrAnimatingResizeSet() {
+ assertFalse(taskPositioner.isResizingOrAnimating)
+
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ taskPositioner.onDragPositioningMove(
+ STARTING_BOUNDS.left.toFloat() - 20,
+ STARTING_BOUNDS.top.toFloat() - 20
+ )
+
+ // isResizingOrAnimating should be set to true after move during a resize
+ assertTrue(taskPositioner.isResizingOrAnimating)
+
+ taskPositioner.onDragPositioningEnd(
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ // isResizingOrAnimating should be not be set till false until after transition animation
+ assertTrue(taskPositioner.isResizingOrAnimating)
+ }
+
+ @Test
+ fun testIsResizingOrAnimatingResizeResetAfterAbortedTransition() {
+ performDrag(STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat(), STARTING_BOUNDS.left.toFloat() - 20,
+ STARTING_BOUNDS.top.toFloat() - 20, CTRL_TYPE_TOP or CTRL_TYPE_RIGHT)
+
+ taskPositioner.onTransitionConsumed(mockTransitionBinder, true /* aborted */,
+ mockTransaction)
+
+ // isResizingOrAnimating should be set to false until after transition successfully consumed
+ assertFalse(taskPositioner.isResizingOrAnimating)
+ }
+
+ @Test
+ fun testIsResizingOrAnimatingResizeResetAfterNonAbortedTransition() {
+ performDrag(STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat(), STARTING_BOUNDS.left.toFloat() - 20,
+ STARTING_BOUNDS.top.toFloat() - 20, CTRL_TYPE_TOP or CTRL_TYPE_RIGHT)
+
+ taskPositioner.onTransitionConsumed(mockTransitionBinder, false /* aborted */,
+ mockTransaction)
+
+ // isResizingOrAnimating should be set to false until after transition successfully consumed
+ assertFalse(taskPositioner.isResizingOrAnimating)
+ }
+
private fun performDrag(
startX: Float,
startY: Float,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index a759b53..0841210 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -26,6 +26,7 @@
import android.view.Surface.ROTATION_90
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.TransitionInfo
import android.window.WindowContainerToken
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTaskOrganizer
@@ -33,10 +34,12 @@
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED
+import junit.framework.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -85,6 +88,12 @@
@Mock
private lateinit var mockTransaction: SurfaceControl.Transaction
@Mock
+ private lateinit var mockTransitionBinder: IBinder
+ @Mock
+ private lateinit var mockTransitionInfo: TransitionInfo
+ @Mock
+ private lateinit var mockFinishCallback: TransitionFinishCallback
+ @Mock
private lateinit var mockTransitions: Transitions
private lateinit var taskPositioner: VeiledResizeTaskPositioner
@@ -188,13 +197,12 @@
verify(mockDesktopWindowDecoration, never()).createResizeVeil()
verify(mockDesktopWindowDecoration, never()).hideResizeVeil()
- verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
token == taskBinder &&
(change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
- change.configuration.windowConfiguration.bounds == rectAfterEnd
- }
- })
+ change.configuration.windowConfiguration.bounds == rectAfterEnd }},
+ eq(taskPositioner))
}
@Test
@@ -369,14 +377,13 @@
)
// Verify task's top bound is set to stable bounds top since dragged outside stable bounds
// but not in disallowed end bounds area.
- verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
token == taskBinder &&
(change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
change.configuration.windowConfiguration.bounds.top ==
- STABLE_BOUNDS_LANDSCAPE.top
- }
- })
+ STABLE_BOUNDS_LANDSCAPE.top }},
+ eq(taskPositioner))
}
@Test
@@ -399,16 +406,15 @@
newX,
newY
)
- verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
token == taskBinder &&
(change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
change.configuration.windowConfiguration.bounds.top ==
VALID_DRAG_AREA.bottom &&
change.configuration.windowConfiguration.bounds.left ==
- VALID_DRAG_AREA.left
- }
- })
+ VALID_DRAG_AREA.left }},
+ eq(taskPositioner))
}
@Test
@@ -456,6 +462,47 @@
verify(mockDisplayLayout, times(2)).getStableBounds(any())
}
+ @Test
+ fun testIsResizingOrAnimatingResizeSet() {
+ Assert.assertFalse(taskPositioner.isResizingOrAnimating)
+
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ taskPositioner.onDragPositioningMove(
+ STARTING_BOUNDS.left.toFloat() - 20,
+ STARTING_BOUNDS.top.toFloat() - 20
+ )
+
+ // isResizingOrAnimating should be set to true after move during a resize
+ Assert.assertTrue(taskPositioner.isResizingOrAnimating)
+
+ taskPositioner.onDragPositioningEnd(
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ // isResizingOrAnimating should be not be set till false until after transition animation
+ Assert.assertTrue(taskPositioner.isResizingOrAnimating)
+ }
+
+ @Test
+ fun testIsResizingOrAnimatingResizeResetAfterStartAnimation() {
+ performDrag(
+ STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat(),
+ STARTING_BOUNDS.left.toFloat() - 20, STARTING_BOUNDS.top.toFloat() - 20,
+ CTRL_TYPE_TOP or CTRL_TYPE_RIGHT)
+
+ taskPositioner.startAnimation(mockTransitionBinder, mockTransitionInfo, mockTransaction,
+ mockTransaction, mockFinishCallback)
+
+ // isResizingOrAnimating should be set to false until after transition successfully consumed
+ Assert.assertFalse(taskPositioner.isResizingOrAnimating)
+ }
+
private fun performDrag(
startX: Float,
startY: Float,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index fe508e2..32a91461 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -32,6 +32,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.argThat;
@@ -261,11 +262,6 @@
eq(new Rect(100, 300, 400, 364)));
}
- verify(mMockSurfaceControlFinishT)
- .setPosition(mMockTaskSurface, TASK_POSITION_IN_PARENT.x,
- TASK_POSITION_IN_PARENT.y);
- verify(mMockSurfaceControlFinishT)
- .setWindowCrop(mMockTaskSurface, 300, 100);
verify(mMockSurfaceControlStartT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS);
verify(mMockSurfaceControlFinishT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS);
verify(mMockSurfaceControlStartT)
@@ -642,6 +638,66 @@
eq(0) /* index */, eq(mandatorySystemGestures()));
}
+ @Test
+ public void testTaskPositionAndCropNotSetWhenFalse() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setBounds(TASK_BOUNDS)
+ .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
+ .setVisible(true)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM)
+ .build();
+ taskInfo.isFocused = true;
+ // Density is 2. Shadow radius is 10px. Caption height is 64px.
+ taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+
+
+ mRelayoutParams.mSetTaskPositionAndCrop = false;
+ windowDecor.relayout(taskInfo);
+
+ verify(mMockSurfaceControlStartT, never()).setWindowCrop(
+ eq(mMockTaskSurface), anyInt(), anyInt());
+ verify(mMockSurfaceControlFinishT, never()).setPosition(
+ eq(mMockTaskSurface), anyFloat(), anyFloat());
+ verify(mMockSurfaceControlFinishT, never()).setWindowCrop(
+ eq(mMockTaskSurface), anyInt(), anyInt());
+ }
+
+ @Test
+ public void testTaskPositionAndCropSetWhenSetTrue() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setBounds(TASK_BOUNDS)
+ .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
+ .setVisible(true)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM)
+ .build();
+ taskInfo.isFocused = true;
+ // Density is 2. Shadow radius is 10px. Caption height is 64px.
+ taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+
+ mRelayoutParams.mSetTaskPositionAndCrop = true;
+ windowDecor.relayout(taskInfo);
+
+ verify(mMockSurfaceControlStartT).setWindowCrop(
+ eq(mMockTaskSurface), anyInt(), anyInt());
+ verify(mMockSurfaceControlFinishT).setPosition(
+ eq(mMockTaskSurface), anyFloat(), anyFloat());
+ verify(mMockSurfaceControlFinishT).setWindowCrop(
+ eq(mMockTaskSurface), anyInt(), anyInt());
+ }
+
+
private TestWindowDecoration createWindowDecoration(ActivityManager.RunningTaskInfo taskInfo) {
return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer,
taskInfo, mMockTaskSurface, mWindowConfiguration,
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 4741170..eebf8aa 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -38,6 +38,7 @@
cc_aconfig_library {
name: "hwui_flags_cc_lib",
+ host_supported: true,
aconfig_declarations: "hwui_flags",
}
@@ -109,12 +110,15 @@
"libbase",
"libharfbuzz_ng",
"libminikin",
+ "server_configurable_flags",
],
static_libs: [
"libui-types",
],
+ whole_static_libs: ["hwui_flags_cc_lib"],
+
target: {
android: {
shared_libs: [
@@ -146,7 +150,6 @@
"libstatspull_lazy",
"libstatssocket_lazy",
"libtonemap",
- "hwui_flags_cc_lib",
],
},
host: {
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index ca11975..c156c46 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -15,6 +15,13 @@
}
flag {
+ name: "high_contrast_text_luminance"
+ namespace: "accessibility"
+ description: "Use luminance to determine how to make text more high contrast, instead of RGB heuristic"
+ bug: "186567103"
+}
+
+flag {
name: "hdr_10bit_plus"
namespace: "core_graphics"
description: "Use 10101010 and FP16 formats for HDR-UI when available"
diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h
index 2e6e976..8f99990 100644
--- a/libs/hwui/hwui/DrawTextFunctor.h
+++ b/libs/hwui/hwui/DrawTextFunctor.h
@@ -16,7 +16,9 @@
#include <SkFontMetrics.h>
#include <SkRRect.h>
+#include <com_android_graphics_hwui_flags.h>
+#include "../utils/Color.h"
#include "Canvas.h"
#include "FeatureFlags.h"
#include "MinikinUtils.h"
@@ -27,6 +29,8 @@
#include "hwui/PaintFilter.h"
#include "pipeline/skia/SkiaRecordingCanvas.h"
+namespace flags = com::android::graphics::hwui::flags;
+
namespace android {
static inline void drawStroke(SkScalar left, SkScalar right, SkScalar top, SkScalar thickness,
@@ -73,8 +77,14 @@
if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) {
// high contrast draw path
int color = paint.getColor();
- int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color);
- bool darken = channelSum < (128 * 3);
+ bool darken;
+ if (flags::high_contrast_text_luminance()) {
+ uirenderer::Lab lab = uirenderer::sRGBToLab(color);
+ darken = lab.L <= 50;
+ } else {
+ int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color);
+ darken = channelSum < (128 * 3);
+ }
// outline
gDrawTextBlobMode = DrawTextBlobMode::HctOutline;
diff --git a/libs/hwui/pipeline/skia/DumpOpsCanvas.h b/libs/hwui/pipeline/skia/DumpOpsCanvas.h
index 6a052db..260547c 100644
--- a/libs/hwui/pipeline/skia/DumpOpsCanvas.h
+++ b/libs/hwui/pipeline/skia/DumpOpsCanvas.h
@@ -90,11 +90,6 @@
mOutput << mIdent << "drawTextBlob" << std::endl;
}
- void onDrawImage2(const SkImage*, SkScalar dx, SkScalar dy, const SkSamplingOptions&,
- const SkPaint*) override {
- mOutput << mIdent << "drawImage" << std::endl;
- }
-
void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&, const SkSamplingOptions&,
const SkPaint*, SrcRectConstraint) override {
mOutput << mIdent << "drawImageRect" << std::endl;
diff --git a/libs/hwui/tests/common/CallCountingCanvas.h b/libs/hwui/tests/common/CallCountingCanvas.h
index dc36a2e..df5f04f99 100644
--- a/libs/hwui/tests/common/CallCountingCanvas.h
+++ b/libs/hwui/tests/common/CallCountingCanvas.h
@@ -109,12 +109,6 @@
drawPoints++;
}
- int drawImageCount = 0;
- void onDrawImage2(const SkImage* image, SkScalar dx, SkScalar dy, const SkSamplingOptions&,
- const SkPaint* paint) override {
- drawImageCount++;
- }
-
int drawImageRectCount = 0;
void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&, const SkSamplingOptions&,
const SkPaint*, SkCanvas::SrcRectConstraint) override {
diff --git a/libs/hwui/tests/unit/CanvasOpTests.cpp b/libs/hwui/tests/unit/CanvasOpTests.cpp
index 18c5047..4ae76e2 100644
--- a/libs/hwui/tests/unit/CanvasOpTests.cpp
+++ b/libs/hwui/tests/unit/CanvasOpTests.cpp
@@ -492,7 +492,7 @@
CallCountingCanvas canvas;
EXPECT_EQ(0, canvas.sumTotalDrawCalls());
rasterizeCanvasBuffer(buffer, &canvas);
- EXPECT_EQ(1, canvas.drawImageCount);
+ EXPECT_EQ(1, canvas.drawImageRectCount);
EXPECT_EQ(1, canvas.sumTotalDrawCalls());
}
diff --git a/libs/hwui/tests/unit/FatalTestCanvas.h b/libs/hwui/tests/unit/FatalTestCanvas.h
index 96a0c61..8b95e0c 100644
--- a/libs/hwui/tests/unit/FatalTestCanvas.h
+++ b/libs/hwui/tests/unit/FatalTestCanvas.h
@@ -69,10 +69,6 @@
void onDrawPath(const SkPath&, const SkPaint&) {
ADD_FAILURE() << "onDrawPath not expected in this test";
}
- void onDrawImage2(const SkImage*, SkScalar dx, SkScalar dy, const SkSamplingOptions&,
- const SkPaint*) {
- ADD_FAILURE() << "onDrawImage not expected in this test";
- }
void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&, const SkSamplingOptions&,
const SkPaint*, SrcRectConstraint) {
ADD_FAILURE() << "onDrawImageRect not expected in this test";
diff --git a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
index 073a835..ca54087 100644
--- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
+++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
@@ -941,8 +941,9 @@
void onDrawRect(const SkRect& rect, const SkPaint& paint) override {
EXPECT_EQ(0, mDrawCounter++);
}
- void onDrawImage2(const SkImage*, SkScalar dx, SkScalar dy, const SkSamplingOptions&,
- const SkPaint*) override {
+ void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&,
+ const SkSamplingOptions&, const SkPaint*,
+ SrcRectConstraint) override {
EXPECT_EQ(1, mDrawCounter++);
}
};
diff --git a/libs/hwui/tests/unit/SkiaPipelineTests.cpp b/libs/hwui/tests/unit/SkiaPipelineTests.cpp
index 3ded540..785e286 100644
--- a/libs/hwui/tests/unit/SkiaPipelineTests.cpp
+++ b/libs/hwui/tests/unit/SkiaPipelineTests.cpp
@@ -303,8 +303,9 @@
class ClippedTestCanvas : public SkCanvas {
public:
ClippedTestCanvas() : SkCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) {}
- void onDrawImage2(const SkImage*, SkScalar dx, SkScalar dy, const SkSamplingOptions&,
- const SkPaint*) override {
+ void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&,
+ const SkSamplingOptions&, const SkPaint*,
+ SrcRectConstraint) override {
EXPECT_EQ(0, mDrawCounter++);
EXPECT_EQ(SkRect::MakeLTRB(10, 20, 30, 40), TestUtils::getClipBounds(this));
EXPECT_TRUE(getTotalMatrix().isIdentity());
@@ -338,8 +339,9 @@
class ClippedTestCanvas : public SkCanvas {
public:
ClippedTestCanvas() : SkCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) {}
- void onDrawImage2(const SkImage*, SkScalar dx, SkScalar dy, const SkSamplingOptions&,
- const SkPaint*) override {
+ void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&,
+ const SkSamplingOptions&, const SkPaint*,
+ SrcRectConstraint) override {
EXPECT_EQ(0, mDrawCounter++);
// Expect clip to be rotated.
EXPECT_EQ(SkRect::MakeLTRB(CANVAS_HEIGHT - dirty.fTop - dirty.height(), dirty.fLeft,
diff --git a/libs/hwui/utils/ForceDark.h b/libs/hwui/utils/ForceDark.h
index 28538c4b..ecfe41f 100644
--- a/libs/hwui/utils/ForceDark.h
+++ b/libs/hwui/utils/ForceDark.h
@@ -17,6 +17,8 @@
#ifndef FORCEDARKUTILS_H
#define FORCEDARKUTILS_H
+#include <stdint.h>
+
namespace android {
namespace uirenderer {
@@ -26,9 +28,9 @@
* This should stay in sync with the java @IntDef in
* frameworks/base/graphics/java/android/graphics/ForceDarkType.java
*/
-enum class ForceDarkType : __uint8_t { NONE = 0, FORCE_DARK = 1, FORCE_INVERT_COLOR_DARK = 2 };
+enum class ForceDarkType : uint8_t { NONE = 0, FORCE_DARK = 1, FORCE_INVERT_COLOR_DARK = 2 };
} /* namespace uirenderer */
} /* namespace android */
-#endif // FORCEDARKUTILS_H
\ No newline at end of file
+#endif // FORCEDARKUTILS_H
diff --git a/location/java/android/location/flags/gnss.aconfig b/location/java/android/location/flags/gnss.aconfig
index a8464d3..794a555 100644
--- a/location/java/android/location/flags/gnss.aconfig
+++ b/location/java/android/location/flags/gnss.aconfig
@@ -34,3 +34,10 @@
description: "Flag for location validation"
bug: "314328533"
}
+
+flag {
+ name: "gnss_configuration_from_resource"
+ namespace: "location"
+ description: "Flag for GNSS configuration from resource"
+ bug: "317734846"
+}
diff --git a/native/graphics/jni/libjnigraphics.map.txt b/native/graphics/jni/libjnigraphics.map.txt
index e0df794..193728a 100644
--- a/native/graphics/jni/libjnigraphics.map.txt
+++ b/native/graphics/jni/libjnigraphics.map.txt
@@ -18,21 +18,21 @@
AImageDecoder_getRepeatCount; # introduced=31
AImageDecoder_advanceFrame; # introduced=31
AImageDecoder_rewind; # introduced=31
- AImageDecoder_getFrameInfo; # introduced = 31
- AImageDecoder_setInternallyHandleDisposePrevious; # introduced = 31
+ AImageDecoder_getFrameInfo; # introduced=31
+ AImageDecoder_setInternallyHandleDisposePrevious; # introduced=31
AImageDecoderHeaderInfo_getWidth; # introduced=30
AImageDecoderHeaderInfo_getHeight; # introduced=30
AImageDecoderHeaderInfo_getMimeType; # introduced=30
AImageDecoderHeaderInfo_getAlphaFlags; # introduced=30
AImageDecoderHeaderInfo_getAndroidBitmapFormat; # introduced=30
AImageDecoderHeaderInfo_getDataSpace; # introduced=30
- AImageDecoderFrameInfo_create; # introduced = 31
- AImageDecoderFrameInfo_delete; # introduced = 31
- AImageDecoderFrameInfo_getDuration; # introduced = 31
- AImageDecoderFrameInfo_getFrameRect; # introduced = 31
- AImageDecoderFrameInfo_hasAlphaWithinBounds; # introduced = 31
- AImageDecoderFrameInfo_getDisposeOp; # introduced = 31
- AImageDecoderFrameInfo_getBlendOp; # introduced = 31
+ AImageDecoderFrameInfo_create; # introduced=31
+ AImageDecoderFrameInfo_delete; # introduced=31
+ AImageDecoderFrameInfo_getDuration; # introduced=31
+ AImageDecoderFrameInfo_getFrameRect; # introduced=31
+ AImageDecoderFrameInfo_hasAlphaWithinBounds; # introduced=31
+ AImageDecoderFrameInfo_getDisposeOp; # introduced=31
+ AImageDecoderFrameInfo_getBlendOp; # introduced=31
AndroidBitmap_getInfo;
AndroidBitmap_getDataSpace;
AndroidBitmap_lockPixels;
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java
index 679f696..b29cb2a 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java
@@ -34,7 +34,10 @@
public class AnonymousSourceFragment extends DialogFragment {
public static String TAG = AnonymousSourceFragment.class.getSimpleName();
+ @NonNull
private InstallActionListener mInstallActionListener;
+ @NonNull
+ private AlertDialog mDialog;
@Override
public void onAttach(@NonNull Context context) {
@@ -45,7 +48,7 @@
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
- return new AlertDialog.Builder(getActivity())
+ mDialog = new AlertDialog.Builder(requireContext())
.setMessage(R.string.anonymous_source_warning)
.setPositiveButton(R.string.anonymous_source_continue,
((dialog, which) -> mInstallActionListener.onPositiveResponse(
@@ -53,6 +56,7 @@
.setNegativeButton(R.string.cancel,
((dialog, which) -> mInstallActionListener.onNegativeResponse(
InstallStage.STAGE_USER_ACTION_REQUIRED))).create();
+ return mDialog;
}
@Override
@@ -60,4 +64,24 @@
super.onCancel(dialog);
mInstallActionListener.onNegativeResponse(InstallStage.STAGE_USER_ACTION_REQUIRED);
}
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ // This prevents tapjacking since an overlay activity started in front of Pia will
+ // cause Pia to be paused.
+ mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
+ }
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java
index 49901de..2314d6b 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java
@@ -35,8 +35,12 @@
public class ExternalSourcesBlockedFragment extends DialogFragment {
private final String TAG = ExternalSourcesBlockedFragment.class.getSimpleName();
+ @NonNull
private final InstallUserActionRequired mDialogData;
+ @NonNull
private InstallActionListener mInstallActionListener;
+ @NonNull
+ private AlertDialog mDialog;
public ExternalSourcesBlockedFragment(InstallUserActionRequired dialogData) {
mDialogData = dialogData;
@@ -51,7 +55,7 @@
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
- return new AlertDialog.Builder(requireContext())
+ mDialog = new AlertDialog.Builder(requireContext())
.setTitle(mDialogData.getAppLabel())
.setIcon(mDialogData.getAppIcon())
.setMessage(R.string.untrusted_external_source_warning)
@@ -62,6 +66,7 @@
(dialog, which) -> mInstallActionListener.onNegativeResponse(
mDialogData.getStageCode()))
.create();
+ return mDialog;
}
@Override
@@ -69,4 +74,24 @@
super.onCancel(dialog);
mInstallActionListener.onNegativeResponse(mDialogData.getStageCode());
}
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ // This prevents tapjacking since an overlay activity started in front of Pia will
+ // cause Pia to be paused.
+ mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
+ }
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java
index 25363d0..5ca02ea 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java
@@ -42,6 +42,8 @@
private final InstallUserActionRequired mDialogData;
@NonNull
private InstallActionListener mInstallActionListener;
+ @NonNull
+ private AlertDialog mDialog;
public InstallConfirmationFragment(@NonNull InstallUserActionRequired dialogData) {
mDialogData = dialogData;
@@ -58,7 +60,7 @@
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null);
- AlertDialog dialog = new AlertDialog.Builder(requireContext())
+ mDialog = new AlertDialog.Builder(requireContext())
.setIcon(mDialogData.getAppIcon())
.setTitle(mDialogData.getAppLabel())
.setView(dialogView)
@@ -84,7 +86,7 @@
}
viewToEnable.setVisibility(View.VISIBLE);
- return dialog;
+ return mDialog;
}
@Override
@@ -92,4 +94,24 @@
super.onCancel(dialog);
mInstallActionListener.onNegativeResponse(mDialogData.getStageCode());
}
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ // This prevents tapjacking since an overlay activity started in front of Pia will
+ // cause Pia to be paused.
+ mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
+ }
}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 14bcac2..aa0903c 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -145,6 +145,16 @@
}
flag {
+ name: "enable_background_keyguard_ondrawn_callback"
+ namespace: "systemui"
+ description: "Calls the onDrawn keyguard in the background, without being blocked by main"
+ "thread work. This results in the screen to turn on earlier when the main thread is stuck. "
+ "Note that, even after this callback is called, we're waiting for all windows to finish "
+ " drawing."
+ bug: "295873557"
+}
+
+flag {
name: "qs_new_pipeline"
namespace: "systemui"
description: "Use the new pipeline for Quick Settings. Should have no behavior changes."
@@ -303,3 +313,10 @@
description: "Displays the auto on toggle in the bluetooth QS tile dialog"
bug: "316985153"
}
+
+flag {
+ name: "smartspace_relocate_to_bottom"
+ namespace: "systemui"
+ description: "Relocate Smartspace to bottom of the Lock Screen"
+ bug: "316212788"
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 17c4e02..5a4e0a9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -16,6 +16,7 @@
package com.android.systemui.communal.ui.compose
+import android.appwidget.AppWidgetHostView
import android.os.Bundle
import android.util.SizeF
import android.widget.FrameLayout
@@ -376,7 +377,7 @@
AndroidView(
modifier = modifier,
factory = { context ->
- FrameLayout(context).apply { addView(model.remoteViews.apply(context, this)) }
+ AppWidgetHostView(context).apply { updateAppWidget(model.remoteViews) }
},
// For reusing composition in lazy lists.
onReset = {},
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 67a6820..ff53ff2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -37,9 +37,6 @@
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
-/** Set this to `true` to use the LockscreenContent replacement of KeyguardRootView. */
-private val UseLockscreenContent = false
-
/** The lock screen scene shows when the device is locked. */
@SysUISingleton
class LockscreenScene
@@ -48,7 +45,6 @@
@Application private val applicationScope: CoroutineScope,
private val viewModel: LockscreenSceneViewModel,
private val lockscreenContent: Lazy<LockscreenContent>,
- private val viewBasedLockscreenContent: Lazy<ViewBasedLockscreenContent>,
) : ComposableScene {
override val key = SceneKey.Lockscreen
@@ -73,7 +69,6 @@
) {
LockscreenScene(
lockscreenContent = lockscreenContent,
- viewBasedLockscreenContent = viewBasedLockscreenContent,
modifier = modifier,
)
}
@@ -93,22 +88,13 @@
}
@Composable
-private fun SceneScope.LockscreenScene(
+private fun LockscreenScene(
lockscreenContent: Lazy<LockscreenContent>,
- viewBasedLockscreenContent: Lazy<ViewBasedLockscreenContent>,
modifier: Modifier = Modifier,
) {
- if (UseLockscreenContent) {
- lockscreenContent
- .get()
- .Content(
- modifier = modifier.fillMaxSize(),
- )
- } else {
- with(viewBasedLockscreenContent.get()) {
- Content(
- modifier = modifier.fillMaxSize(),
- )
- }
- }
+ lockscreenContent
+ .get()
+ .Content(
+ modifier = modifier.fillMaxSize(),
+ )
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
index 9abb50c..3677cab 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
@@ -20,6 +20,7 @@
import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule
import com.android.systemui.keyguard.ui.composable.blueprint.ShortcutsBesideUdfpsBlueprintModule
import com.android.systemui.keyguard.ui.composable.blueprint.SplitShadeBlueprintModule
+import com.android.systemui.keyguard.ui.composable.section.OptionalSectionModule
import dagger.Module
@Module(
@@ -27,6 +28,7 @@
[
CommunalBlueprintModule::class,
DefaultBlueprintModule::class,
+ OptionalSectionModule::class,
ShortcutsBesideUdfpsBlueprintModule::class,
SplitShadeBlueprintModule::class,
],
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt
deleted file mode 100644
index 976161b..0000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.composable
-
-import android.graphics.Rect
-import android.view.View
-import android.view.ViewGroup
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.toComposeRect
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.onPlaced
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.core.view.isVisible
-import com.android.compose.animation.scene.SceneScope
-import com.android.systemui.keyguard.qualifiers.KeyguardRootView
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
-import com.android.systemui.notifications.ui.composable.NotificationStack
-import com.android.systemui.res.R
-import javax.inject.Inject
-
-/**
- * Renders the content of the lockscreen.
- *
- * This is different from [LockscreenContent] (which is pure compose) and uses a view-based
- * implementation of the lockscreen scene content that relies on [KeyguardRootView].
- *
- * TODO(b/316211368): remove this once [LockscreenContent] is feature complete.
- */
-class ViewBasedLockscreenContent
-@Inject
-constructor(
- private val viewModel: LockscreenSceneViewModel,
- @KeyguardRootView private val viewProvider: () -> @JvmSuppressWildcards View,
-) {
- @Composable
- fun SceneScope.Content(
- modifier: Modifier = Modifier,
- ) {
- fun findSettingsMenu(): View {
- return viewProvider().requireViewById(R.id.keyguard_settings_button)
- }
-
- LockscreenLongPress(
- viewModel = viewModel.longPress,
- modifier = modifier,
- ) { onSettingsMenuPlaced ->
- AndroidView(
- factory = { _ ->
- val keyguardRootView = viewProvider()
- // Remove the KeyguardRootView from any parent it might already have in legacy
- // code just in case (a view can't have two parents).
- (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView)
- keyguardRootView
- },
- modifier = Modifier.fillMaxSize(),
- )
-
- val notificationStackPosition by
- viewModel.keyguardRoot.notificationBounds.collectAsState()
-
- Layout(
- modifier =
- Modifier.fillMaxSize().onPlaced {
- val settingsMenuView = findSettingsMenu()
- onSettingsMenuPlaced(
- if (settingsMenuView.isVisible) {
- val bounds = Rect()
- settingsMenuView.getHitRect(bounds)
- bounds.toComposeRect()
- } else {
- null
- }
- )
- },
- content = {
- NotificationStack(
- viewModel = viewModel.notifications,
- isScrimVisible = false,
- )
- }
- ) { measurables, constraints ->
- check(measurables.size == 1)
- val height = notificationStackPosition.height.toInt()
- val childConstraints = constraints.copy(minHeight = height, maxHeight = height)
- val placeable = measurables[0].measure(childConstraints)
- layout(constraints.maxWidth, constraints.maxHeight) {
- val start = (constraints.maxWidth - placeable.measuredWidth) / 2
- placeable.placeRelative(x = start, y = notificationStackPosition.top.toInt())
- }
- }
- }
- }
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
new file mode 100644
index 0000000..c418490
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.blueprint
+
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.displayCutout
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.union
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalDensity
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.plugins.clocks.ClockController
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+/** Produces a [BurnInState] that can be used to query the `LockscreenBurnInViewModel` flows. */
+@Composable
+fun rememberBurnIn(
+ clockInteractor: KeyguardClockInteractor,
+): BurnInState {
+ val clock by clockInteractor.currentClock.collectAsState()
+
+ val (smartspaceTop, onSmartspaceTopChanged) = remember { mutableStateOf<Float?>(null) }
+ val (smallClockTop, onSmallClockTopChanged) = remember { mutableStateOf<Float?>(null) }
+
+ val topmostTop =
+ when {
+ smartspaceTop != null && smallClockTop != null -> min(smartspaceTop, smallClockTop)
+ smartspaceTop != null -> smartspaceTop
+ smallClockTop != null -> smallClockTop
+ else -> 0f
+ }.roundToInt()
+
+ val params = rememberBurnInParameters(clock, topmostTop)
+
+ return remember(params, onSmartspaceTopChanged, onSmallClockTopChanged) {
+ BurnInState(
+ parameters = params,
+ onSmartspaceTopChanged = onSmartspaceTopChanged,
+ onSmallClockTopChanged = onSmallClockTopChanged,
+ )
+ }
+}
+
+@Composable
+private fun rememberBurnInParameters(
+ clock: ClockController?,
+ topmostTop: Int,
+): BurnInParameters {
+ val density = LocalDensity.current
+ val topInset = WindowInsets.systemBars.union(WindowInsets.displayCutout).getTop(density)
+
+ return remember(clock, topInset, topmostTop) {
+ BurnInParameters(
+ clockControllerProvider = { clock },
+ topInset = topInset,
+ statusViewTop = topmostTop,
+ )
+ }
+}
+
+data class BurnInState(
+ /** Parameters for use with the `LockscreenBurnInViewModel. */
+ val parameters: BurnInParameters,
+ /**
+ * Callback to invoke when the top coordinate of the smartspace element is updated, pass `null`
+ * when the element is not shown.
+ */
+ val onSmartspaceTopChanged: (Float?) -> Unit,
+ /**
+ * Callback to invoke when the top coordinate of the small clock element is updated, pass `null`
+ * when the element is not shown.
+ */
+ val onSmallClockTopChanged: (Float?) -> Unit,
+)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index d9d98cb..84d4246 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -24,6 +24,7 @@
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.IntRect
import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
@@ -37,6 +38,7 @@
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
+import java.util.Optional
import javax.inject.Inject
/**
@@ -52,9 +54,10 @@
private val smartSpaceSection: SmartSpaceSection,
private val notificationSection: NotificationSection,
private val lockSection: LockSection,
- private val ambientIndicationSection: AmbientIndicationSection,
+ private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>,
private val bottomAreaSection: BottomAreaSection,
private val settingsMenuSection: SettingsMenuSection,
+ private val clockInteractor: KeyguardClockInteractor,
) : LockscreenSceneBlueprint {
override val id: String = "default"
@@ -62,6 +65,7 @@
@Composable
override fun SceneScope.Content(modifier: Modifier) {
val isUdfpsVisible = viewModel.isUdfpsVisible
+ val burnIn = rememberBurnIn(clockInteractor)
LockscreenLongPress(
viewModel = viewModel.longPress,
@@ -74,14 +78,25 @@
modifier = Modifier.fillMaxWidth(),
) {
with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
- with(clockSection) { SmallClock(modifier = Modifier.fillMaxWidth()) }
- with(smartSpaceSection) { SmartSpace(modifier = Modifier.fillMaxWidth()) }
+ with(clockSection) {
+ SmallClock(
+ onTopChanged = burnIn.onSmallClockTopChanged,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ with(smartSpaceSection) {
+ SmartSpace(
+ burnInParams = burnIn.parameters,
+ onTopChanged = burnIn.onSmartspaceTopChanged,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
with(notificationSection) {
Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
}
- if (!isUdfpsVisible) {
- with(ambientIndicationSection) {
+ if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
AmbientIndication(modifier = Modifier.fillMaxWidth())
}
}
@@ -91,8 +106,8 @@
// Aligned to bottom and constrained to below the lock icon.
Column(modifier = Modifier.fillMaxWidth()) {
- if (isUdfpsVisible) {
- with(ambientIndicationSection) {
+ if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
AmbientIndication(modifier = Modifier.fillMaxWidth())
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
index 4704f5c..4148462 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -24,6 +24,7 @@
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.IntRect
import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
@@ -37,6 +38,7 @@
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
+import java.util.Optional
import javax.inject.Inject
/**
@@ -52,9 +54,10 @@
private val smartSpaceSection: SmartSpaceSection,
private val notificationSection: NotificationSection,
private val lockSection: LockSection,
- private val ambientIndicationSection: AmbientIndicationSection,
+ private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>,
private val bottomAreaSection: BottomAreaSection,
private val settingsMenuSection: SettingsMenuSection,
+ private val clockInteractor: KeyguardClockInteractor,
) : LockscreenSceneBlueprint {
override val id: String = "shortcuts-besides-udfps"
@@ -62,6 +65,7 @@
@Composable
override fun SceneScope.Content(modifier: Modifier) {
val isUdfpsVisible = viewModel.isUdfpsVisible
+ val burnIn = rememberBurnIn(clockInteractor)
LockscreenLongPress(
viewModel = viewModel.longPress,
@@ -74,14 +78,25 @@
modifier = Modifier.fillMaxWidth(),
) {
with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
- with(clockSection) { SmallClock(modifier = Modifier.fillMaxWidth()) }
- with(smartSpaceSection) { SmartSpace(modifier = Modifier.fillMaxWidth()) }
+ with(clockSection) {
+ SmallClock(
+ onTopChanged = burnIn.onSmallClockTopChanged,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ with(smartSpaceSection) {
+ SmartSpace(
+ burnInParams = burnIn.parameters,
+ onTopChanged = burnIn.onSmartspaceTopChanged,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
with(notificationSection) {
Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
}
- if (!isUdfpsVisible) {
- with(ambientIndicationSection) {
+ if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
AmbientIndication(modifier = Modifier.fillMaxWidth())
}
}
@@ -97,8 +112,8 @@
// Aligned to bottom and constrained to below the lock icon.
Column(modifier = Modifier.fillMaxWidth()) {
- if (isUdfpsVisible) {
- with(ambientIndicationSection) {
+ if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
AmbientIndication(modifier = Modifier.fillMaxWidth())
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt
new file mode 100644
index 0000000..f9dd04b
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.modifier
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onPlaced
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.BurnInScaleViewModel
+
+/**
+ * Modifies the composable to account for anti-burn in translation, alpha, and scaling.
+ *
+ * Please override [isClock] as `true` if the composable is an element that's part of a clock.
+ */
+@Composable
+fun Modifier.burnInAware(
+ viewModel: AodBurnInViewModel,
+ params: BurnInParameters,
+ isClock: Boolean = false,
+): Modifier {
+ val translationX by viewModel.translationX(params).collectAsState(initial = 0f)
+ val translationY by viewModel.translationY(params).collectAsState(initial = 0f)
+ val alpha by viewModel.alpha.collectAsState(initial = 1f)
+ val scaleViewModel by viewModel.scale(params).collectAsState(initial = BurnInScaleViewModel())
+
+ return this.graphicsLayer {
+ val scale =
+ when {
+ scaleViewModel.scaleClockOnly && isClock -> scaleViewModel.scale
+ !scaleViewModel.scaleClockOnly -> scaleViewModel.scale
+ else -> 1f
+ }
+
+ this.translationX = translationX
+ this.translationY = translationY
+ this.alpha = alpha
+ this.scaleX = scale
+ this.scaleY = scale
+ }
+}
+
+/** Reports the "top" coordinate of the modified composable to the given [consumer]. */
+@Composable
+fun Modifier.onTopPlacementChanged(
+ consumer: (Float) -> Unit,
+): Modifier {
+ return onPlaced { coordinates -> consumer(coordinates.boundsInWindow().top) }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt
index 0e7ac5e..af9a195 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt
@@ -16,36 +16,11 @@
package com.android.systemui.keyguard.ui.composable.section
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
-import javax.inject.Inject
-class AmbientIndicationSection @Inject constructor() {
- @Composable
- fun SceneScope.AmbientIndication(modifier: Modifier = Modifier) {
- MovableElement(
- key = AmbientIndicationElementKey,
- modifier = modifier,
- ) {
- Box(
- modifier = Modifier.fillMaxWidth().background(Color.Green),
- ) {
- Text(
- text = "TODO(b/316211368): Ambient indication",
- color = Color.White,
- modifier = Modifier.align(Alignment.Center),
- )
- }
- }
- }
+/** Defines interface for classes that can render the ambient indication area. */
+interface AmbientIndicationSection {
+ @Composable fun SceneScope.AmbientIndication(modifier: Modifier)
}
-
-private val AmbientIndicationElementKey = ElementKey("AmbientIndication")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
index db20f65..8bd0d45 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
@@ -35,10 +35,10 @@
import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
+import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.KeyguardIndicationController
@@ -55,7 +55,7 @@
private val vibratorHelper: VibratorHelper,
private val indicationController: KeyguardIndicationController,
private val indicationAreaViewModel: KeyguardIndicationAreaViewModel,
- private val keyguardRootViewModel: KeyguardRootViewModel,
+ private val alphaViewModel: AodAlphaViewModel,
) {
/**
* Renders a single lockscreen shortcut.
@@ -74,20 +74,22 @@
key = if (isStart) StartButtonElementKey else EndButtonElementKey,
modifier = modifier,
) {
- Shortcut(
- viewId = if (isStart) R.id.start_button else R.id.end_button,
- viewModel = if (isStart) viewModel.startButton else viewModel.endButton,
- transitionAlpha = viewModel.transitionAlpha,
- falsingManager = falsingManager,
- vibratorHelper = vibratorHelper,
- indicationController = indicationController,
- modifier =
- if (applyPadding) {
- Modifier.shortcutPadding()
- } else {
- Modifier
- }
- )
+ content {
+ Shortcut(
+ viewId = if (isStart) R.id.start_button else R.id.end_button,
+ viewModel = if (isStart) viewModel.startButton else viewModel.endButton,
+ transitionAlpha = viewModel.transitionAlpha,
+ falsingManager = falsingManager,
+ vibratorHelper = vibratorHelper,
+ indicationController = indicationController,
+ modifier =
+ if (applyPadding) {
+ Modifier.shortcutPadding()
+ } else {
+ Modifier
+ }
+ )
+ }
}
}
@@ -99,11 +101,13 @@
key = IndicationAreaElementKey,
modifier = modifier.shortcutPadding(),
) {
- IndicationArea(
- indicationAreaViewModel = indicationAreaViewModel,
- keyguardRootViewModel = keyguardRootViewModel,
- indicationController = indicationController,
- )
+ content {
+ IndicationArea(
+ indicationAreaViewModel = indicationAreaViewModel,
+ alphaViewModel = alphaViewModel,
+ indicationController = indicationController,
+ )
+ }
}
}
@@ -179,7 +183,7 @@
@Composable
private fun IndicationArea(
indicationAreaViewModel: KeyguardIndicationAreaViewModel,
- keyguardRootViewModel: KeyguardRootViewModel,
+ alphaViewModel: AodAlphaViewModel,
indicationController: KeyguardIndicationController,
modifier: Modifier = Modifier,
) {
@@ -192,7 +196,7 @@
KeyguardIndicationAreaBinder.bind(
view = view,
viewModel = indicationAreaViewModel,
- keyguardRootViewModel = keyguardRootViewModel,
+ aodAlphaViewModel = alphaViewModel,
indicationController = indicationController,
)
)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
index eaf8063..f021bb6 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
@@ -16,41 +16,73 @@
package com.android.systemui.keyguard.ui.composable.section
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material3.Text
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
+import com.android.keyguard.KeyguardClockSwitch
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.res.R
import javax.inject.Inject
class ClockSection
@Inject
constructor(
private val viewModel: KeyguardClockViewModel,
+ private val clockInteractor: KeyguardClockInteractor,
) {
+
@Composable
- fun SceneScope.SmallClock(modifier: Modifier = Modifier) {
- if (viewModel.useLargeClock) {
+ fun SceneScope.SmallClock(
+ onTopChanged: (top: Float?) -> Unit,
+ modifier: Modifier = Modifier,
+ ) {
+ val clockSize by viewModel.clockSize.collectAsState()
+ val currentClock by viewModel.currentClock.collectAsState()
+ viewModel.clock = currentClock
+
+ if (clockSize != KeyguardClockSwitch.SMALL) {
+ onTopChanged(null)
return
}
+ if (currentClock?.smallClock?.view == null) {
+ return
+ }
+
+ val view = LocalView.current
+
+ DisposableEffect(view) {
+ clockInteractor.clockEventController.registerListeners(view)
+
+ onDispose { clockInteractor.clockEventController.unregisterListeners() }
+ }
+
MovableElement(
key = ClockElementKey,
modifier = modifier,
) {
- Box(
- modifier = Modifier.fillMaxWidth().background(Color.Magenta),
- ) {
- Text(
- text = "TODO(b/316211368): Small clock",
- color = Color.White,
- modifier = Modifier.align(Alignment.Center),
+ content {
+ AndroidView(
+ factory = { checkNotNull(currentClock).smallClock.view },
+ modifier =
+ Modifier.padding(
+ horizontal =
+ dimensionResource(R.dimen.keyguard_affordance_horizontal_offset)
+ )
+ .padding(top = { viewModel.getSmallClockTopMargin(view.context) })
+ .onTopPlacementChanged(onTopChanged),
)
}
}
@@ -58,21 +90,36 @@
@Composable
fun SceneScope.LargeClock(modifier: Modifier = Modifier) {
- if (!viewModel.useLargeClock) {
+ val clockSize by viewModel.clockSize.collectAsState()
+ val currentClock by viewModel.currentClock.collectAsState()
+ viewModel.clock = currentClock
+
+ if (clockSize != KeyguardClockSwitch.LARGE) {
return
}
+ if (currentClock?.largeClock?.view == null) {
+ return
+ }
+
+ val view = LocalView.current
+
+ DisposableEffect(view) {
+ clockInteractor.clockEventController.registerListeners(view)
+
+ onDispose { clockInteractor.clockEventController.unregisterListeners() }
+ }
+
MovableElement(
key = ClockElementKey,
modifier = modifier,
) {
- Box(
- modifier = Modifier.fillMaxWidth().background(Color.Blue),
- ) {
- Text(
- text = "TODO(b/316211368): Large clock",
- color = Color.White,
- modifier = Modifier.align(Alignment.Center),
+ content {
+ AndroidView(
+ factory = { checkNotNull(currentClock).largeClock.view },
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(top = { viewModel.getLargeClockTopMargin(view.context) })
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index c547e2b..900616f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -22,7 +22,6 @@
import com.android.systemui.notifications.ui.composable.NotificationStack
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
class NotificationSection
@Inject
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/OptionalSectionModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/OptionalSectionModule.kt
new file mode 100644
index 0000000..5b7a8e6
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/OptionalSectionModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.section
+
+import dagger.BindsOptionalOf
+import dagger.Module
+
+/**
+ * Dagger module for providing placeholders for optional lockscreen scene sections that don't exist
+ * in AOSP but may be provided by OEMs.
+ */
+@Module
+interface OptionalSectionModule {
+ @BindsOptionalOf fun ambientIndicationSection(): AmbientIndicationSection
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt
index 3c49cbc..9b71844 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt
@@ -36,6 +36,10 @@
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
+import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.res.R
import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
@@ -47,11 +51,16 @@
private val lockscreenSmartspaceController: LockscreenSmartspaceController,
private val keyguardUnlockAnimationController: KeyguardUnlockAnimationController,
private val keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel,
+ private val aodBurnInViewModel: AodBurnInViewModel,
) {
@Composable
- fun SceneScope.SmartSpace(modifier: Modifier = Modifier) {
+ fun SceneScope.SmartSpace(
+ burnInParams: BurnInParameters,
+ onTopChanged: (top: Float?) -> Unit,
+ modifier: Modifier = Modifier,
+ ) {
Column(
- modifier = modifier.element(SmartSpaceElementKey),
+ modifier = modifier.element(SmartSpaceElementKey).onTopPlacementChanged(onTopChanged),
) {
if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) {
return
@@ -71,9 +80,21 @@
start = paddingBelowClockStart,
),
) {
- Date()
+ Date(
+ modifier =
+ Modifier.burnInAware(
+ viewModel = aodBurnInViewModel,
+ params = burnInParams,
+ ),
+ )
Spacer(modifier = Modifier.width(4.dp))
- Weather()
+ Weather(
+ modifier =
+ Modifier.burnInAware(
+ viewModel = aodBurnInViewModel,
+ params = burnInParams,
+ ),
+ )
}
}
@@ -84,6 +105,10 @@
start = paddingBelowClockStart,
end = paddingBelowClockEnd,
)
+ .burnInAware(
+ viewModel = aodBurnInViewModel,
+ params = burnInParams,
+ ),
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt
index 6811eb4..ddc12ff 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt
@@ -21,9 +21,11 @@
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
@@ -51,37 +53,43 @@
key = StatusBarElementKey,
modifier = modifier,
) {
- AndroidView(
- factory = {
- notificationPanelView.get().findViewById<View>(R.id.keyguard_header)?.let {
- (it.parent as ViewGroup).removeView(it)
- }
-
- val provider =
- object : ShadeViewStateProvider {
- override val lockscreenShadeDragProgress: Float = 0f
- override val panelViewExpandedHeight: Float = 0f
- override fun shouldHeadsUpBeVisible(): Boolean {
- return false
- }
+ content {
+ AndroidView(
+ factory = {
+ notificationPanelView.get().findViewById<View>(R.id.keyguard_header)?.let {
+ (it.parent as ViewGroup).removeView(it)
}
- @SuppressLint("InflateParams")
- val view =
- LayoutInflater.from(context)
- .inflate(
- R.layout.keyguard_status_bar,
- null,
- false,
- ) as KeyguardStatusBarView
- componentFactory.build(view, provider).keyguardStatusBarViewController.init()
- view
- },
- modifier =
- Modifier.fillMaxWidth().height {
- Utils.getStatusBarHeaderHeightKeyguard(context)
+ val provider =
+ object : ShadeViewStateProvider {
+ override val lockscreenShadeDragProgress: Float = 0f
+ override val panelViewExpandedHeight: Float = 0f
+
+ override fun shouldHeadsUpBeVisible(): Boolean {
+ return false
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ val view =
+ LayoutInflater.from(context)
+ .inflate(
+ R.layout.keyguard_status_bar,
+ null,
+ false,
+ ) as KeyguardStatusBarView
+ componentFactory
+ .build(view, provider)
+ .keyguardStatusBarViewController
+ .init()
+ view
},
- )
+ modifier =
+ Modifier.fillMaxWidth().padding(horizontal = 16.dp).height {
+ Utils.getStatusBarHeaderHeightKeyguard(context)
+ },
+ )
+ }
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 12f1b30..0eec024 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -44,7 +44,7 @@
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.ValueKey
-import com.android.compose.animation.scene.animateSharedFloatAsState
+import com.android.compose.animation.scene.animateElementFloatAsState
import com.android.systemui.notifications.ui.composable.Notifications.Form
import com.android.systemui.notifications.ui.composable.Notifications.SharedValues.SharedExpansionValue
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
@@ -157,10 +157,10 @@
modifier: Modifier = Modifier,
) {
val elementKey = Notifications.Elements.NotificationPlaceholder
- Box(
+ Element(
+ elementKey,
modifier =
modifier
- .element(elementKey)
.debugBackground(viewModel)
.onSizeChanged { size: IntSize ->
debugLog(viewModel) { "STACK onSizeChanged: size=$size" }
@@ -182,19 +182,23 @@
}
) {
val animatedExpansion by
- animateSharedFloatAsState(
+ animateElementFloatAsState(
value = if (form == Form.HunFromTop) 0f else 1f,
- key = SharedExpansionValue,
- element = elementKey
+ key = SharedExpansionValue
)
debugLog(viewModel) { "STACK composed: expansion=$animatedExpansion" }
- if (viewModel.isPlaceholderTextVisible) {
- Text(
- text = "Notifications",
- style = MaterialTheme.typography.titleLarge,
- color = MaterialTheme.colorScheme.onSurface,
- modifier = Modifier.align(Alignment.Center),
- )
+
+ content {
+ if (viewModel.isPlaceholderTextVisible) {
+ Box(Modifier.fillMaxSize()) {
+ Text(
+ text = "Notifications",
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.align(Alignment.Center),
+ )
+ }
+ }
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index f3cde53..65a53f5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -98,7 +98,7 @@
key = QuickSettings.Elements.Content,
modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 300.dp)
) {
- QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState)
+ content { QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) }
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index 4bbb78b..99f81ee 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -50,7 +50,7 @@
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.ValueKey
-import com.android.compose.animation.scene.animateSharedFloatAsState
+import com.android.compose.animation.scene.animateSceneFloatAsState
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.settingslib.Utils
import com.android.systemui.battery.BatteryMeterView
@@ -69,7 +69,6 @@
object ShadeHeader {
object Elements {
- val FormatPlaceholder = ElementKey("ShadeHeaderFormatPlaceholder")
val ExpandedContent = ElementKey("ShadeHeaderExpandedContent")
val CollapsedContent = ElementKey("ShadeHeaderCollapsedContent")
}
@@ -92,14 +91,7 @@
statusBarIconController: StatusBarIconController,
modifier: Modifier = Modifier,
) {
- // TODO(b/298153892): Remove this once animateSharedFloatAsState.element can be null.
- Spacer(Modifier.element(ShadeHeader.Elements.FormatPlaceholder))
- val formatProgress =
- animateSharedFloatAsState(
- 0.0f,
- ShadeHeader.Keys.transitionProgress,
- ShadeHeader.Elements.FormatPlaceholder
- )
+ val formatProgress = animateSceneFloatAsState(0.0f, ShadeHeader.Keys.transitionProgress)
val cutoutWidth = LocalDisplayCutout.current.width()
val cutoutLocation = LocalDisplayCutout.current.location
@@ -217,14 +209,7 @@
statusBarIconController: StatusBarIconController,
modifier: Modifier = Modifier,
) {
- // TODO(b/298153892): Remove this once animateSharedFloatAsState.element can be null.
- Spacer(Modifier.element(ShadeHeader.Elements.FormatPlaceholder))
- val formatProgress =
- animateSharedFloatAsState(
- 1.0f,
- ShadeHeader.Keys.transitionProgress,
- ShadeHeader.Elements.FormatPlaceholder
- )
+ val formatProgress = animateSceneFloatAsState(1.0f, ShadeHeader.Keys.transitionProgress)
val useExpandedFormat by
remember(formatProgress) { derivedStateOf { formatProgress.value > 0.5f } }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
index 2944bd9..b26194f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -17,10 +17,15 @@
package com.android.compose.animation.scene
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.unit.Dp
@@ -28,180 +33,263 @@
import com.android.compose.ui.util.lerp
/**
- * Animate a shared Int value.
+ * A [State] whose [value] is animated.
*
- * @see SceneScope.animateSharedValueAsState
+ * Important: This animated value should always be ready *after* composition, e.g. during layout,
+ * drawing or inside a LaunchedEffect. If you read [value] during composition, it will probably
+ * throw an exception, for 2 important reasons:
+ * 1. You should never read animated values during composition, because this will probably lead to
+ * bad performance.
+ * 2. Given that this value depends on the target value in different scenes, its current value
+ * (depending on the current transition state) can only be computed once the full tree has been
+ * composed.
+ *
+ * If you don't have the choice and *have to* get the value during composition, for instance because
+ * a Modifier or Composable reading this value does not have a lazy/lambda-based API, then you can
+ * access [unsafeCompositionState] and use a fallback value for the first frame where this animated
+ * value can not be computed yet. Note however that doing so will be bad for performance and might
+ * lead to late-by-one-frame flickers.
+ */
+@Stable
+interface AnimatedState<T> : State<T> {
+ /**
+ * Return a [State] that can be read during composition.
+ *
+ * Important: You should avoid using this as much as possible and instead read [value] during
+ * layout/drawing, otherwise you will probably end up with a few frames that have a value that
+ * is not correctly interpolated.
+ */
+ @Composable fun unsafeCompositionState(initialValue: T): State<T>
+}
+
+/**
+ * Animate a scene Int value.
+ *
+ * @see SceneScope.animateSceneValueAsState
*/
@Composable
-fun SceneScope.animateSharedIntAsState(
+fun SceneScope.animateSceneIntAsState(
value: Int,
key: ValueKey,
- element: ElementKey?,
canOverflow: Boolean = true,
-): State<Int> {
- return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
+): AnimatedState<Int> {
+ return animateSceneValueAsState(value, key, ::lerp, canOverflow)
}
/**
- * Animate a shared Int value.
+ * Animate a shared element Int value.
*
- * @see MovableElementScope.animateSharedValueAsState
+ * @see ElementScope.animateElementValueAsState
*/
@Composable
-fun MovableElementScope.animateSharedIntAsState(
+fun ElementScope<*>.animateElementIntAsState(
value: Int,
- debugName: String,
+ key: ValueKey,
canOverflow: Boolean = true,
-): State<Int> {
- return animateSharedValueAsState(value, debugName, ::lerp, canOverflow)
+): AnimatedState<Int> {
+ return animateElementValueAsState(value, key, ::lerp, canOverflow)
}
/**
- * Animate a shared Float value.
+ * Animate a scene Float value.
*
- * @see SceneScope.animateSharedValueAsState
+ * @see SceneScope.animateSceneValueAsState
*/
@Composable
-fun SceneScope.animateSharedFloatAsState(
+fun SceneScope.animateSceneFloatAsState(
value: Float,
key: ValueKey,
- element: ElementKey?,
canOverflow: Boolean = true,
-): State<Float> {
- return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
+): AnimatedState<Float> {
+ return animateSceneValueAsState(value, key, ::lerp, canOverflow)
}
/**
- * Animate a shared Float value.
+ * Animate a shared element Float value.
*
- * @see MovableElementScope.animateSharedValueAsState
+ * @see ElementScope.animateElementValueAsState
*/
@Composable
-fun MovableElementScope.animateSharedFloatAsState(
+fun ElementScope<*>.animateElementFloatAsState(
value: Float,
- debugName: String,
+ key: ValueKey,
canOverflow: Boolean = true,
-): State<Float> {
- return animateSharedValueAsState(value, debugName, ::lerp, canOverflow)
+): AnimatedState<Float> {
+ return animateElementValueAsState(value, key, ::lerp, canOverflow)
}
/**
- * Animate a shared Dp value.
+ * Animate a scene Dp value.
*
- * @see SceneScope.animateSharedValueAsState
+ * @see SceneScope.animateSceneValueAsState
*/
@Composable
-fun SceneScope.animateSharedDpAsState(
+fun SceneScope.animateSceneDpAsState(
value: Dp,
key: ValueKey,
- element: ElementKey?,
canOverflow: Boolean = true,
-): State<Dp> {
- return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
+): AnimatedState<Dp> {
+ return animateSceneValueAsState(value, key, ::lerp, canOverflow)
}
/**
- * Animate a shared Dp value.
+ * Animate a shared element Dp value.
*
- * @see MovableElementScope.animateSharedValueAsState
+ * @see ElementScope.animateElementValueAsState
*/
@Composable
-fun MovableElementScope.animateSharedDpAsState(
+fun ElementScope<*>.animateElementDpAsState(
value: Dp,
- debugName: String,
+ key: ValueKey,
canOverflow: Boolean = true,
-): State<Dp> {
- return animateSharedValueAsState(value, debugName, ::lerp, canOverflow)
+): AnimatedState<Dp> {
+ return animateElementValueAsState(value, key, ::lerp, canOverflow)
}
/**
- * Animate a shared Color value.
+ * Animate a scene Color value.
*
- * @see SceneScope.animateSharedValueAsState
+ * @see SceneScope.animateSceneValueAsState
*/
@Composable
-fun SceneScope.animateSharedColorAsState(
+fun SceneScope.animateSceneColorAsState(
value: Color,
key: ValueKey,
- element: ElementKey?,
-): State<Color> {
- return animateSharedValueAsState(value, key, element, ::lerp, canOverflow = false)
+): AnimatedState<Color> {
+ return animateSceneValueAsState(value, key, ::lerp, canOverflow = false)
}
/**
- * Animate a shared Color value.
+ * Animate a shared element Color value.
*
- * @see MovableElementScope.animateSharedValueAsState
+ * @see ElementScope.animateElementValueAsState
*/
@Composable
-fun MovableElementScope.animateSharedColorAsState(
+fun ElementScope<*>.animateElementColorAsState(
value: Color,
- debugName: String,
-): State<Color> {
- return animateSharedValueAsState(value, debugName, ::lerp, canOverflow = false)
+ key: ValueKey,
+): AnimatedState<Color> {
+ return animateElementValueAsState(value, key, ::lerp, canOverflow = false)
}
@Composable
internal fun <T> animateSharedValueAsState(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
- element: Element?,
+ scene: SceneKey,
+ element: ElementKey?,
key: ValueKey,
value: T,
lerp: (T, T, Float) -> T,
canOverflow: Boolean,
-): State<T> {
- val sharedValue =
- Snapshot.withoutReadObservation {
- val sharedValues =
- element?.sceneValues?.getValue(scene.key)?.sharedValues ?: scene.sharedValues
- sharedValues.getOrPut(key) { Element.SharedValue(key, value) } as Element.SharedValue<T>
- }
+): AnimatedState<T> {
+ DisposableEffect(layoutImpl, scene, element, key) {
+ // Create the associated maps that hold the current value for each (element, scene) pair.
+ val valueMap = layoutImpl.sharedValues.getOrPut(key) { mutableMapOf() }
+ val sceneToValueMap =
+ valueMap.getOrPut(element) { SnapshotStateMap<SceneKey, Any>() }
+ as SnapshotStateMap<SceneKey, T>
+ sceneToValueMap[scene] = value
- if (value != sharedValue.value) {
- sharedValue.value = value
+ onDispose {
+ // Remove the value associated to the current scene, and eventually remove the maps if
+ // they are empty.
+ sceneToValueMap.remove(scene)
+
+ if (sceneToValueMap.isEmpty() && valueMap[element] === sceneToValueMap) {
+ valueMap.remove(element)
+
+ if (valueMap.isEmpty() && layoutImpl.sharedValues[key] === valueMap) {
+ layoutImpl.sharedValues.remove(key)
+ }
+ }
+ }
}
- return remember(layoutImpl, element, sharedValue, lerp, canOverflow) {
- derivedStateOf { computeValue(layoutImpl, element, sharedValue, lerp, canOverflow) }
+ // Update the current value. Note that side effects run after disposable effects, so we know
+ // that the associated maps were created at this point.
+ SideEffect { sceneToValueMap<T>(layoutImpl, key, element)[scene] = value }
+
+ return remember(layoutImpl, scene, element, lerp, canOverflow) {
+ object : AnimatedState<T> {
+ override val value: T
+ get() = value(layoutImpl, scene, element, key, lerp, canOverflow)
+
+ @Composable
+ override fun unsafeCompositionState(initialValue: T): State<T> {
+ val state = remember { mutableStateOf(initialValue) }
+
+ val animatedState = this
+ LaunchedEffect(animatedState) {
+ snapshotFlow { animatedState.value }.collect { state.value = it }
+ }
+
+ return state
+ }
+ }
}
}
-private fun <T> computeValue(
+private fun <T> sceneToValueMap(
layoutImpl: SceneTransitionLayoutImpl,
- element: Element?,
- sharedValue: Element.SharedValue<T>,
+ key: ValueKey,
+ element: ElementKey?
+): MutableMap<SceneKey, T> {
+ return layoutImpl.sharedValues[key]?.get(element)?.let { it as SnapshotStateMap<SceneKey, T> }
+ ?: error(valueReadTooEarlyMessage(key))
+}
+
+private fun valueReadTooEarlyMessage(key: ValueKey) =
+ "Animated value $key was read before its target values were set. This probably " +
+ "means that you are reading it during composition, which you should not do. See the " +
+ "documentation of AnimatedState for more information."
+
+private fun <T> value(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: SceneKey,
+ element: ElementKey?,
+ key: ValueKey,
lerp: (T, T, Float) -> T,
canOverflow: Boolean,
): T {
- val transition = layoutImpl.state.currentTransition
- if (transition == null || !layoutImpl.isTransitionReady(transition)) {
- return sharedValue.value
- }
+ return valueOrNull(layoutImpl, scene, element, key, lerp, canOverflow)
+ ?: error(valueReadTooEarlyMessage(key))
+}
- fun sceneValue(scene: SceneKey): Element.SharedValue<T>? {
- val sharedValues =
- if (element == null) {
- layoutImpl.scene(scene).sharedValues
- } else {
- element.sceneValues[scene]?.sharedValues
- }
- ?: return null
- val value = sharedValues[sharedValue.key] ?: return null
- return value as Element.SharedValue<T>
- }
+private fun <T> valueOrNull(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: SceneKey,
+ element: ElementKey?,
+ key: ValueKey,
+ lerp: (T, T, Float) -> T,
+ canOverflow: Boolean,
+): T? {
+ val sceneToValueMap = sceneToValueMap<T>(layoutImpl, key, element)
+ fun sceneValue(scene: SceneKey): T? = sceneToValueMap[scene]
- val fromValue = sceneValue(transition.fromScene)
- val toValue = sceneValue(transition.toScene)
- return if (fromValue != null && toValue != null) {
- val progress =
- if (canOverflow) transition.progress else transition.progress.coerceIn(0f, 1f)
- lerp(fromValue.value, toValue.value, progress)
- } else if (fromValue != null) {
- fromValue.value
- } else if (toValue != null) {
- toValue.value
- } else {
- sharedValue.value
+ return when (val transition = layoutImpl.state.transitionState) {
+ is TransitionState.Idle -> sceneValue(transition.currentScene)
+ is TransitionState.Transition -> {
+ // Note: no need to check for transition ready here given that all target values are
+ // defined during composition, we should already have the correct values to interpolate
+ // between here.
+ val fromValue = sceneValue(transition.fromScene)
+ val toValue = sceneValue(transition.toScene)
+ if (fromValue != null && toValue != null) {
+ if (fromValue == toValue) {
+ // Optimization: avoid reading progress if the values are the same, so we don't
+ // relayout/redraw for nothing.
+ fromValue
+ } else {
+ val progress =
+ if (canOverflow) transition.progress
+ else transition.progress.coerceIn(0f, 1f)
+ lerp(fromValue, toValue, progress)
+ }
+ } else fromValue ?: toValue
+ }
}
+ // TODO(b/311600838): Remove this. We should not have to fallback to the current scene value,
+ // but we have to because code of removed nodes can still run if they are placed with a graphics
+ // layer.
+ ?: sceneValue(scene)
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index a85d9bf..280fbfb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -16,15 +16,10 @@
package com.android.compose.animation.scene
-import android.graphics.Picture
-import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -52,43 +47,22 @@
@Stable
internal class Element(val key: ElementKey) {
/**
- * The last values of this element, coming from any scene. Note that this value will be unstable
+ * The last state of this element, coming from any scene. Note that this state will be unstable
* if this element is present in multiple scenes but the shared element animation is disabled,
- * given that multiple instances of the element with different states will write to these
- * values. You should prefer using [TargetValues.lastValues] in the current scene if it is
- * defined.
+ * given that multiple instances of the element with different states will write to this state.
+ * You should prefer using [SceneState.lastState] in the current scene when it is defined.
*/
- val lastSharedValues = Values()
+ val lastSharedState = State()
- /** The mapping between a scene and the values/state this element has in that scene, if any. */
- val sceneValues = SnapshotStateMap<SceneKey, TargetValues>()
-
- /**
- * The movable content of this element, if this element is composed using
- * [SceneScope.MovableElement].
- */
- private var _movableContent: (@Composable (@Composable () -> Unit) -> Unit)? = null
- val movableContent: @Composable (@Composable () -> Unit) -> Unit
- get() =
- _movableContent
- ?: movableContentOf { content: @Composable () -> Unit -> content() }
- .also { _movableContent = it }
-
- /**
- * The [Picture] to which we save the last drawing commands of this element, if it is movable.
- * This is necessary because the content of this element might not be composed in the scene it
- * should currently be drawn.
- */
- private var _picture: Picture? = null
- val picture: Picture
- get() = _picture ?: Picture().also { _picture = it }
+ /** The mapping between a scene and the state this element has in that scene, if any. */
+ val sceneStates = mutableMapOf<SceneKey, SceneState>()
override fun toString(): String {
return "Element(key=$key)"
}
- /** The current values of this element, either in a specific scene or in a shared context. */
- class Values {
+ /** The state of this element, either in a specific scene or in a shared context. */
+ class State {
/** The offset of the element, relative to the SceneTransitionLayout containing it. */
var offset = Offset.Unspecified
@@ -102,16 +76,14 @@
var alpha = AlphaUnspecified
}
- /** The target values of this element in a given scene. */
+ /** The last and target state of this element in a given scene. */
@Stable
- class TargetValues(val scene: SceneKey) {
- val lastValues = Values()
+ class SceneState(val scene: SceneKey) {
+ val lastState = State()
var targetSize by mutableStateOf(SizeUnspecified)
var targetOffset by mutableStateOf(Offset.Unspecified)
- val sharedValues = SnapshotStateMap<ValueKey, SharedValue<*>>()
-
/**
* The attached [ElementNode] a Modifier.element() for a given element and scene. During
* composition, this set could have 0 to 2 elements. After composition and after all
@@ -120,12 +92,6 @@
val nodes = mutableSetOf<ElementNode>()
}
- /** A shared value of this element. */
- @Stable
- class SharedValue<T>(val key: ValueKey, initialValue: T) {
- var value by mutableStateOf(initialValue)
- }
-
companion object {
val SizeUnspecified = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
val AlphaUnspecified = Float.MIN_VALUE
@@ -147,27 +113,18 @@
scene: Scene,
key: ElementKey,
): Modifier {
- val element: Element
- val sceneValues: Element.TargetValues
-
- // Get the element associated to [key] if it was already composed in another scene,
- // otherwise create it and add it to our Map<ElementKey, Element>. This is done inside a
- // withoutReadObservation() because there is no need to recompose when that map is mutated.
- Snapshot.withoutReadObservation {
- element = layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
- sceneValues =
- element.sceneValues[scene.key]
- ?: Element.TargetValues(scene.key).also { element.sceneValues[scene.key] = it }
- }
-
- return this.then(ElementModifier(layoutImpl, scene, element, sceneValues))
+ return this.then(ElementModifier(layoutImpl, scene, key))
// TODO(b/311132415): Move this into ElementNode once we can create a delegate
// IntermediateLayoutModifierNode.
.intermediateLayout { measurable, constraints ->
- val placeable =
- measure(layoutImpl, scene, element, sceneValues, measurable, constraints)
+ // TODO(b/311132415): No need to fetch the element and sceneState from the map anymore
+ // once this is merged into ElementNode.
+ val element = layoutImpl.elements.getValue(key)
+ val sceneState = element.sceneStates.getValue(scene.key)
+
+ val placeable = measure(layoutImpl, scene, element, sceneState, measurable, constraints)
layout(placeable.width, placeable.height) {
- place(layoutImpl, scene, element, sceneValues, placeable, placementScope = this)
+ place(layoutImpl, scene, element, sceneState, placeable, placementScope = this)
}
}
.testTag(key.testTag)
@@ -180,72 +137,89 @@
private data class ElementModifier(
private val layoutImpl: SceneTransitionLayoutImpl,
private val scene: Scene,
- private val element: Element,
- private val sceneValues: Element.TargetValues,
+ private val key: ElementKey,
) : ModifierNodeElement<ElementNode>() {
- override fun create(): ElementNode = ElementNode(layoutImpl, scene, element, sceneValues)
+ override fun create(): ElementNode = ElementNode(layoutImpl, scene, key)
override fun update(node: ElementNode) {
- node.update(layoutImpl, scene, element, sceneValues)
+ node.update(layoutImpl, scene, key)
}
}
internal class ElementNode(
private var layoutImpl: SceneTransitionLayoutImpl,
private var scene: Scene,
- private var element: Element,
- private var sceneValues: Element.TargetValues,
+ private var key: ElementKey,
) : Modifier.Node(), DrawModifierNode {
+ private var _element: Element? = null
+ private val element: Element
+ get() = _element!!
+
+ private var _sceneState: Element.SceneState? = null
+ private val sceneState: Element.SceneState
+ get() = _sceneState!!
override fun onAttach() {
super.onAttach()
- addNodeToSceneValues()
+ updateElementAndSceneValues()
+ addNodeToSceneState()
}
- private fun addNodeToSceneValues() {
- sceneValues.nodes.add(this)
+ private fun updateElementAndSceneValues() {
+ val element =
+ layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
+ _element = element
+ _sceneState =
+ element.sceneStates[scene.key]
+ ?: Element.SceneState(scene.key).also { element.sceneStates[scene.key] = it }
+ }
+
+ private fun addNodeToSceneState() {
+ sceneState.nodes.add(this)
coroutineScope.launch {
// At this point all [CodeLocationNode] have been attached or detached, which means that
- // [sceneValues.codeLocations] should have exactly 1 element, otherwise this means that
+ // [sceneState.codeLocations] should have exactly 1 element, otherwise this means that
// this element was composed multiple times in the same scene.
- val nCodeLocations = sceneValues.nodes.size
- if (nCodeLocations != 1 || !sceneValues.nodes.contains(this@ElementNode)) {
- error("${element.key} was composed $nCodeLocations times in ${sceneValues.scene}")
+ val nCodeLocations = sceneState.nodes.size
+ if (nCodeLocations != 1 || !sceneState.nodes.contains(this@ElementNode)) {
+ error("$key was composed $nCodeLocations times in ${sceneState.scene}")
}
}
}
override fun onDetach() {
super.onDetach()
- removeNodeFromSceneValues()
- maybePruneMaps(layoutImpl, element, sceneValues)
+ removeNodeFromSceneState()
+ maybePruneMaps(layoutImpl, element, sceneState)
+
+ _element = null
+ _sceneState = null
}
- private fun removeNodeFromSceneValues() {
- sceneValues.nodes.remove(this)
+ private fun removeNodeFromSceneState() {
+ sceneState.nodes.remove(this)
}
fun update(
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
- element: Element,
- sceneValues: Element.TargetValues,
+ key: ElementKey,
) {
check(layoutImpl == this.layoutImpl && scene == this.scene)
- removeNodeFromSceneValues()
+ removeNodeFromSceneState()
val prevElement = this.element
- val prevSceneValues = this.sceneValues
- this.element = element
- this.sceneValues = sceneValues
+ val prevSceneState = this.sceneState
+ this.key = key
+ updateElementAndSceneValues()
- addNodeToSceneValues()
- maybePruneMaps(layoutImpl, prevElement, prevSceneValues)
+ addNodeToSceneState()
+ maybePruneMaps(layoutImpl, prevElement, prevSceneState)
}
override fun ContentDrawScope.draw() {
- val drawScale = getDrawScale(layoutImpl, element, scene, sceneValues)
+ val drawScale = getDrawScale(layoutImpl, element, scene, sceneState)
if (drawScale == Scale.Default) {
drawContent()
} else {
@@ -263,18 +237,16 @@
private fun maybePruneMaps(
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
) {
// If element is not composed from this scene anymore, remove the scene values. This
// works because [onAttach] is called before [onDetach], so if an element is moved from
// the UI tree we will first add the new code location then remove the old one.
- if (
- sceneValues.nodes.isEmpty() && element.sceneValues[sceneValues.scene] == sceneValues
- ) {
- element.sceneValues.remove(sceneValues.scene)
+ if (sceneState.nodes.isEmpty() && element.sceneStates[sceneState.scene] == sceneState) {
+ element.sceneStates.remove(sceneState.scene)
// If the element is not composed in any scene, remove it from the elements map.
- if (element.sceneValues.isEmpty() && layoutImpl.elements[element.key] == element) {
+ if (element.sceneStates.isEmpty() && layoutImpl.elements[element.key] == element) {
layoutImpl.elements.remove(element.key)
}
}
@@ -293,8 +265,8 @@
if (
transition == null ||
!layoutImpl.isTransitionReady(transition) ||
- transition.fromScene !in element.sceneValues ||
- transition.toScene !in element.sceneValues
+ transition.fromScene !in element.sceneStates ||
+ transition.toScene !in element.sceneStates
) {
return true
}
@@ -310,7 +282,6 @@
transition,
scene.key,
element.key,
- sharedTransformation,
)
}
@@ -319,17 +290,14 @@
transition: TransitionState.Transition,
scene: SceneKey,
element: ElementKey,
- sharedTransformation: SharedElementTransformation?
): Boolean {
- val scenePicker = sharedTransformation?.scenePicker ?: DefaultSharedElementScenePicker
+ val scenePicker = element.scenePicker
val fromScene = transition.fromScene
val toScene = transition.toScene
return scenePicker.sceneDuringTransition(
element = element,
- fromScene = fromScene,
- toScene = toScene,
- progress = transition::progress,
+ transition = transition,
fromSceneZIndex = layoutImpl.scenes.getValue(fromScene).zIndex,
toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex,
) == scene
@@ -374,28 +342,28 @@
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
scene: Scene,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
): Boolean {
val transition = layoutImpl.state.currentTransition ?: return true
if (!layoutImpl.isTransitionReady(transition)) {
val lastValue =
- sceneValues.lastValues.alpha.takeIf { it != Element.AlphaUnspecified }
- ?: element.lastSharedValues.alpha.takeIf { it != Element.AlphaUnspecified } ?: 1f
+ sceneState.lastState.alpha.takeIf { it != Element.AlphaUnspecified }
+ ?: element.lastSharedState.alpha.takeIf { it != Element.AlphaUnspecified } ?: 1f
return lastValue == 1f
}
val fromScene = transition.fromScene
val toScene = transition.toScene
- val fromValues = element.sceneValues[fromScene]
- val toValues = element.sceneValues[toScene]
+ val fromState = element.sceneStates[fromScene]
+ val toState = element.sceneStates[toScene]
- if (fromValues == null && toValues == null) {
+ if (fromState == null && toState == null) {
error("This should not happen, element $element is neither in $fromScene or $toScene")
}
- val isSharedElement = fromValues != null && toValues != null
+ val isSharedElement = fromState != null && toState != null
if (isSharedElement && isSharedElementEnabled(layoutImpl.state, transition, element.key)) {
return true
}
@@ -415,7 +383,7 @@
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
scene: Scene,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
): Float {
return computeValue(
layoutImpl,
@@ -426,9 +394,8 @@
idleValue = 1f,
currentValue = { 1f },
lastValue = {
- sceneValues.lastValues.alpha.takeIf { it != Element.AlphaUnspecified }
- ?: element.lastSharedValues.alpha.takeIf { it != Element.AlphaUnspecified }
- ?: 1f
+ sceneState.lastState.alpha.takeIf { it != Element.AlphaUnspecified }
+ ?: element.lastSharedState.alpha.takeIf { it != Element.AlphaUnspecified } ?: 1f
},
::lerp,
)
@@ -440,15 +407,15 @@
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
element: Element,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
measurable: Measurable,
constraints: Constraints,
): Placeable {
// Update the size this element has in this scene when idle.
val targetSizeInScene = lookaheadSize
- if (targetSizeInScene != sceneValues.targetSize) {
+ if (targetSizeInScene != sceneState.targetSize) {
// TODO(b/290930950): Better handle when this changes to avoid instant size jumps.
- sceneValues.targetSize = targetSizeInScene
+ sceneState.targetSize = targetSizeInScene
}
// Some lambdas called (max once) by computeValue() will need to measure [measurable], in which
@@ -468,8 +435,8 @@
idleValue = lookaheadSize,
currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
lastValue = {
- sceneValues.lastValues.size.takeIf { it != Element.SizeUnspecified }
- ?: element.lastSharedValues.size.takeIf { it != Element.SizeUnspecified }
+ sceneState.lastState.size.takeIf { it != Element.SizeUnspecified }
+ ?: element.lastSharedState.size.takeIf { it != Element.SizeUnspecified }
?: measurable.measure(constraints).also { maybePlaceable = it }.size()
},
::lerp,
@@ -485,8 +452,8 @@
)
val size = placeable.size()
- element.lastSharedValues.size = size
- sceneValues.lastValues.size = size
+ element.lastSharedState.size = size
+ sceneState.lastState.size = size
return placeable
}
@@ -494,7 +461,7 @@
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
scene: Scene,
- sceneValues: Element.TargetValues
+ sceneState: Element.SceneState
): Scale {
return computeValue(
layoutImpl,
@@ -505,8 +472,8 @@
idleValue = Scale.Default,
currentValue = { Scale.Default },
lastValue = {
- sceneValues.lastValues.drawScale.takeIf { it != Scale.Default }
- ?: element.lastSharedValues.drawScale
+ sceneState.lastState.drawScale.takeIf { it != Scale.Default }
+ ?: element.lastSharedState.drawScale
},
::lerp,
)
@@ -517,7 +484,7 @@
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
element: Element,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
placeable: Placeable,
placementScope: Placeable.PlacementScope,
) {
@@ -526,14 +493,14 @@
// when idle.
val coords = coordinates ?: error("Element ${element.key} does not have any coordinates")
val targetOffsetInScene = lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
- if (targetOffsetInScene != sceneValues.targetOffset) {
+ if (targetOffsetInScene != sceneState.targetOffset) {
// TODO(b/290930950): Better handle when this changes to avoid instant offset jumps.
- sceneValues.targetOffset = targetOffsetInScene
+ sceneState.targetOffset = targetOffsetInScene
}
val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero)
- val lastSharedValues = element.lastSharedValues
- val lastValues = sceneValues.lastValues
+ val lastSharedState = element.lastSharedState
+ val lastSceneState = sceneState.lastState
val targetOffset =
computeValue(
layoutImpl,
@@ -544,36 +511,36 @@
idleValue = targetOffsetInScene,
currentValue = { currentOffset },
lastValue = {
- lastValues.offset.takeIf { it.isSpecified }
- ?: lastSharedValues.offset.takeIf { it.isSpecified } ?: currentOffset
+ lastSceneState.offset.takeIf { it.isSpecified }
+ ?: lastSharedState.offset.takeIf { it.isSpecified } ?: currentOffset
},
::lerp,
)
- lastSharedValues.offset = targetOffset
- lastValues.offset = targetOffset
+ lastSharedState.offset = targetOffset
+ lastSceneState.offset = targetOffset
// No need to place the element in this scene if we don't want to draw it anyways. Note that
- // it's still important to compute the target offset and update lastValues, otherwise it
- // will be out of date.
+ // it's still important to compute the target offset and update last(Shared|Scene)State,
+ // otherwise they will be out of date.
if (!shouldDrawElement(layoutImpl, scene, element)) {
return
}
val offset = (targetOffset - currentOffset).round()
- if (isElementOpaque(layoutImpl, element, scene, sceneValues)) {
+ if (isElementOpaque(layoutImpl, element, scene, sceneState)) {
// TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is not
// animated once b/305195729 is fixed. Test that drawing is not invalidated in that
// case.
placeable.place(offset)
- lastSharedValues.alpha = 1f
- lastValues.alpha = 1f
+ lastSharedState.alpha = 1f
+ lastSceneState.alpha = 1f
} else {
placeable.placeWithLayer(offset) {
- val alpha = elementAlpha(layoutImpl, element, scene, sceneValues)
+ val alpha = elementAlpha(layoutImpl, element, scene, sceneState)
this.alpha = alpha
- lastSharedValues.alpha = alpha
- lastValues.alpha = alpha
+ lastSharedState.alpha = alpha
+ lastSceneState.alpha = alpha
}
}
}
@@ -605,7 +572,7 @@
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
element: Element,
- sceneValue: (Element.TargetValues) -> T,
+ sceneValue: (Element.SceneState) -> T,
transformation: (ElementTransformations) -> PropertyTransformation<T>?,
idleValue: T,
currentValue: () -> T,
@@ -628,10 +595,10 @@
val fromScene = transition.fromScene
val toScene = transition.toScene
- val fromValues = element.sceneValues[fromScene]
- val toValues = element.sceneValues[toScene]
+ val fromState = element.sceneStates[fromScene]
+ val toState = element.sceneStates[toScene]
- if (fromValues == null && toValues == null) {
+ if (fromState == null && toState == null) {
// TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
// run anymore.
return lastValue()
@@ -640,10 +607,10 @@
// The element is shared: interpolate between the value in fromScene and the value in toScene.
// TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
// elements follow the finger direction.
- val isSharedElement = fromValues != null && toValues != null
+ val isSharedElement = fromState != null && toState != null
if (isSharedElement && isSharedElementEnabled(layoutImpl.state, transition, element.key)) {
- val start = sceneValue(fromValues!!)
- val end = sceneValue(toValues!!)
+ val start = sceneValue(fromState!!)
+ val end = sceneValue(toState!!)
// Make sure we don't read progress if values are the same and we don't need to interpolate,
// so we don't invalidate the phase where this is read.
@@ -659,12 +626,12 @@
// Get the transformed value, i.e. the target value at the beginning (for entering elements) or
// end (for leaving elements) of the transition.
- val sceneValues =
+ val sceneState =
checkNotNull(
when {
- isSharedElement && scene.key == fromScene -> fromValues
- isSharedElement -> toValues
- else -> fromValues ?: toValues
+ isSharedElement && scene.key == fromScene -> fromState
+ isSharedElement -> toState
+ else -> fromState ?: toState
}
)
@@ -673,7 +640,7 @@
layoutImpl,
scene,
element,
- sceneValues,
+ sceneState,
transition,
idleValue,
)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index 84d3b86..90f46bd 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -64,10 +64,10 @@
identity: Any = Object(),
/**
- * Whether this element is a background and usually drawn below other elements. This should be
- * set to true to make sure that shared backgrounds are drawn below elements of other scenes.
+ * The [ElementScenePicker] to use when deciding in which scene we should draw shared Elements
+ * or compose MovableElements.
*/
- val isBackground: Boolean = false,
+ val scenePicker: ElementScenePicker = DefaultElementScenePicker,
) : Key(name, identity), ElementMatcher {
@VisibleForTesting
// TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
index 49df2f6..af3c099 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
@@ -16,27 +16,36 @@
package com.android.compose.animation.scene
-import android.util.Log
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.draw.drawWithCache
-import androidx.compose.ui.graphics.Canvas
-import androidx.compose.ui.graphics.drawscope.draw
-import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
-import androidx.compose.ui.graphics.nativeCanvas
-import androidx.compose.ui.layout.layout
-import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.IntSize
-private const val TAG = "MovableElement"
+@Composable
+internal fun Element(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ key: ElementKey,
+ modifier: Modifier,
+ content: @Composable ElementScope<ElementContentScope>.() -> Unit,
+) {
+ Box(modifier.element(layoutImpl, scene, key)) {
+ val sceneScope = scene.scope
+ val boxScope = this
+ val elementScope =
+ remember(layoutImpl, key, scene, sceneScope, boxScope) {
+ ElementScopeImpl(layoutImpl, key, scene, sceneScope, boxScope)
+ }
+
+ content(elementScope)
+ }
+}
@Composable
internal fun MovableElement(
@@ -44,72 +53,113 @@
scene: Scene,
key: ElementKey,
modifier: Modifier,
- content: @Composable MovableElementScope.() -> Unit,
+ content: @Composable ElementScope<MovableElementContentScope>.() -> Unit,
) {
Box(modifier.element(layoutImpl, scene, key)) {
- // Get the Element from the map. It will always be the same and we don't want to recompose
- // every time an element is added/removed from SceneTransitionLayoutImpl.elements, so we
- // disable read observation during the look-up in that map.
- val element = Snapshot.withoutReadObservation { layoutImpl.elements.getValue(key) }
- val movableElementScope =
- remember(layoutImpl, element, scene) {
- MovableElementScopeImpl(layoutImpl, element, scene)
+ val sceneScope = scene.scope
+ val boxScope = this
+ val elementScope =
+ remember(layoutImpl, key, scene, sceneScope, boxScope) {
+ MovableElementScopeImpl(layoutImpl, key, scene, sceneScope, boxScope)
}
- // The [Picture] to which we save the last drawing commands of this element. This is
- // necessary because the content of this element might not be composed in this scene, in
- // which case we still need to draw it.
- val picture = element.picture
+ content(elementScope)
+ }
+}
+private abstract class BaseElementScope<ContentScope>(
+ private val layoutImpl: SceneTransitionLayoutImpl,
+ private val element: ElementKey,
+ private val scene: Scene,
+) : ElementScope<ContentScope> {
+ @Composable
+ override fun <T> animateElementValueAsState(
+ value: T,
+ key: ValueKey,
+ lerp: (start: T, stop: T, fraction: Float) -> T,
+ canOverflow: Boolean
+ ): AnimatedState<T> {
+ return animateSharedValueAsState(
+ layoutImpl,
+ scene.key,
+ element,
+ key,
+ value,
+ lerp,
+ canOverflow,
+ )
+ }
+}
+
+private class ElementScopeImpl(
+ layoutImpl: SceneTransitionLayoutImpl,
+ element: ElementKey,
+ scene: Scene,
+ private val sceneScope: SceneScope,
+ private val boxScope: BoxScope,
+) : BaseElementScope<ElementContentScope>(layoutImpl, element, scene) {
+ private val contentScope =
+ object : ElementContentScope, SceneScope by sceneScope, BoxScope by boxScope {}
+
+ @Composable
+ override fun content(content: @Composable ElementContentScope.() -> Unit) {
+ contentScope.content()
+ }
+}
+
+private class MovableElementScopeImpl(
+ private val layoutImpl: SceneTransitionLayoutImpl,
+ private val element: ElementKey,
+ private val scene: Scene,
+ private val sceneScope: BaseSceneScope,
+ private val boxScope: BoxScope,
+) : BaseElementScope<MovableElementContentScope>(layoutImpl, element, scene) {
+ private val contentScope =
+ object : MovableElementContentScope, BaseSceneScope by sceneScope, BoxScope by boxScope {}
+
+ @Composable
+ override fun content(content: @Composable MovableElementContentScope.() -> Unit) {
// Whether we should compose the movable element here. The scene picker logic to know in
// which scene we should compose/draw a movable element might depend on the current
// transition progress, so we put this in a derivedStateOf to prevent many recompositions
// during the transition.
+ // TODO(b/317026105): Use derivedStateOf only if the scene picker reads the progress in its
+ // logic.
val shouldComposeMovableElement by
remember(layoutImpl, scene.key, element) {
derivedStateOf { shouldComposeMovableElement(layoutImpl, scene.key, element) }
}
if (shouldComposeMovableElement) {
- Box(
- Modifier.drawWithCache {
- val width = size.width.toInt()
- val height = size.height.toInt()
-
- onDrawWithContent {
- // Save the draw commands into [picture] for later to draw the last content
- // even when this movable content is not composed.
- val pictureCanvas = Canvas(picture.beginRecording(width, height))
- draw(this, this.layoutDirection, pictureCanvas, this.size) {
- this@onDrawWithContent.drawContent()
+ val movableContent: MovableElementContent =
+ layoutImpl.movableContents[element]
+ ?: movableContentOf {
+ contentScope: MovableElementContentScope,
+ content: @Composable MovableElementContentScope.() -> Unit ->
+ contentScope.content()
}
- picture.endRecording()
+ .also { layoutImpl.movableContents[element] = it }
- // Draw the content.
- drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) }
- }
- }
- ) {
- element.movableContent { movableElementScope.content() }
- }
+ // Important: Don't introduce any parent Box or other layout here, because contentScope
+ // delegates its BoxScope implementation to the Box where this content() function is
+ // called, so it's important that this movableContent is composed directly under that
+ // Box.
+ movableContent(contentScope, content)
} else {
- // If we are not composed, we draw the previous drawing commands at the same size as the
- // movable content when it was composed in this scene.
- val sceneValues = element.sceneValues.getValue(scene.key)
-
- Spacer(
- Modifier.layout { measurable, _ ->
- val size =
- sceneValues.targetSize.takeIf { it != Element.SizeUnspecified }
- ?: IntSize.Zero
- val placeable =
- measurable.measure(Constraints.fixed(size.width, size.height))
- layout(size.width, size.height) { placeable.place(0, 0) }
- }
- .drawBehind {
- drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) }
- }
- )
+ // If we are not composed, we still need to lay out an empty space with the same *target
+ // size* as its movable content, i.e. the same *size when idle*. During transitions,
+ // this size will be used to interpolate the transition size, during the intermediate
+ // layout pass.
+ Layout { _, _ ->
+ // No need to measure or place anything.
+ val size =
+ placeholderContentSize(
+ layoutImpl,
+ scene.key,
+ layoutImpl.elements.getValue(element),
+ )
+ layout(size.width, size.height) {}
+ }
}
}
}
@@ -117,7 +167,7 @@
private fun shouldComposeMovableElement(
layoutImpl: SceneTransitionLayoutImpl,
scene: SceneKey,
- element: Element,
+ element: ElementKey,
): Boolean {
val transition =
layoutImpl.state.currentTransition
@@ -130,72 +180,55 @@
val fromReady = layoutImpl.isSceneReady(fromScene)
val toReady = layoutImpl.isSceneReady(toScene)
- val otherScene =
- when (scene) {
- fromScene -> toScene
- toScene -> fromScene
- else ->
- error(
- "shouldComposeMovableElement(scene=$scene) called with fromScene=$fromScene " +
- "and toScene=$toScene"
- )
- }
-
- val isShared = otherScene in element.sceneValues
-
- if (isShared && !toReady && !fromReady) {
- // This should usually not happen given that fromScene should be ready, but let's log a
- // warning here in case it does so it helps debugging flicker issues caused by this part of
- // the code.
- Log.w(
- TAG,
- "MovableElement $element might have to be composed for the first time in both " +
- "fromScene=$fromScene and toScene=$toScene. This will probably lead to a flicker " +
- "where the size of the element will jump from IntSize.Zero to its actual size " +
- "during the transition."
- )
- }
-
- // Element is not shared in this transition.
- if (!isShared) {
- return true
- }
-
- // toScene is not ready (because we are composing it for the first time), so we compose it there
- // first. This is the most common scenario when starting a transition that has a shared movable
- // element.
- if (!toReady) {
+ if (!fromReady && !toReady) {
+ // Neither of the scenes will be drawn, so where we compose it doesn't really matter. Note
+ // that we could have slightly more complicated logic here to optimize for this case, but
+ // it's not worth it given that readyScenes should disappear soon (b/316901148).
return scene == toScene
}
- // This should usually not happen, but if we are also composing for the first time in fromScene
- // then we should compose it there only.
- if (!fromReady) {
- return scene == fromScene
- }
+ // If one of the scenes is not ready, compose it in the other one to make sure it is drawn.
+ if (!fromReady) return scene == toScene
+ if (!toReady) return scene == fromScene
+ // Always compose movable elements in the scene picked by their scene picker.
return shouldDrawOrComposeSharedElement(
layoutImpl,
transition,
scene,
- element.key,
- sharedElementTransformation(layoutImpl.state, transition, element.key),
+ element,
)
}
-private class MovableElementScopeImpl(
- private val layoutImpl: SceneTransitionLayoutImpl,
- private val element: Element,
- private val scene: Scene,
-) : MovableElementScope {
- @Composable
- override fun <T> animateSharedValueAsState(
- value: T,
- debugName: String,
- lerp: (start: T, stop: T, fraction: Float) -> T,
- canOverflow: Boolean,
- ): State<T> {
- val key = remember { ValueKey(debugName) }
- return animateSharedValueAsState(layoutImpl, scene, element, key, value, lerp, canOverflow)
+/**
+ * Return the size of the placeholder/space that is composed when the movable content is not
+ * composed in a scene.
+ */
+private fun placeholderContentSize(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: SceneKey,
+ element: Element,
+): IntSize {
+ // If the content of the movable element was already composed in this scene before, use that
+ // target size.
+ val targetValueInScene = element.sceneStates.getValue(scene).targetSize
+ if (targetValueInScene != Element.SizeUnspecified) {
+ return targetValueInScene
}
+
+ // This code is only run during transitions (otherwise the content would be composed and the
+ // placeholder would not), so it's ok to cast the state into a Transition directly.
+ val transition = layoutImpl.state.transitionState as TransitionState.Transition
+
+ // If the content was already composed in the other scene, we use that target size assuming it
+ // doesn't change between scenes.
+ // TODO(b/317026105): Provide a way to give a hint size/content for cases where this is not
+ // true.
+ val otherScene = if (transition.fromScene == scene) transition.toScene else transition.fromScene
+ val targetValueInOtherScene = element.sceneStates[otherScene]?.targetSize
+ if (targetValueInOtherScene != null && targetValueInOtherScene != Element.SizeUnspecified) {
+ return targetValueInOtherScene
+ }
+
+ return IntSize.Zero
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt
index 560e92b..454c0ec 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt
@@ -75,8 +75,8 @@
if (
bounds == null ||
- bounds.lastSharedValues.size == Element.SizeUnspecified ||
- bounds.lastSharedValues.offset == Offset.Unspecified
+ bounds.lastSharedState.size == Element.SizeUnspecified ||
+ bounds.lastSharedState.offset == Offset.Unspecified
) {
drawContent()
return
@@ -87,14 +87,14 @@
canvas.withSaveLayer(size.toRect(), Paint()) {
drawContent()
- val offset = bounds.lastSharedValues.offset - element.lastSharedValues.offset
+ val offset = bounds.lastSharedState.offset - element.lastSharedState.offset
translate(offset.x, offset.y) { drawHole(bounds) }
}
}
}
private fun DrawScope.drawHole(bounds: Element) {
- val boundsSize = bounds.lastSharedValues.size.toSize()
+ val boundsSize = bounds.lastSharedState.size.toSize()
if (shape == RectangleShape) {
drawRect(Color.Black, size = boundsSize, blendMode = BlendMode.DstOut)
return
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 30e50a9..3537b79 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -20,13 +20,10 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
@@ -45,16 +42,13 @@
actions: Map<UserAction, SceneKey>,
zIndex: Float,
) {
- private val scope = SceneScopeImpl(layoutImpl, this)
+ internal val scope = SceneScopeImpl(layoutImpl, this)
var content by mutableStateOf(content)
var userActions by mutableStateOf(actions)
var zIndex by mutableFloatStateOf(zIndex)
var targetSize by mutableStateOf(IntSize.Zero)
- /** The shared values in this scene that are not tied to a specific element. */
- val sharedValues = SnapshotStateMap<ValueKey, Element.SharedValue<*>>()
-
@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun Content(modifier: Modifier = Modifier) {
@@ -77,7 +71,7 @@
}
}
-private class SceneScopeImpl(
+internal class SceneScopeImpl(
private val layoutImpl: SceneTransitionLayoutImpl,
private val scene: Scene,
) : SceneScope {
@@ -87,6 +81,42 @@
return element(layoutImpl, scene, key)
}
+ @Composable
+ override fun Element(
+ key: ElementKey,
+ modifier: Modifier,
+ content: @Composable (ElementScope<ElementContentScope>.() -> Unit)
+ ) {
+ Element(layoutImpl, scene, key, modifier, content)
+ }
+
+ @Composable
+ override fun MovableElement(
+ key: ElementKey,
+ modifier: Modifier,
+ content: @Composable (ElementScope<MovableElementContentScope>.() -> Unit)
+ ) {
+ MovableElement(layoutImpl, scene, key, modifier, content)
+ }
+
+ @Composable
+ override fun <T> animateSceneValueAsState(
+ value: T,
+ key: ValueKey,
+ lerp: (T, T, Float) -> T,
+ canOverflow: Boolean
+ ): AnimatedState<T> {
+ return animateSharedValueAsState(
+ layoutImpl = layoutImpl,
+ scene = scene.key,
+ element = null,
+ key = key,
+ value = value,
+ lerp = lerp,
+ canOverflow = canOverflow,
+ )
+ }
+
override fun Modifier.horizontalNestedScrollToScene(
leftBehavior: NestedScrollBehavior,
rightBehavior: NestedScrollBehavior,
@@ -109,45 +139,6 @@
bottomOrRightBehavior = bottomBehavior,
)
- @Composable
- override fun <T> animateSharedValueAsState(
- value: T,
- key: ValueKey,
- element: ElementKey?,
- lerp: (T, T, Float) -> T,
- canOverflow: Boolean
- ): State<T> {
- val element =
- element?.let { key ->
- Snapshot.withoutReadObservation {
- layoutImpl.elements[key]
- ?: error(
- "Element $key is not composed. Make sure to call " +
- "animateSharedXAsState *after* Modifier.element(key)."
- )
- }
- }
-
- return animateSharedValueAsState(
- layoutImpl,
- scene,
- element,
- key,
- value,
- lerp,
- canOverflow,
- )
- }
-
- @Composable
- override fun MovableElement(
- key: ElementKey,
- modifier: Modifier,
- content: @Composable MovableElementScope.() -> Unit,
- ) {
- MovableElement(layoutImpl, scene, key, modifier, content)
- }
-
override fun Modifier.punchHole(
element: ElementKey,
bounds: ElementKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 5eb339e..84fade89 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -22,9 +22,9 @@
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
@@ -98,9 +98,9 @@
*/
@DslMarker annotation class ElementDsl
-@ElementDsl
@Stable
-interface SceneScope {
+@ElementDsl
+interface BaseSceneScope {
/** The state of the [SceneTransitionLayout] in which this scene is contained. */
val layoutState: SceneTransitionLayoutState
@@ -111,21 +111,74 @@
* that the element can be transformed and animated when the scene transitions in or out.
*
* Additionally, this [key] will be used to detect elements that are shared between scenes to
- * automatically interpolate their size, offset and [shared values][animateSharedValueAsState].
+ * automatically interpolate their size and offset. If you need to animate shared element values
+ * (i.e. values associated to this element that change depending on which scene it is composed
+ * in), use [Element] instead.
*
* Note that shared elements tagged using this function will be duplicated in each scene they
* are part of, so any **internal** state (e.g. state created using `remember {
* mutableStateOf(...) }`) will be lost. If you need to preserve internal state, you should use
* [MovableElement] instead.
*
+ * @see Element
* @see MovableElement
- *
- * TODO(b/291566282): Migrate this to the new Modifier Node API and remove the @Composable
- * constraint.
*/
fun Modifier.element(key: ElementKey): Modifier
/**
+ * Create an element identified by [key].
+ *
+ * Similar to [element], this creates an element that will be automatically shared when present
+ * in multiple scenes and that can be transformed during transitions, the same way that
+ * [element] does.
+ *
+ * The only difference with [element] is that the provided [ElementScope] allows you to
+ * [animate element values][ElementScope.animateElementValueAsState] or specify its
+ * [movable content][Element.movableContent] that will be "moved" and composed only once during
+ * transitions (as opposed to [element] that duplicates shared elements) so that any internal
+ * state is preserved during and after the transition.
+ *
+ * @see element
+ * @see MovableElement
+ */
+ @Composable
+ fun Element(
+ key: ElementKey,
+ modifier: Modifier,
+
+ // TODO(b/317026105): As discussed in http://shortn/_gJVdltF8Si, remove the @Composable
+ // scope here to make sure that callers specify the content in ElementScope.content {} or
+ // ElementScope.movableContent {}.
+ content: @Composable ElementScope<ElementContentScope>.() -> Unit,
+ )
+
+ /**
+ * Create a *movable* element identified by [key].
+ *
+ * Similar to [Element], this creates an element that will be automatically shared when present
+ * in multiple scenes and that can be transformed during transitions, and you can also use the
+ * provided [ElementScope] to [animate element values][ElementScope.animateElementValueAsState].
+ *
+ * The important difference with [element] and [Element] is that this element
+ * [content][ElementScope.content] will be "moved" and composed only once during transitions, as
+ * opposed to [element] and [Element] that duplicates shared elements, so that any internal
+ * state is preserved during and after the transition.
+ *
+ * @see element
+ * @see Element
+ */
+ @Composable
+ fun MovableElement(
+ key: ElementKey,
+ modifier: Modifier,
+
+ // TODO(b/317026105): As discussed in http://shortn/_gJVdltF8Si, remove the @Composable
+ // scope here to make sure that callers specify the content in ElementScope.content {} or
+ // ElementScope.movableContent {}.
+ content: @Composable ElementScope<MovableElementContentScope>.() -> Unit,
+ )
+
+ /**
* Adds a [NestedScrollConnection] to intercept scroll events not handled by the scrollable
* component.
*
@@ -150,51 +203,6 @@
): Modifier
/**
- * Create a *movable* element identified by [key].
- *
- * This creates an element that will be automatically shared when present in multiple scenes and
- * that can be transformed during transitions, the same way that [element] does. The major
- * difference with [element] is that elements created with [MovableElement] will be "moved" and
- * composed only once during transitions (as opposed to [element] that duplicates shared
- * elements) so that any internal state is preserved during and after the transition.
- *
- * @see element
- */
- @Composable
- fun MovableElement(
- key: ElementKey,
- modifier: Modifier,
- content: @Composable MovableElementScope.() -> Unit,
- )
-
- /**
- * Animate some value of a shared element.
- *
- * @param value the value of this shared value in the current scene.
- * @param key the key of this shared value.
- * @param element the element associated with this value. If `null`, this value will be
- * associated at the scene level, which means that [key] should be used maximum once in the
- * same scene.
- * @param lerp the *linear* interpolation function that should be used to interpolate between
- * two different values. Note that it has to be linear because the [fraction] passed to this
- * interpolator is already interpolated.
- * @param canOverflow whether this value can overflow past the values it is interpolated
- * between, for instance because the transition is animated using a bouncy spring.
- * @see animateSharedIntAsState
- * @see animateSharedFloatAsState
- * @see animateSharedDpAsState
- * @see animateSharedColorAsState
- */
- @Composable
- fun <T> animateSharedValueAsState(
- value: T,
- key: ValueKey,
- element: ElementKey?,
- lerp: (start: T, stop: T, fraction: Float) -> T,
- canOverflow: Boolean,
- ): State<T>
-
- /**
* Punch a hole in this [element] using the bounds of [bounds] in [scene] and the given [shape].
*
* Punching a hole in an element will "remove" any pixel drawn by that element in the hole area.
@@ -213,19 +221,96 @@
fun Modifier.noResizeDuringTransitions(): Modifier
}
-// TODO(b/291053742): Add animateSharedValueAsState(targetValue) without any ValueKey and ElementKey
-// arguments to allow sharing values inside a movable element.
+@Stable
@ElementDsl
-interface MovableElementScope {
+interface SceneScope : BaseSceneScope {
+ /**
+ * Animate some value at the scene level.
+ *
+ * @param value the value of this shared value in the current scene.
+ * @param key the key of this shared value.
+ * @param lerp the *linear* interpolation function that should be used to interpolate between
+ * two different values. Note that it has to be linear because the [fraction] passed to this
+ * interpolator is already interpolated.
+ * @param canOverflow whether this value can overflow past the values it is interpolated
+ * between, for instance because the transition is animated using a bouncy spring.
+ * @see animateSceneIntAsState
+ * @see animateSceneFloatAsState
+ * @see animateSceneDpAsState
+ * @see animateSceneColorAsState
+ */
@Composable
- fun <T> animateSharedValueAsState(
+ fun <T> animateSceneValueAsState(
value: T,
- debugName: String,
+ key: ValueKey,
lerp: (start: T, stop: T, fraction: Float) -> T,
canOverflow: Boolean,
- ): State<T>
+ ): AnimatedState<T>
}
+@Stable
+@ElementDsl
+interface ElementScope<ContentScope> {
+ /**
+ * Animate some value associated to this element.
+ *
+ * @param value the value of this shared value in the current scene.
+ * @param key the key of this shared value.
+ * @param lerp the *linear* interpolation function that should be used to interpolate between
+ * two different values. Note that it has to be linear because the [fraction] passed to this
+ * interpolator is already interpolated.
+ * @param canOverflow whether this value can overflow past the values it is interpolated
+ * between, for instance because the transition is animated using a bouncy spring.
+ * @see animateElementIntAsState
+ * @see animateElementFloatAsState
+ * @see animateElementDpAsState
+ * @see animateElementColorAsState
+ */
+ @Composable
+ fun <T> animateElementValueAsState(
+ value: T,
+ key: ValueKey,
+ lerp: (start: T, stop: T, fraction: Float) -> T,
+ canOverflow: Boolean,
+ ): AnimatedState<T>
+
+ /**
+ * The content of this element.
+ *
+ * Important: This must be called exactly once, after all calls to [animateElementValueAsState].
+ */
+ @Composable fun content(content: @Composable ContentScope.() -> Unit)
+}
+
+/**
+ * The exact same scope as [androidx.compose.foundation.layout.BoxScope].
+ *
+ * We can't reuse BoxScope directly because of the @LayoutScopeMarker annotation on it, which would
+ * prevent us from calling Modifier.element() and other methods of [SceneScope] inside any Box {} in
+ * the [content][ElementScope.content] of a [SceneScope.Element] or a [SceneScope.MovableElement].
+ */
+@Stable
+@ElementDsl
+interface ElementBoxScope {
+ /** @see [androidx.compose.foundation.layout.BoxScope.align]. */
+ @Stable fun Modifier.align(alignment: Alignment): Modifier
+
+ /** @see [androidx.compose.foundation.layout.BoxScope.matchParentSize]. */
+ @Stable fun Modifier.matchParentSize(): Modifier
+}
+
+/** The scope for "normal" (not movable) elements. */
+@Stable @ElementDsl interface ElementContentScope : SceneScope, ElementBoxScope
+
+/**
+ * The scope for the content of movable elements.
+ *
+ * Note that it extends [BaseSceneScope] and not [SceneScope] because movable elements should not
+ * call [SceneScope.animateSceneValueAsState], given that their content is not composed in all
+ * scenes.
+ */
+@Stable @ElementDsl interface MovableElementContentScope : BaseSceneScope, ElementBoxScope
+
/** An action performed by the user. */
sealed interface UserAction
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 45e1a0f..0227aba 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -36,6 +36,16 @@
import com.android.compose.ui.util.lerp
import kotlinx.coroutines.CoroutineScope
+/**
+ * The type for the content of movable elements.
+ *
+ * TODO(b/317972419): Revert back to make this movable content have a single @Composable lambda
+ * parameter.
+ */
+internal typealias MovableElementContent =
+ @Composable
+ (MovableElementContentScope, @Composable MovableElementContentScope.() -> Unit) -> Unit
+
@Stable
internal class SceneTransitionLayoutImpl(
internal val state: SceneTransitionLayoutStateImpl,
@@ -56,16 +66,47 @@
/**
* The map of [Element]s.
*
- * Note that this map is *mutated* directly during composition, so it is a [SnapshotStateMap] to
- * make sure that mutations are reverted if composition is cancelled.
+ * Important: [Element]s from this map should never be accessed during composition because the
+ * Elements are added when the associated Modifier.element() node is attached to the Modifier
+ * tree, i.e. after composition.
*/
- internal val elements = SnapshotStateMap<ElementKey, Element>()
+ internal val elements = mutableMapOf<ElementKey, Element>()
+
+ /**
+ * The map of contents of movable elements.
+ *
+ * Note that given that this map is mutated directly during a composition, it has to be a
+ * [SnapshotStateMap] to make sure that mutations are reverted if composition is cancelled.
+ */
+ private var _movableContents: SnapshotStateMap<ElementKey, MovableElementContent>? = null
+ val movableContents: SnapshotStateMap<ElementKey, MovableElementContent>
+ get() =
+ _movableContents
+ ?: SnapshotStateMap<ElementKey, MovableElementContent>().also {
+ _movableContents = it
+ }
+
+ /**
+ * The different values of a shared value keyed by a a [ValueKey] and the different elements and
+ * scenes it is associated to.
+ */
+ private var _sharedValues:
+ MutableMap<ValueKey, MutableMap<ElementKey?, SnapshotStateMap<SceneKey, *>>>? =
+ null
+ internal val sharedValues:
+ MutableMap<ValueKey, MutableMap<ElementKey?, SnapshotStateMap<SceneKey, *>>>
+ get() =
+ _sharedValues
+ ?: mutableMapOf<ValueKey, MutableMap<ElementKey?, SnapshotStateMap<SceneKey, *>>>()
+ .also { _sharedValues = it }
/**
* The scenes that are "ready", i.e. they were composed and fully laid-out at least once.
*
* Note that this map is *read* during composition, so it is a [SnapshotStateMap] to make sure
* that we recompose when modifications are made to this map.
+ *
+ * TODO(b/316901148): Remove this map.
*/
private val readyScenes = SnapshotStateMap<SceneKey, Boolean>()
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index d1ba582..0607aa1 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -92,6 +92,20 @@
/** Whether user input is currently driving the transition. */
abstract val isUserInputOngoing: Boolean
+
+ /**
+ * Whether we are transitioning. If [from] or [to] is empty, we will also check that they
+ * match the scenes we are animating from and/or to.
+ */
+ fun isTransitioning(from: SceneKey? = null, to: SceneKey? = null): Boolean {
+ return (from == null || fromScene == from) && (to == null || toScene == to)
+ }
+
+ /** Whether we are transitioning from [scene] to [other], or from [other] to [scene]. */
+ fun isTransitioningBetween(scene: SceneKey, other: SceneKey): Boolean {
+ return isTransitioning(from = scene, to = other) ||
+ isTransitioning(from = other, to = scene)
+ }
}
}
@@ -111,13 +125,12 @@
override fun isTransitioning(from: SceneKey?, to: SceneKey?): Boolean {
val transition = currentTransition ?: return false
- return (from == null || transition.fromScene == from) &&
- (to == null || transition.toScene == to)
+ return transition.isTransitioning(from, to)
}
override fun isTransitioningBetween(scene: SceneKey, other: SceneKey): Boolean {
- return isTransitioning(from = scene, to = other) ||
- isTransitioning(from = other, to = scene)
+ val transition = currentTransition ?: return false
+ return transition.isTransitioningBetween(scene, other)
}
/** Start a new [transition], instantly interrupting any ongoing transition if there was one. */
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index dfa2a9a..dc8505c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -119,14 +119,8 @@
*
* @param enabled whether the matched element(s) should actually be shared in this transition.
* Defaults to true.
- * @param scenePicker the [SharedElementScenePicker] to use when deciding in which scene we
- * should draw or compose this shared element.
*/
- fun sharedElement(
- matcher: ElementMatcher,
- enabled: Boolean = true,
- scenePicker: SharedElementScenePicker = DefaultSharedElementScenePicker,
- )
+ fun sharedElement(matcher: ElementMatcher, enabled: Boolean = true)
/**
* Adds the transformations in [builder] but in reversed order. This allows you to partially
@@ -136,37 +130,65 @@
fun reversed(builder: TransitionBuilder.() -> Unit)
}
-interface SharedElementScenePicker {
+/**
+ * An interface to decide where we should draw shared Elements or compose MovableElements.
+ *
+ * @see DefaultElementScenePicker
+ * @see HighestZIndexScenePicker
+ * @see LowestZIndexScenePicker
+ * @see MovableElementScenePicker
+ */
+interface ElementScenePicker {
/**
* Return the scene in which [element] should be drawn (when using `Modifier.element(key)`) or
- * composed (when using `MovableElement(key)`) during the transition from [fromScene] to
- * [toScene].
+ * composed (when using `MovableElement(key)`) during the given [transition].
+ *
+ * Important: For [MovableElements][SceneScope.MovableElement], this scene picker will *always*
+ * be used during transitions to decide whether we should compose that element in a given scene
+ * or not. Therefore, you should make sure that the returned [SceneKey] contains the movable
+ * element, otherwise that element will not be composed in any scene during the transition.
*/
fun sceneDuringTransition(
element: ElementKey,
- fromScene: SceneKey,
- toScene: SceneKey,
- progress: () -> Float,
+ transition: TransitionState.Transition,
fromSceneZIndex: Float,
toSceneZIndex: Float,
): SceneKey
-}
-object DefaultSharedElementScenePicker : SharedElementScenePicker {
- override fun sceneDuringTransition(
+ /**
+ * Return [transition.fromScene] if it is in [scenes] and [transition.toScene] is not, or return
+ * [transition.toScene] if it is in [scenes] and [transition.fromScene] is not, otherwise throw
+ * an exception (i.e. if neither or both of fromScene and toScene are in [scenes]).
+ *
+ * This function can be useful when computing the scene in which a movable element should be
+ * composed.
+ */
+ fun pickSingleSceneIn(
+ scenes: Set<SceneKey>,
+ transition: TransitionState.Transition,
element: ElementKey,
- fromScene: SceneKey,
- toScene: SceneKey,
- progress: () -> Float,
- fromSceneZIndex: Float,
- toSceneZIndex: Float
): SceneKey {
- // By default shared elements are drawn in the highest scene possible, unless it is a
- // background.
- return if (
- (fromSceneZIndex > toSceneZIndex && !element.isBackground) ||
- (fromSceneZIndex < toSceneZIndex && element.isBackground)
- ) {
+ val fromScene = transition.fromScene
+ val toScene = transition.toScene
+ val fromSceneInScenes = scenes.contains(fromScene)
+ val toSceneInScenes = scenes.contains(toScene)
+ if (fromSceneInScenes && toSceneInScenes) {
+ error(
+ "Element $element can be in both $fromScene and $toScene. You should add a " +
+ "special case for this transition before calling pickSingleSceneIn()."
+ )
+ }
+
+ if (!fromSceneInScenes && !toSceneInScenes) {
+ error(
+ "Element $element can be neither in $fromScene and $toScene. This either means " +
+ "that you should add one of them in the scenes set passed to " +
+ "pickSingleSceneIn(), or there is an internal error and this element was " +
+ "composed when it shouldn't be."
+ )
+ }
+
+ return if (fromSceneInScenes) {
fromScene
} else {
toScene
@@ -174,6 +196,66 @@
}
}
+/** An [ElementScenePicker] that draws/composes elements in the scene with the highest z-order. */
+object HighestZIndexScenePicker : ElementScenePicker {
+ override fun sceneDuringTransition(
+ element: ElementKey,
+ transition: TransitionState.Transition,
+ fromSceneZIndex: Float,
+ toSceneZIndex: Float
+ ): SceneKey {
+ return if (fromSceneZIndex > toSceneZIndex) {
+ transition.fromScene
+ } else {
+ transition.toScene
+ }
+ }
+}
+
+/** An [ElementScenePicker] that draws/composes elements in the scene with the lowest z-order. */
+object LowestZIndexScenePicker : ElementScenePicker {
+ override fun sceneDuringTransition(
+ element: ElementKey,
+ transition: TransitionState.Transition,
+ fromSceneZIndex: Float,
+ toSceneZIndex: Float
+ ): SceneKey {
+ return if (fromSceneZIndex < toSceneZIndex) {
+ transition.fromScene
+ } else {
+ transition.toScene
+ }
+ }
+}
+
+/**
+ * An [ElementScenePicker] that draws/composes elements in the scene we are transitioning to, iff
+ * that scene is in [scenes].
+ *
+ * This picker can be useful for movable elements whose content size depends on its content (because
+ * it wraps it) in at least one scene. That way, the target size of the MovableElement will be
+ * computed in the scene we are going to and, given that this element was probably already composed
+ * in the scene we are going from before starting the transition, the interpolated size of the
+ * movable element during the transition should be correct.
+ *
+ * The downside of this picker is that the zIndex of the element when going from scene A to scene B
+ * is not the same as when going from scene B to scene A, so it's not usable in situations where
+ * z-ordering during the transition matters.
+ */
+class MovableElementScenePicker(private val scenes: Set<SceneKey>) : ElementScenePicker {
+ override fun sceneDuringTransition(
+ element: ElementKey,
+ transition: TransitionState.Transition,
+ fromSceneZIndex: Float,
+ toSceneZIndex: Float,
+ ): SceneKey {
+ return if (scenes.contains(transition.toScene)) transition.toScene else transition.fromScene
+ }
+}
+
+/** The default [ElementScenePicker]. */
+val DefaultElementScenePicker = HighestZIndexScenePicker
+
@TransitionDsl
interface PropertyTransformationBuilder {
/**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index 7046866..b96f9be 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -108,12 +108,8 @@
range = null
}
- override fun sharedElement(
- matcher: ElementMatcher,
- enabled: Boolean,
- scenePicker: SharedElementScenePicker,
- ) {
- transformations.add(SharedElementTransformation(matcher, enabled, scenePicker))
+ override fun sharedElement(matcher: ElementMatcher, enabled: Boolean) {
+ transformations.add(SharedElementTransformation(matcher, enabled))
}
override fun timestampRange(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
index 40c814e..124ec29 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -36,12 +36,12 @@
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
element: Element,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
transition: TransitionState.Transition,
value: IntSize,
): IntSize {
fun anchorSizeIn(scene: SceneKey): IntSize {
- val size = layoutImpl.elements[anchor]?.sceneValues?.get(scene)?.targetSize
+ val size = layoutImpl.elements[anchor]?.sceneStates?.get(scene)?.targetSize
return if (size != null && size != Element.SizeUnspecified) {
IntSize(
width = if (anchorWidth) size.width else value.width,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
index a1d6319..7aa702b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -35,13 +35,13 @@
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
element: Element,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
transition: TransitionState.Transition,
value: Offset,
): Offset {
val anchor = layoutImpl.elements[anchor] ?: return value
fun anchorOffsetIn(scene: SceneKey): Offset? {
- return anchor.sceneValues[scene]?.targetOffset?.takeIf { it.isSpecified }
+ return anchor.sceneStates[scene]?.targetOffset?.takeIf { it.isSpecified }
}
// [element] will move the same amount as [anchor] does.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
index d1cf8ee..6704a3b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
@@ -39,7 +39,7 @@
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
element: Element,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
transition: TransitionState.Transition,
value: Scale,
): Scale {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index 70534dd..191a8fb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -34,12 +34,12 @@
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
element: Element,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
transition: TransitionState.Transition,
value: Offset
): Offset {
val sceneSize = scene.targetSize
- val elementSize = sceneValues.targetSize
+ val elementSize = sceneState.targetSize
if (elementSize == Element.SizeUnspecified) {
return value
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
index 17032dc..41f626e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -30,7 +30,7 @@
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
element: Element,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
transition: TransitionState.Transition,
value: Float
): Float {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
index 233ae59..f5207dc 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -37,7 +37,7 @@
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
element: Element,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
transition: TransitionState.Transition,
value: IntSize,
): IntSize {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index 0cd11b9..04254fb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -20,7 +20,6 @@
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.Scene
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
-import com.android.compose.animation.scene.SharedElementScenePicker
import com.android.compose.animation.scene.TransitionState
/** A transformation applied to one or more elements during a transition. */
@@ -48,7 +47,6 @@
internal class SharedElementTransformation(
override val matcher: ElementMatcher,
internal val enabled: Boolean,
- internal val scenePicker: SharedElementScenePicker,
) : Transformation
/** A transformation that changes the value of an element property, like its size or offset. */
@@ -62,7 +60,7 @@
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
element: Element,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
transition: TransitionState.Transition,
value: T,
): T
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
index 864b937..04d5914 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -35,7 +35,7 @@
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
element: Element,
- sceneValues: Element.TargetValues,
+ sceneState: Element.SceneState,
transition: TransitionState.Transition,
value: Offset,
): Offset {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
index 5473186..a116501 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
@@ -18,10 +18,11 @@
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
-import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
@@ -32,6 +33,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.ui.util.lerp
import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -62,17 +64,17 @@
onCurrentValueChanged: (Values) -> Unit,
) {
val key = TestElements.Foo
- Box(Modifier.element(key)) {
- val int by animateSharedIntAsState(targetValues.int, TestValues.Value1, key)
- val float by animateSharedFloatAsState(targetValues.float, TestValues.Value2, key)
- val dp by animateSharedDpAsState(targetValues.dp, TestValues.Value3, key)
- val color by
- animateSharedColorAsState(targetValues.color, TestValues.Value4, element = null)
+ Element(key, Modifier) {
+ val int by animateElementIntAsState(targetValues.int, key = TestValues.Value1)
+ val float by animateElementFloatAsState(targetValues.float, key = TestValues.Value2)
+ val dp by animateElementDpAsState(targetValues.dp, key = TestValues.Value3)
+ val color by animateElementColorAsState(targetValues.color, key = TestValues.Value4)
- // Make sure we read the values during composition, so that we recompose and call
- // onCurrentValueChanged() with the latest values.
- val currentValues = Values(int, float, dp, color)
- SideEffect { onCurrentValueChanged(currentValues) }
+ content {
+ LaunchedEffect(Unit) {
+ snapshotFlow { Values(int, float, dp, color) }.collect(onCurrentValueChanged)
+ }
+ }
}
}
@@ -83,30 +85,34 @@
) {
val key = TestElements.Foo
MovableElement(key = key, Modifier) {
- val int by
- animateSharedIntAsState(targetValues.int, debugName = TestValues.Value1.debugName)
- val float by
- animateSharedFloatAsState(
- targetValues.float,
- debugName = TestValues.Value2.debugName
- )
- val dp by
- animateSharedDpAsState(targetValues.dp, debugName = TestValues.Value3.debugName)
- val color by
- animateSharedColorAsState(
- targetValues.color,
- debugName = TestValues.Value4.debugName
- )
+ val int by animateElementIntAsState(targetValues.int, key = TestValues.Value1)
+ val float by animateElementFloatAsState(targetValues.float, key = TestValues.Value2)
+ val dp by animateElementDpAsState(targetValues.dp, key = TestValues.Value3)
+ val color by animateElementColorAsState(targetValues.color, key = TestValues.Value4)
- // Make sure we read the values during composition, so that we recompose and call
- // onCurrentValueChanged() with the latest values.
- val currentValues = Values(int, float, dp, color)
- SideEffect { onCurrentValueChanged(currentValues) }
+ LaunchedEffect(Unit) {
+ snapshotFlow { Values(int, float, dp, color) }.collect(onCurrentValueChanged)
+ }
+ }
+ }
+
+ @Composable
+ private fun SceneScope.SceneValues(
+ targetValues: Values,
+ onCurrentValueChanged: (Values) -> Unit,
+ ) {
+ val int by animateSceneIntAsState(targetValues.int, key = TestValues.Value1)
+ val float by animateSceneFloatAsState(targetValues.float, key = TestValues.Value2)
+ val dp by animateSceneDpAsState(targetValues.dp, key = TestValues.Value3)
+ val color by animateSceneColorAsState(targetValues.color, key = TestValues.Value4)
+
+ LaunchedEffect(Unit) {
+ snapshotFlow { Values(int, float, dp, color) }.collect(onCurrentValueChanged)
}
}
@Test
- fun animateSharedValues() {
+ fun animateElementValues() {
val fromValues = Values(int = 0, float = 0f, dp = 0.dp, color = Color.Red)
val toValues = Values(int = 100, float = 100f, dp = 100.dp, color = Color.Blue)
@@ -194,24 +200,183 @@
}
at(16) {
- // Given that we use MovableElement here, animateSharedXAsState is composed only
- // once, in the highest scene (in this case, in toScene).
- assertThat(lastValueInFrom).isEqualTo(fromValues)
+ assertThat(lastValueInFrom).isEqualTo(lerp(fromValues, toValues, fraction = 0.25f))
assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.25f))
}
at(32) {
- assertThat(lastValueInFrom).isEqualTo(fromValues)
+ assertThat(lastValueInFrom).isEqualTo(lerp(fromValues, toValues, fraction = 0.5f))
assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.5f))
}
at(48) {
- assertThat(lastValueInFrom).isEqualTo(fromValues)
+ assertThat(lastValueInFrom).isEqualTo(lerp(fromValues, toValues, fraction = 0.75f))
assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.75f))
}
after {
+ assertThat(lastValueInFrom).isEqualTo(toValues)
+ assertThat(lastValueInTo).isEqualTo(toValues)
+ }
+ }
+ }
+
+ @Test
+ fun animateSceneValues() {
+ val fromValues = Values(int = 0, float = 0f, dp = 0.dp, color = Color.Red)
+ val toValues = Values(int = 100, float = 100f, dp = 100.dp, color = Color.Blue)
+
+ var lastValueInFrom = fromValues
+ var lastValueInTo = toValues
+
+ rule.testTransition(
+ fromSceneContent = {
+ SceneValues(
+ targetValues = fromValues,
+ onCurrentValueChanged = { lastValueInFrom = it }
+ )
+ },
+ toSceneContent = {
+ SceneValues(targetValues = toValues, onCurrentValueChanged = { lastValueInTo = it })
+ },
+ transition = {
+ // The transition lasts 64ms = 4 frames.
+ spec = tween(durationMillis = 16 * 4, easing = LinearEasing)
+ },
+ fromScene = TestScenes.SceneA,
+ toScene = TestScenes.SceneB,
+ ) {
+ before {
assertThat(lastValueInFrom).isEqualTo(fromValues)
+
+ // to was not composed yet, so lastValueInTo was not set yet.
+ assertThat(lastValueInTo).isEqualTo(toValues)
+ }
+
+ at(16) {
+ // Given that we use scene values here, animateSceneXAsState is composed in both
+ // scenes and values should be interpolated with the transition fraction.
+ val expectedValues = lerp(fromValues, toValues, fraction = 0.25f)
+ assertThat(lastValueInFrom).isEqualTo(expectedValues)
+ assertThat(lastValueInTo).isEqualTo(expectedValues)
+ }
+
+ at(32) {
+ val expectedValues = lerp(fromValues, toValues, fraction = 0.5f)
+ assertThat(lastValueInFrom).isEqualTo(expectedValues)
+ assertThat(lastValueInTo).isEqualTo(expectedValues)
+ }
+
+ at(48) {
+ val expectedValues = lerp(fromValues, toValues, fraction = 0.75f)
+ assertThat(lastValueInFrom).isEqualTo(expectedValues)
+ assertThat(lastValueInTo).isEqualTo(expectedValues)
+ }
+
+ after {
+ assertThat(lastValueInFrom).isEqualTo(toValues)
+ assertThat(lastValueInTo).isEqualTo(toValues)
+ }
+ }
+ }
+
+ @Test
+ fun readingAnimatedStateValueDuringCompositionThrows() {
+ assertThrows(IllegalStateException::class.java) {
+ rule.testTransition(
+ fromSceneContent = { animateSceneIntAsState(0, TestValues.Value1).value },
+ toSceneContent = {},
+ transition = {},
+ ) {}
+ }
+ }
+
+ @Test
+ fun readingAnimatedStateValueDuringCompositionIsStillPossible() {
+ @Composable
+ fun SceneScope.SceneValuesDuringComposition(
+ targetValues: Values,
+ onCurrentValueChanged: (Values) -> Unit,
+ ) {
+ val int by
+ animateSceneIntAsState(targetValues.int, key = TestValues.Value1)
+ .unsafeCompositionState(targetValues.int)
+ val float by
+ animateSceneFloatAsState(targetValues.float, key = TestValues.Value2)
+ .unsafeCompositionState(targetValues.float)
+ val dp by
+ animateSceneDpAsState(targetValues.dp, key = TestValues.Value3)
+ .unsafeCompositionState(targetValues.dp)
+ val color by
+ animateSceneColorAsState(targetValues.color, key = TestValues.Value4)
+ .unsafeCompositionState(targetValues.color)
+
+ val values = Values(int, float, dp, color)
+ SideEffect { onCurrentValueChanged(values) }
+ }
+
+ val fromValues = Values(int = 0, float = 0f, dp = 0.dp, color = Color.Red)
+ val toValues = Values(int = 100, float = 100f, dp = 100.dp, color = Color.Blue)
+
+ var lastValueInFrom = fromValues
+ var lastValueInTo = toValues
+
+ rule.testTransition(
+ fromSceneContent = {
+ SceneValuesDuringComposition(
+ targetValues = fromValues,
+ onCurrentValueChanged = { lastValueInFrom = it },
+ )
+ },
+ toSceneContent = {
+ SceneValuesDuringComposition(
+ targetValues = toValues,
+ onCurrentValueChanged = { lastValueInTo = it },
+ )
+ },
+ transition = {
+ // The transition lasts 64ms = 4 frames.
+ spec = tween(durationMillis = 16 * 4, easing = LinearEasing)
+ },
+ ) {
+ before {
+ assertThat(lastValueInFrom).isEqualTo(fromValues)
+
+ // to was not composed yet, so lastValueInTo was not set yet.
+ assertThat(lastValueInTo).isEqualTo(toValues)
+ }
+
+ at(16) {
+ // Because we are using unsafeCompositionState(), values are one frame behind their
+ // expected progress so at this first frame we are at progress = 0% instead of 25%.
+ val expectedValues = lerp(fromValues, toValues, fraction = 0f)
+ assertThat(lastValueInFrom).isEqualTo(expectedValues)
+ assertThat(lastValueInTo).isEqualTo(expectedValues)
+ }
+
+ at(32) {
+ // One frame behind, so 25% instead of 50%.
+ val expectedValues = lerp(fromValues, toValues, fraction = 0.25f)
+ assertThat(lastValueInFrom).isEqualTo(expectedValues)
+ assertThat(lastValueInTo).isEqualTo(expectedValues)
+ }
+
+ at(48) {
+ // One frame behind, so 50% instead of 75%.
+ val expectedValues = lerp(fromValues, toValues, fraction = 0.5f)
+ assertThat(lastValueInFrom).isEqualTo(expectedValues)
+ assertThat(lastValueInTo).isEqualTo(expectedValues)
+ }
+
+ after {
+ // from should have been last composed at progress = 100% before it is removed from
+ // composition, but given that we are one frame behind the last values are stuck at
+ // 75%.
+ assertThat(lastValueInFrom).isEqualTo(lerp(fromValues, toValues, fraction = 0.75f))
+
+ // The after {} block resumes the clock and will run as many frames as necessary so
+ // that the application is idle, so the toScene settle to the idle state and to the
+ // final values.
assertThat(lastValueInTo).isEqualTo(toValues)
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementScenePickerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementScenePickerTest.kt
new file mode 100644
index 0000000..3b022e8
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementScenePickerTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ElementScenePickerTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun highestZIndexPicker() {
+ val key = ElementKey("TestElement", scenePicker = HighestZIndexScenePicker)
+ rule.testTransition(
+ fromSceneContent = { Box(Modifier.element(key).size(10.dp)) },
+ toSceneContent = { Box(Modifier.element(key).size(10.dp)) },
+ transition = { spec = tween(4 * 16, easing = LinearEasing) },
+ fromScene = TestScenes.SceneA,
+ toScene = TestScenes.SceneB,
+ ) {
+ before {
+ onElement(key, TestScenes.SceneA).assertIsDisplayed()
+ onElement(key, TestScenes.SceneB).assertDoesNotExist()
+ }
+ at(32) {
+ // Scene B has the highest index, so the element is placed only there.
+ onElement(key, TestScenes.SceneA).assertExists().assertIsNotDisplayed()
+ onElement(key, TestScenes.SceneB).assertIsDisplayed()
+ }
+ after {
+ onElement(key, TestScenes.SceneA).assertDoesNotExist()
+ onElement(key, TestScenes.SceneB).assertIsDisplayed()
+ }
+ }
+ }
+
+ @Test
+ fun lowestZIndexPicker() {
+ val key = ElementKey("TestElement", scenePicker = LowestZIndexScenePicker)
+ rule.testTransition(
+ fromSceneContent = { Box(Modifier.element(key).size(10.dp)) },
+ toSceneContent = { Box(Modifier.element(key).size(10.dp)) },
+ transition = { spec = tween(4 * 16, easing = LinearEasing) },
+ fromScene = TestScenes.SceneA,
+ toScene = TestScenes.SceneB,
+ ) {
+ before {
+ onElement(key, TestScenes.SceneA).assertIsDisplayed()
+ onElement(key, TestScenes.SceneB).assertDoesNotExist()
+ }
+ at(32) {
+ // Scene A has the lowest index, so the element is placed only there.
+ onElement(key, TestScenes.SceneA).assertIsDisplayed()
+ onElement(key, TestScenes.SceneB).assertExists().assertIsNotDisplayed()
+ }
+ after {
+ onElement(key, TestScenes.SceneA).assertDoesNotExist()
+ onElement(key, TestScenes.SceneB).assertIsDisplayed()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index da5a0a0..54c5de7 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -306,7 +306,7 @@
assertThat(layoutImpl.elements.keys).containsExactly(key)
val element = layoutImpl.elements.getValue(key)
- assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneB)
+ assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneB)
// Scene C, state 0: the same element is reused.
currentScene = TestScenes.SceneC
@@ -315,7 +315,7 @@
assertThat(layoutImpl.elements.keys).containsExactly(key)
assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
- assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneC)
+ assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneC)
// Scene C, state 1: the same element is reused.
sceneCState = 1
@@ -323,7 +323,7 @@
assertThat(layoutImpl.elements.keys).containsExactly(key)
assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
- assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneC)
+ assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneC)
// Scene D, state 0: the same element is reused.
currentScene = TestScenes.SceneD
@@ -332,7 +332,7 @@
assertThat(layoutImpl.elements.keys).containsExactly(key)
assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
- assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneD)
+ assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneD)
// Scene D, state 1: the same element is reused.
sceneDState = 1
@@ -340,13 +340,13 @@
assertThat(layoutImpl.elements.keys).containsExactly(key)
assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
- assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneD)
+ assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneD)
// Scene D, state 2: the element is removed from the map.
sceneDState = 2
rule.waitForIdle()
- assertThat(element.sceneValues).isEmpty()
+ assertThat(element.sceneStates).isEmpty()
assertThat(layoutImpl.elements).isEmpty()
}
@@ -442,7 +442,7 @@
// There is only Foo in the elements map.
assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
val fooElement = layoutImpl.elements.getValue(TestElements.Foo)
- assertThat(fooElement.sceneValues.keys).containsExactly(TestScenes.SceneA)
+ assertThat(fooElement.sceneStates.keys).containsExactly(TestScenes.SceneA)
key = TestElements.Bar
@@ -450,8 +450,8 @@
rule.waitForIdle()
assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Bar)
val barElement = layoutImpl.elements.getValue(TestElements.Bar)
- assertThat(barElement.sceneValues.keys).containsExactly(TestScenes.SceneA)
- assertThat(fooElement.sceneValues).isEmpty()
+ assertThat(barElement.sceneStates.keys).containsExactly(TestScenes.SceneA)
+ assertThat(fooElement.sceneStates).isEmpty()
}
@Test
@@ -505,7 +505,7 @@
// There is only Foo in the elements map.
assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
val element = layoutImpl.elements.getValue(TestElements.Foo)
- val sceneValues = element.sceneValues
+ val sceneValues = element.sceneStates
assertThat(sceneValues.keys).containsExactly(TestScenes.SceneA)
// Get the ElementModifier node that should be reused later on when coming back to this
@@ -528,7 +528,7 @@
assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
val newElement = layoutImpl.elements.getValue(TestElements.Foo)
- val newSceneValues = newElement.sceneValues
+ val newSceneValues = newElement.sceneStates
assertThat(newElement).isNotEqualTo(element)
assertThat(newSceneValues).isNotEqualTo(sceneValues)
assertThat(newSceneValues.keys).containsExactly(TestScenes.SceneA)
@@ -579,11 +579,11 @@
fun foo() = layoutImpl().elements[TestElements.Foo] ?: error("Foo not in elements map")
- fun Element.lastSharedOffset() = lastSharedValues.offset.toDpOffset()
+ fun Element.lastSharedOffset() = lastSharedState.offset.toDpOffset()
fun Element.lastOffsetIn(scene: SceneKey) =
- (sceneValues[scene] ?: error("$scene not in sceneValues map"))
- .lastValues
+ (sceneStates[scene] ?: error("$scene not in sceneValues map"))
+ .lastState
.offset
.toDpOffset()
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementScenePickerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementScenePickerTest.kt
new file mode 100644
index 0000000..fb46a34
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementScenePickerTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class MovableElementScenePickerTest {
+ @Test
+ fun toSceneInScenes() {
+ val picker = MovableElementScenePicker(scenes = setOf(TestScenes.SceneA, TestScenes.SceneB))
+ assertThat(
+ picker.sceneDuringTransition(
+ TestElements.Foo,
+ transition(from = TestScenes.SceneA, to = TestScenes.SceneB),
+ fromSceneZIndex = 0f,
+ toSceneZIndex = 1f,
+ )
+ )
+ .isEqualTo(TestScenes.SceneB)
+ }
+
+ @Test
+ fun toSceneNotInScenes() {
+ val picker = MovableElementScenePicker(scenes = emptySet())
+ assertThat(
+ picker.sceneDuringTransition(
+ TestElements.Foo,
+ transition(from = TestScenes.SceneA, to = TestScenes.SceneB),
+ fromSceneZIndex = 0f,
+ toSceneZIndex = 1f,
+ )
+ )
+ .isEqualTo(TestScenes.SceneA)
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
index 3cd65cd..35cb691 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
@@ -28,19 +28,24 @@
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.hasParent
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.test.assertSizeIsEqualTo
import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -58,7 +63,7 @@
@Composable
private fun SceneScope.MovableCounter(key: ElementKey, modifier: Modifier) {
- MovableElement(key, modifier) { Counter() }
+ MovableElement(key, modifier) { content { Counter() } }
}
@Test
@@ -142,39 +147,37 @@
@Test
fun movableElementIsMovedAndComposedOnlyOnce() {
- rule.testTransition(
- fromSceneContent = { MovableCounter(TestElements.Foo, Modifier.size(50.dp)) },
- toSceneContent = { MovableCounter(TestElements.Foo, Modifier.size(100.dp)) },
- transition = {
- spec = tween(durationMillis = 16 * 4, easing = LinearEasing)
- sharedElement(
- TestElements.Foo,
- scenePicker =
- object : SharedElementScenePicker {
- override fun sceneDuringTransition(
- element: ElementKey,
- fromScene: SceneKey,
- toScene: SceneKey,
- progress: () -> Float,
- fromSceneZIndex: Float,
- toSceneZIndex: Float
- ): SceneKey {
- assertThat(fromScene).isEqualTo(TestScenes.SceneA)
- assertThat(toScene).isEqualTo(TestScenes.SceneB)
- assertThat(fromSceneZIndex).isEqualTo(0)
- assertThat(toSceneZIndex).isEqualTo(1)
+ val key =
+ ElementKey(
+ "Foo",
+ scenePicker =
+ object : ElementScenePicker {
+ override fun sceneDuringTransition(
+ element: ElementKey,
+ transition: TransitionState.Transition,
+ fromSceneZIndex: Float,
+ toSceneZIndex: Float
+ ): SceneKey {
+ assertThat(transition.fromScene).isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(fromSceneZIndex).isEqualTo(0)
+ assertThat(toSceneZIndex).isEqualTo(1)
- // Compose Foo in Scene A if progress < 0.65f, otherwise compose it
- // in Scene B.
- return if (progress() < 0.65f) {
- TestScenes.SceneA
- } else {
- TestScenes.SceneB
- }
+ // Compose Foo in Scene A if progress < 0.65f, otherwise compose it
+ // in Scene B.
+ return if (transition.progress < 0.65f) {
+ TestScenes.SceneA
+ } else {
+ TestScenes.SceneB
}
}
- )
- },
+ }
+ )
+
+ rule.testTransition(
+ fromSceneContent = { MovableCounter(key, Modifier.size(50.dp)) },
+ toSceneContent = { MovableCounter(key, Modifier.size(100.dp)) },
+ transition = { spec = tween(durationMillis = 16 * 4, easing = LinearEasing) },
fromScene = TestScenes.SceneA,
toScene = TestScenes.SceneB,
) {
@@ -257,4 +260,73 @@
}
}
}
+
+ @Test
+ @Ignore("b/317972419#comment2")
+ fun movableElementContentIsRecomposedIfContentParametersChange() {
+ @Composable
+ fun SceneScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
+ MovableElement(TestElements.Foo, modifier) { content { Text(text) } }
+ }
+
+ rule.testTransition(
+ fromSceneContent = { MovableFoo(text = "fromScene") },
+ toSceneContent = { MovableFoo(text = "toScene") },
+ transition = { spec = tween(durationMillis = 16 * 4, easing = LinearEasing) },
+ fromScene = TestScenes.SceneA,
+ toScene = TestScenes.SceneB,
+ ) {
+ // Before the transition, only fromScene is composed.
+ before {
+ rule.onNodeWithText("fromScene").assertIsDisplayed()
+ rule.onNodeWithText("toScene").assertDoesNotExist()
+ }
+
+ // During the transition, the element is composed in toScene.
+ at(32) {
+ rule.onNodeWithText("fromScene").assertDoesNotExist()
+ rule.onNodeWithText("toScene").assertIsDisplayed()
+ }
+
+ // At the end of the transition, the element is composed in toScene.
+ after {
+ rule.onNodeWithText("fromScene").assertDoesNotExist()
+ rule.onNodeWithText("toScene").assertIsDisplayed()
+ }
+ }
+ }
+
+ @Test
+ fun elementScopeExtendsBoxScope() {
+ rule.setContent {
+ TestSceneScope {
+ Element(TestElements.Foo, Modifier.size(200.dp)) {
+ content {
+ Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd))
+ Box(Modifier.testTag("matchParentSize").matchParentSize())
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("bottomEnd").assertPositionInRootIsEqualTo(200.dp, 200.dp)
+ rule.onNodeWithTag("matchParentSize").assertSizeIsEqualTo(200.dp, 200.dp)
+ }
+
+ @Test
+ fun movableElementScopeExtendsBoxScope() {
+ rule.setContent {
+ TestSceneScope {
+ MovableElement(TestElements.Foo, Modifier.size(200.dp)) {
+ content {
+ Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd))
+ Box(Modifier.testTag("matchParentSize").matchParentSize())
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("bottomEnd").assertPositionInRootIsEqualTo(200.dp, 200.dp)
+ rule.onNodeWithTag("matchParentSize").assertSizeIsEqualTo(200.dp, 200.dp)
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index c5b8d9a..75dee47 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -50,13 +50,4 @@
assertThat(state.isTransitioning(to = TestScenes.SceneA)).isFalse()
assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
}
-
- private fun transition(from: SceneKey, to: SceneKey): TransitionState.Transition {
- return object : TransitionState.Transition(from, to) {
- override val currentScene: SceneKey = from
- override val progress: Float = 0f
- override val isInitiatedByUserInput: Boolean = false
- override val isUserInputOngoing: Boolean = false
- }
- }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index ebbd500..649e499 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -113,25 +113,21 @@
@Composable
private fun SceneScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) {
- Box(
- modifier
- .size(size)
- .background(Color.Red)
- .element(TestElements.Foo)
- .testTag(TestElements.Foo.debugName)
- ) {
+ Element(TestElements.Foo, modifier.size(size).background(Color.Red)) {
// Offset the single child of Foo by some animated shared offset.
- val offset by animateSharedDpAsState(childOffset, TestValues.Value1, TestElements.Foo)
+ val offset by animateElementDpAsState(childOffset, TestValues.Value1)
- Box(
- Modifier.offset {
- val pxOffset = offset.roundToPx()
- IntOffset(pxOffset, pxOffset)
- }
- .size(30.dp)
- .background(Color.Blue)
- .testTag(TestElements.Bar.debugName)
- )
+ content {
+ Box(
+ Modifier.offset {
+ val pxOffset = offset.roundToPx()
+ IntOffset(pxOffset, pxOffset)
+ }
+ .size(30.dp)
+ .background(Color.Blue)
+ .testTag(TestElements.Bar.debugName)
+ )
+ }
}
}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt
new file mode 100644
index 0000000..238b21e1
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+/** A utility to easily create a [TransitionState.Transition] in tests. */
+fun transition(
+ from: SceneKey,
+ to: SceneKey,
+ progress: () -> Float = { 0f },
+ isInitiatedByUserInput: Boolean = false,
+ isUserInputOngoing: Boolean = false,
+): TransitionState.Transition {
+ return object : TransitionState.Transition(from, to) {
+ override val currentScene: SceneKey = from
+ override val progress: Float = progress()
+ override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput
+ override val isUserInputOngoing: Boolean = isUserInputOngoing
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 1f8e29a..62084aa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -28,6 +28,7 @@
import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
@@ -169,6 +170,109 @@
}
@Test
+ fun smartspaceDynamicSizing_oneCard_fullSize() =
+ testSmartspaceDynamicSizing(
+ totalTargets = 1,
+ expectedSizes =
+ listOf(
+ CommunalContentSize.FULL,
+ )
+ )
+
+ @Test
+ fun smartspace_dynamicSizing_twoCards_halfSize() =
+ testSmartspaceDynamicSizing(
+ totalTargets = 2,
+ expectedSizes =
+ listOf(
+ CommunalContentSize.HALF,
+ CommunalContentSize.HALF,
+ )
+ )
+
+ @Test
+ fun smartspace_dynamicSizing_threeCards_thirdSize() =
+ testSmartspaceDynamicSizing(
+ totalTargets = 3,
+ expectedSizes =
+ listOf(
+ CommunalContentSize.THIRD,
+ CommunalContentSize.THIRD,
+ CommunalContentSize.THIRD,
+ )
+ )
+
+ @Test
+ fun smartspace_dynamicSizing_fourCards_oneFullAndThreeThirdSize() =
+ testSmartspaceDynamicSizing(
+ totalTargets = 4,
+ expectedSizes =
+ listOf(
+ CommunalContentSize.FULL,
+ CommunalContentSize.THIRD,
+ CommunalContentSize.THIRD,
+ CommunalContentSize.THIRD,
+ )
+ )
+
+ @Test
+ fun smartspace_dynamicSizing_fiveCards_twoHalfAndThreeThirdSize() =
+ testSmartspaceDynamicSizing(
+ totalTargets = 5,
+ expectedSizes =
+ listOf(
+ CommunalContentSize.HALF,
+ CommunalContentSize.HALF,
+ CommunalContentSize.THIRD,
+ CommunalContentSize.THIRD,
+ CommunalContentSize.THIRD,
+ )
+ )
+
+ @Test
+ fun smartspace_dynamicSizing_sixCards_allThirdSize() =
+ testSmartspaceDynamicSizing(
+ totalTargets = 6,
+ expectedSizes =
+ listOf(
+ CommunalContentSize.THIRD,
+ CommunalContentSize.THIRD,
+ CommunalContentSize.THIRD,
+ CommunalContentSize.THIRD,
+ CommunalContentSize.THIRD,
+ CommunalContentSize.THIRD,
+ )
+ )
+
+ private fun testSmartspaceDynamicSizing(
+ totalTargets: Int,
+ expectedSizes: List<CommunalContentSize>,
+ ) =
+ testScope.runTest {
+ // Keyguard showing, and tutorial completed.
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setKeyguardOccluded(false)
+ tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
+
+ val targets = mutableListOf<SmartspaceTarget>()
+ for (index in 0 until totalTargets) {
+ val target = mock(SmartspaceTarget::class.java)
+ whenever(target.smartspaceTargetId).thenReturn("target$index")
+ whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
+ whenever(target.remoteViews).thenReturn(mock(RemoteViews::class.java))
+ targets.add(target)
+ }
+
+ smartspaceRepository.setCommunalSmartspaceTargets(targets)
+
+ val smartspaceContent by collectLastValue(underTest.smartspaceContent)
+ assertThat(smartspaceContent?.size).isEqualTo(totalTargets)
+ for (index in 0 until totalTargets) {
+ assertThat(smartspaceContent?.get(index)?.size).isEqualTo(expectedSizes[index])
+ }
+ }
+
+ @Test
fun umo_mediaPlaying_showsUmo() =
testScope.runTest {
// Tutorial completed.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
similarity index 96%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
index 477f455..0329794 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -12,20 +12,19 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
- *
*/
package com.android.systemui.keyguard.data.quickaffordance
import android.app.Activity
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.res.R
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -37,17 +36,17 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.junit.runners.Parameterized.Parameter
-import org.junit.runners.Parameterized.Parameters
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.Parameter
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTestCase() {
companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
index 9daf186..e7037a6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
@@ -94,7 +94,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(6)
+ assertThat(values.size).isEqualTo(5)
values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt
new file mode 100644
index 0000000..83782e2
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AodAlphaViewModelTest : SysuiTestCase() {
+
+ @Mock
+ private lateinit var occludedToLockscreenTransitionViewModel:
+ OccludedToLockscreenTransitionViewModel
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val occludedToLockscreenAlpha = MutableStateFlow(0f)
+
+ private lateinit var underTest: AodAlphaViewModel
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(occludedToLockscreenTransitionViewModel.lockscreenAlpha)
+ .thenReturn(occludedToLockscreenAlpha)
+ kosmos.occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel
+
+ underTest = kosmos.aodAlphaViewModel
+ }
+
+ @Test
+ fun alpha() =
+ testScope.runTest {
+ val alpha by collectLastValue(underTest.alpha)
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.OFF,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = testScope,
+ )
+
+ keyguardRepository.setKeyguardAlpha(0.1f)
+ assertThat(alpha).isEqualTo(0.1f)
+ keyguardRepository.setKeyguardAlpha(0.5f)
+ assertThat(alpha).isEqualTo(0.5f)
+ keyguardRepository.setKeyguardAlpha(0.2f)
+ assertThat(alpha).isEqualTo(0.2f)
+ keyguardRepository.setKeyguardAlpha(0f)
+ assertThat(alpha).isEqualTo(0f)
+ occludedToLockscreenAlpha.value = 0.8f
+ assertThat(alpha).isEqualTo(0.8f)
+ }
+
+ @Test
+ fun alpha_whenGone_equalsZero() =
+ testScope.runTest {
+ val alpha by collectLastValue(underTest.alpha)
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope = testScope,
+ )
+
+ keyguardRepository.setKeyguardAlpha(0.1f)
+ assertThat(alpha).isEqualTo(0f)
+ keyguardRepository.setKeyguardAlpha(0.5f)
+ assertThat(alpha).isEqualTo(0f)
+ keyguardRepository.setKeyguardAlpha(1f)
+ assertThat(alpha).isEqualTo(0f)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
new file mode 100644
index 0000000..0543bc2
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
+import com.android.systemui.keyguard.domain.interactor.burnInInteractor
+import com.android.systemui.keyguard.shared.model.BurnInModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.clocks.ClockController
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Answers
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AodBurnInViewModelTest : SysuiTestCase() {
+
+ @Mock private lateinit var burnInInteractor: BurnInInteractor
+ @Mock private lateinit var goneToAodTransitionViewModel: GoneToAodTransitionViewModel
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var clockController: ClockController
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private lateinit var underTest: AodBurnInViewModel
+
+ private var burnInParameters =
+ BurnInParameters(
+ clockControllerProvider = { clockController },
+ )
+ private val burnInFlow = MutableStateFlow(BurnInModel())
+ private val enterFromTopAnimationAlpha = MutableStateFlow(0f)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(burnInInteractor.keyguardBurnIn).thenReturn(burnInFlow)
+ kosmos.burnInInteractor = burnInInteractor
+ whenever(goneToAodTransitionViewModel.enterFromTopAnimationAlpha)
+ .thenReturn(enterFromTopAnimationAlpha)
+ whenever(goneToAodTransitionViewModel.enterFromTopTranslationY(anyInt()))
+ .thenReturn(emptyFlow())
+ kosmos.goneToAodTransitionViewModel = goneToAodTransitionViewModel
+
+ underTest = kosmos.aodBurnInViewModel
+ }
+
+ @Test
+ fun translationY_initializedToZero() =
+ testScope.runTest {
+ val translationY by collectLastValue(underTest.translationY(burnInParameters))
+ assertThat(translationY).isEqualTo(0)
+ }
+
+ @Test
+ fun translationAndScale_whenNotDozing() =
+ testScope.runTest {
+ val translationX by collectLastValue(underTest.translationX(burnInParameters))
+ val translationY by collectLastValue(underTest.translationY(burnInParameters))
+ val scale by collectLastValue(underTest.scale(burnInParameters))
+
+ // Set to not dozing (on lockscreen)
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ value = 1f,
+ transitionState = TransitionState.FINISHED
+ ),
+ validateStep = false,
+ )
+
+ // Trigger a change to the burn-in model
+ burnInFlow.value =
+ BurnInModel(
+ translationX = 20,
+ translationY = 30,
+ scale = 0.5f,
+ )
+
+ assertThat(translationX).isEqualTo(0)
+ assertThat(translationY).isEqualTo(0)
+ assertThat(scale)
+ .isEqualTo(
+ BurnInScaleViewModel(
+ scale = 1f,
+ scaleClockOnly = true,
+ )
+ )
+ }
+
+ @Test
+ fun translationAndScale_whenFullyDozing() =
+ testScope.runTest {
+ burnInParameters = burnInParameters.copy(statusViewTop = 100)
+ val translationX by collectLastValue(underTest.translationX(burnInParameters))
+ val translationY by collectLastValue(underTest.translationY(burnInParameters))
+ val scale by collectLastValue(underTest.scale(burnInParameters))
+
+ // Set to dozing (on AOD)
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ value = 1f,
+ transitionState = TransitionState.FINISHED
+ ),
+ validateStep = false,
+ )
+ // Trigger a change to the burn-in model
+ burnInFlow.value =
+ BurnInModel(
+ translationX = 20,
+ translationY = 30,
+ scale = 0.5f,
+ )
+
+ assertThat(translationX).isEqualTo(20)
+ assertThat(translationY).isEqualTo(30)
+ assertThat(scale)
+ .isEqualTo(
+ BurnInScaleViewModel(
+ scale = 0.5f,
+ scaleClockOnly = true,
+ )
+ )
+
+ // Set to the beginning of GONE->AOD transition
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ value = 0f,
+ transitionState = TransitionState.STARTED
+ ),
+ validateStep = false,
+ )
+ assertThat(translationX).isEqualTo(0)
+ assertThat(translationY).isEqualTo(0)
+ assertThat(scale)
+ .isEqualTo(
+ BurnInScaleViewModel(
+ scale = 1f,
+ scaleClockOnly = true,
+ )
+ )
+ }
+
+ @Test
+ fun translationAndScale_whenFullyDozing_staysOutOfTopInset() =
+ testScope.runTest {
+ burnInParameters =
+ burnInParameters.copy(
+ statusViewTop = 100,
+ topInset = 80,
+ )
+ val translationX by collectLastValue(underTest.translationX(burnInParameters))
+ val translationY by collectLastValue(underTest.translationY(burnInParameters))
+ val scale by collectLastValue(underTest.scale(burnInParameters))
+
+ // Set to dozing (on AOD)
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ value = 1f,
+ transitionState = TransitionState.FINISHED
+ ),
+ validateStep = false,
+ )
+
+ // Trigger a change to the burn-in model
+ burnInFlow.value =
+ BurnInModel(
+ translationX = 20,
+ translationY = -30,
+ scale = 0.5f,
+ )
+ assertThat(translationX).isEqualTo(20)
+ // -20 instead of -30, due to inset of 80
+ assertThat(translationY).isEqualTo(-20)
+ assertThat(scale)
+ .isEqualTo(
+ BurnInScaleViewModel(
+ scale = 0.5f,
+ scaleClockOnly = true,
+ )
+ )
+
+ // Set to the beginning of GONE->AOD transition
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ value = 0f,
+ transitionState = TransitionState.STARTED
+ ),
+ validateStep = false,
+ )
+ assertThat(translationX).isEqualTo(0)
+ assertThat(translationY).isEqualTo(0)
+ assertThat(scale)
+ .isEqualTo(
+ BurnInScaleViewModel(
+ scale = 1f,
+ scaleClockOnly = true,
+ )
+ )
+ }
+
+ @Test
+ fun translationAndScale_useScaleOnly() =
+ testScope.runTest {
+ whenever(clockController.config.useAlternateSmartspaceAODTransition).thenReturn(true)
+
+ val translationX by collectLastValue(underTest.translationX(burnInParameters))
+ val translationY by collectLastValue(underTest.translationY(burnInParameters))
+ val scale by collectLastValue(underTest.scale(burnInParameters))
+
+ // Set to dozing (on AOD)
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ value = 1f,
+ transitionState = TransitionState.FINISHED
+ ),
+ validateStep = false,
+ )
+
+ // Trigger a change to the burn-in model
+ burnInFlow.value =
+ BurnInModel(
+ translationX = 20,
+ translationY = 30,
+ scale = 0.5f,
+ )
+
+ assertThat(translationX).isEqualTo(0)
+ assertThat(translationY).isEqualTo(0)
+ assertThat(scale).isEqualTo(BurnInScaleViewModel(scale = 0.5f, scaleClockOnly = false))
+ }
+
+ @Test
+ fun alpha() =
+ testScope.runTest {
+ val alpha by collectLastValue(underTest.alpha)
+
+ enterFromTopAnimationAlpha.value = 0.2f
+ assertThat(alpha).isEqualTo(0.2f)
+
+ enterFromTopAnimationAlpha.value = 1f
+ assertThat(alpha).isEqualTo(1f)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index 53bca48..e141c2b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -55,6 +55,28 @@
private val underTest = kosmos.dreamingToLockscreenTransitionViewModel
@Test
+ fun shortcutsAlpha_bothShortcutsReceiveLastValue() =
+ testScope.runTest {
+ val valuesLeft by collectValues(underTest.shortcutsAlpha)
+ val valuesRight by collectValues(underTest.shortcutsAlpha)
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ step(0f, TransitionState.STARTED),
+ step(0.3f),
+ step(0.5f),
+ step(0.6f),
+ step(0.8f),
+ step(1f),
+ ),
+ testScope,
+ )
+
+ assertThat(valuesLeft.last()).isEqualTo(1f)
+ assertThat(valuesRight.last()).isEqualTo(1f)
+ }
+
+ @Test
fun dreamOverlayTranslationY() =
testScope.runTest {
val pixels = 100
@@ -73,7 +95,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(7)
+ assertThat(values.size).isEqualTo(6)
values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
}
@@ -95,7 +117,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(4)
+ assertThat(values.size).isEqualTo(3)
values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
}
@@ -210,7 +232,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
index 3c07034..897ce6d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -61,7 +61,7 @@
// Only three values should be present, since the dream overlay runs for a small
// fraction of the overall animation time
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
}
@@ -84,7 +84,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
new file mode 100644
index 0000000..7c3dc97
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags as AConfigFlags
+import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.data.repository.fakeNotificationsKeyguardViewStateRepository
+import com.android.systemui.statusbar.phone.dozeParameters
+import com.android.systemui.statusbar.phone.screenOffAnimationController
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.stopAnimating
+import com.android.systemui.util.ui.value
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyguardRootViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val screenOffAnimationController = kosmos.screenOffAnimationController
+ private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
+ private val fakeNotificationsKeyguardViewStateRepository =
+ kosmos.fakeNotificationsKeyguardViewStateRepository
+ private val dozeParameters = kosmos.dozeParameters
+ private val underTest = kosmos.keyguardRootViewModel
+
+ @Before
+ fun setUp() {
+ mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
+ mSetFlagsRule.enableFlags(FLAG_NEW_AOD_TRANSITION)
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+ }
+
+ @Test
+ fun burnInLayerVisibility() =
+ testScope.runTest {
+ val burnInLayerVisibility by collectLastValue(underTest.burnInLayerVisibility)
+
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ value = 0f,
+ transitionState = TransitionState.STARTED
+ ),
+ validateStep = false,
+ )
+ assertThat(burnInLayerVisibility).isEqualTo(View.VISIBLE)
+ }
+
+ @Test
+ fun iconContainer_isNotVisible_notOnKeyguard_dontShowAodIconsWhenShade() =
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
+ runCurrent()
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.OFF,
+ to = KeyguardState.GONE,
+ testScope,
+ )
+ whenever(screenOffAnimationController.shouldShowAodIconsWhenShade()).thenReturn(false)
+ runCurrent()
+
+ assertThat(isVisible?.value).isFalse()
+ assertThat(isVisible?.isAnimating).isFalse()
+ }
+
+ @Test
+ fun iconContainer_isVisible_bypassEnabled() =
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
+ runCurrent()
+ deviceEntryRepository.setBypassEnabled(true)
+ runCurrent()
+
+ assertThat(isVisible?.value).isTrue()
+ }
+
+ @Test
+ fun iconContainer_isNotVisible_pulseExpanding_notBypassing() =
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
+ runCurrent()
+ fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(true)
+ deviceEntryRepository.setBypassEnabled(false)
+ runCurrent()
+
+ assertThat(isVisible?.value).isEqualTo(false)
+ }
+
+ @Test
+ fun iconContainer_isVisible_notifsFullyHidden_bypassEnabled() =
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
+ runCurrent()
+ fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
+ deviceEntryRepository.setBypassEnabled(true)
+ fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
+ runCurrent()
+
+ assertThat(isVisible?.value).isTrue()
+ assertThat(isVisible?.isAnimating).isTrue()
+ }
+
+ @Test
+ fun iconContainer_isVisible_notifsFullyHidden_bypassDisabled_aodDisabled() =
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
+ runCurrent()
+ fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
+ deviceEntryRepository.setBypassEnabled(false)
+ whenever(dozeParameters.alwaysOn).thenReturn(false)
+ fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
+ runCurrent()
+
+ assertThat(isVisible?.value).isTrue()
+ assertThat(isVisible?.isAnimating).isFalse()
+ }
+
+ @Test
+ fun iconContainer_isVisible_notifsFullyHidden_bypassDisabled_displayNeedsBlanking() =
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
+ runCurrent()
+ fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
+ deviceEntryRepository.setBypassEnabled(false)
+ whenever(dozeParameters.alwaysOn).thenReturn(true)
+ whenever(dozeParameters.displayNeedsBlanking).thenReturn(true)
+ fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
+ runCurrent()
+
+ assertThat(isVisible?.value).isTrue()
+ assertThat(isVisible?.isAnimating).isFalse()
+ }
+
+ @Test
+ fun iconContainer_isVisible_notifsFullyHidden_bypassDisabled() =
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
+ runCurrent()
+ fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
+ deviceEntryRepository.setBypassEnabled(false)
+ whenever(dozeParameters.alwaysOn).thenReturn(true)
+ whenever(dozeParameters.displayNeedsBlanking).thenReturn(false)
+ fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
+ runCurrent()
+
+ assertThat(isVisible?.value).isTrue()
+ assertThat(isVisible?.isAnimating).isTrue()
+ }
+
+ @Test
+ fun isIconContainerVisible_stopAnimation() =
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
+ runCurrent()
+ fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
+ deviceEntryRepository.setBypassEnabled(false)
+ whenever(dozeParameters.alwaysOn).thenReturn(true)
+ whenever(dozeParameters.displayNeedsBlanking).thenReturn(false)
+ fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
+ runCurrent()
+
+ assertThat(isVisible?.isAnimating).isEqualTo(true)
+ isVisible?.stopAnimating()
+ runCurrent()
+
+ assertThat(isVisible?.isAnimating).isEqualTo(false)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index d07836d..74d309c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.keyguard.ui.viewmodel
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -26,6 +28,7 @@
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -94,7 +97,6 @@
KeyguardLongPressViewModel(
interactor = mock(),
),
- keyguardRoot = utils.keyguardRootViewModel(),
notifications = utils.notificationsPlaceholderViewModel(),
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index a346e8b..4843f8b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -75,7 +75,7 @@
// Only three values should be present, since the dream overlay runs for a small
// fraction of the overall animation time
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
}
@@ -98,10 +98,10 @@
testScope = testScope,
)
- assertThat(values.size).isEqualTo(6)
+ assertThat(values.size).isEqualTo(5)
values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
// Validate finished value
- assertThat(values[5]).isEqualTo(0f)
+ assertThat(values[4]).isEqualTo(0f)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index 274bde1..a1b8aab 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -76,7 +76,7 @@
)
// Only 3 values should be present, since the dream overlay runs for a small fraction
// of the overall animation time
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
}
@@ -99,7 +99,7 @@
),
testScope = testScope,
)
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
}
@@ -121,11 +121,11 @@
),
testScope = testScope,
)
- assertThat(values.size).isEqualTo(4)
+ assertThat(values.size).isEqualTo(3)
values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
// Cancel will reset the translation
- assertThat(values[3]).isEqualTo(0)
+ assertThat(values[2]).isEqualTo(0)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
index d419d4a..2111ad5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -95,7 +95,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index f027bc8..90b8362 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -95,7 +95,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(3)
+ assertThat(values.size).isEqualTo(1)
values.forEach { assertThat(it).isEqualTo(0f) }
}
@@ -107,7 +107,7 @@
keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
keyguardTransitionRepository.sendTransitionStep(step(1f))
- assertThat(values.size).isEqualTo(2)
+ assertThat(values.size).isEqualTo(1)
values.forEach { assertThat(it).isEqualTo(0f) }
}
@@ -121,7 +121,7 @@
keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
keyguardTransitionRepository.sendTransitionStep(step(1f))
- assertThat(values.size).isEqualTo(2)
+ assertThat(values.size).isEqualTo(1)
values.forEach { assertThat(it).isEqualTo(1f) }
}
@@ -135,7 +135,7 @@
keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
keyguardTransitionRepository.sendTransitionStep(step(1f))
- assertThat(values.size).isEqualTo(2)
+ assertThat(values.size).isEqualTo(1)
values.forEach { assertThat(it).isEqualTo(1f) }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 224903f..efd4f9b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -153,7 +153,6 @@
KeyguardLongPressViewModel(
interactor = mock(),
),
- keyguardRoot = utils.keyguardRootViewModel(),
notifications = utils.notificationsPlaceholderViewModel(),
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index 53cb8a7..7a78b36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -25,15 +25,14 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.graphics.Point;
import android.os.PowerManager;
-import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.view.View;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardUpdateMonitor;
@@ -68,7 +67,7 @@
import java.util.HashSet;
@SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
@RunWithLooper(setAsMainLooper = true)
public class DozeServiceHostTest extends SysuiTestCase {
@@ -181,6 +180,7 @@
DozeLog.PULSE_REASON_DOCKING,
DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE,
DozeLog.REASON_SENSOR_QUICK_PICKUP,
+ DozeLog.PULSE_REASON_FINGERPRINT_ACTIVATED,
DozeLog.REASON_SENSOR_TAP));
HashSet<Integer> reasonsThatDontPulse = new HashSet<>(
Arrays.asList(DozeLog.REASON_SENSOR_PICKUP,
@@ -232,7 +232,7 @@
public void onSlpiTap_doesnt_pass_negative_values() {
mDozeServiceHost.onSlpiTap(-1, 200);
mDozeServiceHost.onSlpiTap(100, -2);
- verifyZeroInteractions(mDozeInteractor);
+ verify(mDozeInteractor, never()).setLastTapToWakePosition(any());
}
@Test
public void dozeTimeTickSentToDozeInteractor() {
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index e01a2aa..5c362b2 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -963,10 +963,16 @@
<bool name="config_edgeToEdgeBottomSheetDialog">true</bool>
<!--
+ Time in milliseconds the user has to touch the side FPS sensor to successfully authenticate when
+ the screen is turned off with AOD not enabled.
+ TODO(b/302332976) Get this value from the HAL if they can provide an API for it.
+ -->
+ <integer name="config_restToUnlockDurationScreenOff">500</integer>
+ <!--
Time in milliseconds the user has to touch the side FPS sensor to successfully authenticate
TODO(b/302332976) Get this value from the HAL if they can provide an API for it.
-->
- <integer name="config_restToUnlockDuration">300</integer>
+ <integer name="config_restToUnlockDurationDefault">300</integer>
<!--
Width in pixels of the Side FPS sensor.
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index bcc2044..82410fd 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -33,6 +33,7 @@
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.customization.R
import com.android.systemui.dagger.qualifiers.Background
@@ -325,6 +326,10 @@
}
}
+ if (visible) {
+ refreshTime()
+ }
+
smallTimeListener?.update(shouldTimeListenerRun)
largeTimeListener?.update(shouldTimeListenerRun)
}
@@ -346,6 +351,19 @@
weatherData = data
clock?.run { events.onWeatherDataChanged(data) }
}
+
+ override fun onTimeChanged() {
+ refreshTime()
+ }
+
+ private fun refreshTime() {
+ if (!migrateClocksToBlueprint()) {
+ return
+ }
+
+ clock?.smallClock?.events?.onTimeTick()
+ clock?.largeClock?.events?.onTimeTick()
+ }
}
private val zenModeCallback = object : ZenModeController.Callback {
@@ -558,7 +576,8 @@
isRunning = true
when (clockFace.config.tickRate) {
ClockTickRate.PER_MINUTE -> {
- /* Handled by KeyguardClockSwitchController */
+ // Handled by KeyguardClockSwitchController and
+ // by KeyguardUpdateMonitorCallback#onTimeChanged.
}
ClockTickRate.PER_SECOND -> executor.execute(secondsRunnable)
ClockTickRate.PER_FRAME -> {
diff --git a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt
index 603471b..7a560e8 100644
--- a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt
+++ b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt
@@ -19,6 +19,7 @@
import android.annotation.BinderThread
import android.os.Handler
import android.os.Trace
+import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.unfold.SysUIUnfoldComponent
@@ -59,8 +60,11 @@
foldAodAnimationController?.onScreenTurningOn(pendingTasks.registerTask("fold-to-aod"))
pendingTasks.onTasksComplete {
- mainHandler.post {
+ if (Flags.enableBackgroundKeyguardOndrawnCallback()) {
+ // called by whatever thread completes the last task registered.
onDrawn.run()
+ } else {
+ mainHandler.post { onDrawn.run() }
}
}
Trace.endSection()
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 8a1a2da..a4f90eb 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -106,6 +106,7 @@
import javax.inject.Provider;
import kotlinx.coroutines.CoroutineScope;
+import kotlinx.coroutines.Job;
/**
* Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the
@@ -136,6 +137,7 @@
private final Provider<UdfpsController> mUdfpsControllerFactory;
private final Provider<SideFpsController> mSidefpsControllerFactory;
private final CoroutineScope mApplicationCoroutineScope;
+ private Job mBiometricContextListenerJob = null;
// TODO: these should be migrated out once ready
@NonNull private final Provider<PromptCredentialInteractor> mPromptCredentialInteractor;
@@ -914,7 +916,11 @@
@Override
public void setBiometricContextListener(IBiometricContextListener listener) {
- mLogContextInteractor.get().addBiometricContextListener(listener);
+ if (mBiometricContextListenerJob != null) {
+ mBiometricContextListenerJob.cancel(null);
+ }
+ mBiometricContextListenerJob =
+ mLogContextInteractor.get().addBiometricContextListener(listener);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt
index f4231ac..c320350 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt
@@ -26,6 +26,8 @@
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.isDefaultOrientation
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.log.SideFpsLogger
import com.android.systemui.res.R
import java.util.Optional
@@ -47,6 +49,7 @@
windowManager: WindowManager,
displayStateInteractor: DisplayStateInteractor,
fingerprintInteractiveToAuthProvider: Optional<FingerprintInteractiveToAuthProvider>,
+ keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val logger: SideFpsLogger,
) {
@@ -62,8 +65,21 @@
val isAvailable: Flow<Boolean> =
fingerprintPropertyRepository.sensorType.map { it == FingerprintSensorType.POWER_BUTTON }
- val authenticationDuration: Long =
- context.resources?.getInteger(R.integer.config_restToUnlockDuration)?.toLong() ?: 0L
+ val authenticationDuration: Flow<Long> =
+ keyguardTransitionInteractor
+ .isFinishedInStateWhere { it == KeyguardState.OFF || it == KeyguardState.DOZING }
+ .map {
+ if (it)
+ context.resources
+ ?.getInteger(R.integer.config_restToUnlockDurationScreenOff)
+ ?.toLong()
+ else
+ context.resources
+ ?.getInteger(R.integer.config_restToUnlockDurationDefault)
+ ?.toLong()
+ }
+ .map { it ?: 0L }
+ .onEach { logger.authDurationChanged(it) }
val isProlongedTouchRequiredForAuthentication: Flow<Boolean> =
if (fingerprintInteractiveToAuthProvider.isEmpty) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 0f4e583..18fb895 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -24,6 +24,9 @@
import com.android.systemui.communal.data.repository.CommunalWidgetRepository
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalContentSize
+import com.android.systemui.communal.shared.model.CommunalContentSize.FULL
+import com.android.systemui.communal.shared.model.CommunalContentSize.HALF
+import com.android.systemui.communal.shared.model.CommunalContentSize.THIRD
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
@@ -133,12 +136,11 @@
target.featureType == SmartspaceTarget.FEATURE_TIMER &&
target.remoteViews != null
}
- .map Target@{ target ->
+ .mapIndexed Target@{ index, target ->
return@Target CommunalContentModel.Smartspace(
smartspaceTargetId = target.smartspaceTargetId,
remoteViews = target.remoteViews!!,
- // Smartspace always as HALF for now.
- size = CommunalContentSize.HALF,
+ size = dynamicContentSize(targets.size, index),
)
}
}
@@ -147,23 +149,50 @@
/** A list of tutorial content to be displayed in the communal hub in tutorial mode. */
val tutorialContent: List<CommunalContentModel.Tutorial> =
listOf(
- CommunalContentModel.Tutorial(id = 0, CommunalContentSize.FULL),
- CommunalContentModel.Tutorial(id = 1, CommunalContentSize.THIRD),
- CommunalContentModel.Tutorial(id = 2, CommunalContentSize.THIRD),
- CommunalContentModel.Tutorial(id = 3, CommunalContentSize.THIRD),
- CommunalContentModel.Tutorial(id = 4, CommunalContentSize.HALF),
- CommunalContentModel.Tutorial(id = 5, CommunalContentSize.HALF),
- CommunalContentModel.Tutorial(id = 6, CommunalContentSize.HALF),
- CommunalContentModel.Tutorial(id = 7, CommunalContentSize.HALF),
+ CommunalContentModel.Tutorial(id = 0, FULL),
+ CommunalContentModel.Tutorial(id = 1, THIRD),
+ CommunalContentModel.Tutorial(id = 2, THIRD),
+ CommunalContentModel.Tutorial(id = 3, THIRD),
+ CommunalContentModel.Tutorial(id = 4, HALF),
+ CommunalContentModel.Tutorial(id = 5, HALF),
+ CommunalContentModel.Tutorial(id = 6, HALF),
+ CommunalContentModel.Tutorial(id = 7, HALF),
)
val umoContent: Flow<List<CommunalContentModel.Umo>> =
mediaRepository.mediaPlaying.flatMapLatest { mediaPlaying ->
if (mediaPlaying) {
// TODO(b/310254801): support HALF and FULL layouts
- flowOf(listOf(CommunalContentModel.Umo(CommunalContentSize.THIRD)))
+ flowOf(listOf(CommunalContentModel.Umo(THIRD)))
} else {
flowOf(emptyList())
}
}
+
+ companion object {
+ /**
+ * Calculates the content size dynamically based on the total number of contents of that
+ * type.
+ *
+ * Contents with the same type are expected to fill each column evenly. Currently there are
+ * three possible sizes. When the total number is 1, size for that content is [FULL], when
+ * the total number is 2, size for each is [HALF], and 3, size for each is [THIRD].
+ *
+ * When dynamic contents fill in multiple columns, the first column follows the algorithm
+ * above, and the remaining contents are packed in [THIRD]s. For example, when the total
+ * number if 4, the first one is [FULL], filling the column, and the remaining 3 are
+ * [THIRD].
+ *
+ * @param size The total number of contents of this type.
+ * @param index The index of the current content of this type.
+ */
+ private fun dynamicContentSize(size: Int, index: Int): CommunalContentSize {
+ val remainder = size % CommunalContentSize.entries.size
+ return CommunalContentSize.toSize(
+ span =
+ FULL.span /
+ if (index > remainder - 1) CommunalContentSize.entries.size else remainder
+ )
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt
index c903709..572794d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt
@@ -30,5 +30,13 @@
HALF(3),
/** Content takes a third of the height of the column. */
- THIRD(2),
+ THIRD(2);
+
+ companion object {
+ /** Converts from span to communal content size. */
+ fun toSize(span: Int): CommunalContentSize {
+ return entries.find { it.span == span }
+ ?: throw Exception("Invalid span for communal content size")
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
index 5df26b3..a6b4320 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
@@ -28,27 +28,26 @@
/**
* Factory to create dialogs for consenting to show app panels for specific apps.
*
- * [internalDialogFactory] is for facilitating testing.
+ * [dialogFactory] is for facilitating testing.
*/
-class PanelConfirmationDialogFactory(
- private val internalDialogFactory: (Context) -> SystemUIDialog
+class PanelConfirmationDialogFactory @Inject constructor(
+ private val dialogFactory: SystemUIDialog.Factory
) {
- @Inject constructor() : this({ SystemUIDialog(it) })
/**
* Creates a dialog to show to the user. [response] will be true if an only if the user responds
* affirmatively.
*/
fun createConfirmationDialog(
- context: Context,
- appName: CharSequence,
- response: Consumer<Boolean>
+ context: Context,
+ appName: CharSequence,
+ response: Consumer<Boolean>
): Dialog {
val listener =
DialogInterface.OnClickListener { _, which ->
response.accept(which == DialogInterface.BUTTON_POSITIVE)
}
- return internalDialogFactory(context).apply {
+ return dialogFactory.create(context).apply {
setTitle(this.context.getString(R.string.controls_panel_authorization_title, appName))
setMessage(this.context.getString(R.string.controls_panel_authorization, appName))
setCanceledOnTouchOutside(true)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt
index 2ad6014..e42a4a6 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt
@@ -25,20 +25,21 @@
import java.util.function.Consumer
import javax.inject.Inject
-class ControlsDialogsFactory(private val internalDialogFactory: (Context) -> SystemUIDialog) {
+class ControlsDialogsFactory @Inject constructor(
+ private val dialogFactory: SystemUIDialog.Factory
+) {
- @Inject constructor() : this({ SystemUIDialog(it) })
fun createRemoveAppDialog(
- context: Context,
- appName: CharSequence,
- response: Consumer<Boolean>
+ context: Context,
+ appName: CharSequence,
+ response: Consumer<Boolean>
): Dialog {
val listener =
DialogInterface.OnClickListener { _, which ->
response.accept(which == DialogInterface.BUTTON_POSITIVE)
}
- return internalDialogFactory(context).apply {
+ return dialogFactory.create(context).apply {
setTitle(context.getString(R.string.controls_panel_remove_app_authorization, appName))
setCanceledOnTouchOutside(true)
setOnCancelListener { response.accept(false) }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index a25c788..92300ef 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -112,6 +112,7 @@
import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent;
import com.android.systemui.statusbar.notification.row.dagger.NotificationRowComponent;
import com.android.systemui.statusbar.phone.CentralSurfaces;
+import com.android.systemui.statusbar.phone.ConfigurationControllerModule;
import com.android.systemui.statusbar.phone.LetterboxModule;
import com.android.systemui.statusbar.phone.NotificationIconAreaControllerModule;
import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule;
@@ -178,6 +179,7 @@
ClockRegistryModule.class,
CommunalModule.class,
CommonDataLayerModule.class,
+ ConfigurationControllerModule.class,
ConnectivityModule.class,
ControlsModule.class,
CoroutinesModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
index efa1c0a..684627b 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
@@ -19,6 +19,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterIsInstance
@@ -31,4 +32,11 @@
) {
val fingerprintFailure: Flow<FailFingerprintAuthenticationStatus> =
repository.authenticationStatus.filterIsInstance<FailFingerprintAuthenticationStatus>()
+
+ /** Whether fingerprint authentication is currently running or not */
+ val isRunning: Flow<Boolean> = repository.isRunning
+
+ /** Provide the current status of fingerprint authentication. */
+ val authenticationStatus: Flow<FingerprintAuthenticationStatus> =
+ repository.authenticationStatus
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
index 4c4aa5c..8776ec5 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
@@ -118,6 +118,11 @@
* Called when the dozing state may have been updated.
*/
default void onDozingChanged(boolean isDozing) {}
+
+ /**
+ * Called when fingerprint acquisition has started and screen state might need updating.
+ */
+ default void onSideFingerprintAcquisitionStarted() {}
}
interface PulseCallback {
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 5b90ef2..424bd0a 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -514,6 +514,7 @@
case REASON_SENSOR_TAP: return "tap";
case REASON_SENSOR_UDFPS_LONG_PRESS: return "udfps";
case REASON_SENSOR_QUICK_PICKUP: return "quickPickup";
+ case PULSE_REASON_FINGERPRINT_ACTIVATED: return "fingerprint-triggered";
default: throw new IllegalArgumentException("invalid reason: " + pulseReason);
}
}
@@ -542,7 +543,9 @@
PULSE_REASON_SENSOR_SIGMOTION, REASON_SENSOR_PICKUP, REASON_SENSOR_DOUBLE_TAP,
PULSE_REASON_SENSOR_LONG_PRESS, PULSE_REASON_DOCKING, REASON_SENSOR_WAKE_UP_PRESENCE,
PULSE_REASON_SENSOR_WAKE_REACH, REASON_SENSOR_TAP,
- REASON_SENSOR_UDFPS_LONG_PRESS, REASON_SENSOR_QUICK_PICKUP})
+ REASON_SENSOR_UDFPS_LONG_PRESS, REASON_SENSOR_QUICK_PICKUP,
+ PULSE_REASON_FINGERPRINT_ACTIVATED
+ })
public @interface Reason {}
public static final int PULSE_REASON_NONE = -1;
public static final int PULSE_REASON_INTENT = 0;
@@ -557,6 +560,7 @@
public static final int REASON_SENSOR_TAP = 9;
public static final int REASON_SENSOR_UDFPS_LONG_PRESS = 10;
public static final int REASON_SENSOR_QUICK_PICKUP = 11;
+ public static final int PULSE_REASON_FINGERPRINT_ACTIVATED = 12;
- public static final int TOTAL_REASONS = 12;
+ public static final int TOTAL_REASONS = 13;
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 795c3d4..9311187 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -265,6 +265,10 @@
mDozeLog.traceNotificationPulse();
}
+ private void onSideFingerprintAcquisitionStarted() {
+ requestPulse(DozeLog.PULSE_REASON_FINGERPRINT_ACTIVATED, false, null);
+ }
+
private static void runIfNotNull(Runnable runnable) {
if (runnable != null) {
runnable.run();
@@ -690,5 +694,10 @@
public void onNotificationAlerted(Runnable onPulseSuppressedListener) {
onNotification(onPulseSuppressedListener);
}
+
+ @Override
+ public void onSideFingerprintAcquisitionStarted() {
+ DozeTriggers.this.onSideFingerprintAcquisitionStarted();
+ }
};
}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
index 6cb68ba..89bfd96 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
@@ -16,6 +16,7 @@
package com.android.systemui.haptics.slider
+import android.view.MotionEvent
import androidx.annotation.FloatRange
/** Configuration parameters of a [SliderHapticFeedbackProvider] */
@@ -38,6 +39,8 @@
val numberOfLowTicks: Int = 5,
/** Maximum velocity allowed for vibration scaling. This is not expected to change. */
val maxVelocityToScale: Float = 2000f, /* In pixels/sec */
+ /** Axis to use when computing velocity. Must be the same as the slider's axis of movement */
+ val velocityAxis: Int = MotionEvent.AXIS_X,
/** Vibration scale at the upper bookend of the slider */
@FloatRange(from = 0.0, to = 1.0) val upperBookendScale: Float = 1f,
/** Vibration scale at the lower bookend of the slider */
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
index 9e6245a..6f28ab7 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
@@ -162,27 +162,33 @@
override fun onLowerBookend() {
if (!hasVibratedAtLowerBookend) {
- velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
- vibrateOnEdgeCollision(abs(velocityTracker.xVelocity))
+ vibrateOnEdgeCollision(abs(getTrackedVelocity()))
hasVibratedAtLowerBookend = true
}
}
override fun onUpperBookend() {
if (!hasVibratedAtUpperBookend) {
- velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
- vibrateOnEdgeCollision(abs(velocityTracker.xVelocity))
+ vibrateOnEdgeCollision(abs(getTrackedVelocity()))
hasVibratedAtUpperBookend = true
}
}
override fun onProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
- velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
- vibrateDragTexture(abs(velocityTracker.xVelocity), progress)
+ vibrateDragTexture(abs(getTrackedVelocity()), progress)
hasVibratedAtUpperBookend = false
hasVibratedAtLowerBookend = false
}
+ private fun getTrackedVelocity(): Float {
+ velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
+ return if (velocityTracker.isAxisSupported(config.velocityAxis)) {
+ velocityTracker.getAxisVelocity(config.velocityAxis)
+ } else {
+ 0f
+ }
+ }
+
override fun onProgressJump(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
override fun onSelectAndArrow(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index af5d48d..afef875 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -39,12 +39,14 @@
import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
import com.android.systemui.keyguard.ui.view.KeyguardRootView
import com.android.systemui.keyguard.ui.view.layout.KeyguardBlueprintCommandListener
+import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.NotificationShadeWindowView
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.KeyguardIndicationController
@@ -83,6 +85,7 @@
private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor,
private val vibratorHelper: VibratorHelper,
private val falsingManager: FalsingManager,
+ private val aodAlphaViewModel: AodAlphaViewModel,
) : CoreStartable {
private var rootViewHandle: DisposableHandle? = null
@@ -109,7 +112,9 @@
bindKeyguardRootView()
initializeViews()
- KeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel)
+ if (!SceneContainerFlag.isEnabled) {
+ KeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel)
+ }
keyguardBlueprintCommandListener.start()
}
@@ -126,7 +131,7 @@
KeyguardIndicationAreaBinder.bind(
notificationShadeWindowView.requireViewById(R.id.keyguard_indication_area),
keyguardIndicationAreaViewModel,
- keyguardRootViewModel,
+ aodAlphaViewModel,
indicationController,
)
}
@@ -142,13 +147,16 @@
}
private fun bindKeyguardRootView() {
+ if (SceneContainerFlag.isEnabled) {
+ return
+ }
+
rootViewHandle?.dispose()
rootViewHandle =
KeyguardRootViewBinder.bind(
keyguardRootView,
keyguardRootViewModel,
configuration,
- featureFlags,
occludingAppDeviceEntryMessageViewModel,
chipbarCoordinator,
screenOffAnimationController,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
new file mode 100644
index 0000000..70c2e6d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.app.animation.Interpolators
+import com.android.systemui.Flags
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+
+@SysUISingleton
+class FromGlanceableHubTransitionInteractor
+@Inject
+constructor(
+ override val transitionRepository: KeyguardTransitionRepository,
+ override val transitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor(fromState = KeyguardState.GLANCEABLE_HUB) {
+ override fun start() {
+ if (!Flags.communalHub()) {
+ return
+ }
+ }
+
+ override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
+ return ValueAnimator().apply {
+ interpolator = Interpolators.LINEAR
+ duration = DEFAULT_DURATION.inWholeMilliseconds
+ }
+ }
+
+ companion object {
+ const val TAG = "FromGlanceableHubTransitionInteractor"
+ val DEFAULT_DURATION = 500.milliseconds
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index ba7b987..91f8420 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -42,6 +42,7 @@
is FromGoneTransitionInteractor -> Log.d(TAG, "Started $it")
is FromLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
is FromDreamingTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromGlanceableHubTransitionInteractor -> Log.d(TAG, "Started $it")
is FromOccludedTransitionInteractor -> Log.d(TAG, "Started $it")
is FromDozingTransitionInteractor -> Log.d(TAG, "Started $it")
is FromAlternateBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 2d43897..fbf6936 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -96,6 +96,7 @@
KeyguardState.AOD -> false
KeyguardState.DREAMING -> true
KeyguardState.DREAMING_LOCKSCREEN_HOSTED -> true
+ KeyguardState.GLANCEABLE_HUB -> true
KeyguardState.ALTERNATE_BOUNCER -> true
KeyguardState.PRIMARY_BOUNCER -> true
KeyguardState.LOCKSCREEN -> true
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
index 56f5529..d95c38e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
@@ -67,4 +67,10 @@
abstract fun fromAlternateBouncer(
impl: FromAlternateBouncerTransitionInteractor
): TransitionInteractor
+
+ @Binds
+ @IntoSet
+ abstract fun fromGlanceableHub(
+ impl: FromGlanceableHubTransitionInteractor
+ ): TransitionInteractor
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
index f5bcab9..92612b8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -62,6 +62,12 @@
* unlocked if SWIPE security method is used, or if face lockscreen bypass is false.
*/
LOCKSCREEN,
+ /**
+ * Device is locked or on dream and user has swiped from the right edge to enter the glanceable
+ * hub UI. From this state, the user can swipe from the left edge to go back to the lock screen
+ * or dream, as well as swipe down for the notifications and up for the bouncer.
+ */
+ GLANCEABLE_HUB,
/*
* Keyguard is no longer visible. In most cases the user has just authenticated and keyguard
* is being removed, but there are other cases where the user is swiping away keyguard, such as
@@ -95,6 +101,7 @@
DOZING -> false
DREAMING -> false
DREAMING_LOCKSCREEN_HOSTED -> false
+ GLANCEABLE_HUB -> true
AOD -> false
ALTERNATE_BOUNCER -> true
PRIMARY_BOUNCER -> true
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 1277585..cf1d247 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -32,6 +32,7 @@
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
@@ -89,7 +90,6 @@
val start = (startTime / transitionDuration).toFloat()
val chunks = (transitionDuration / duration).toFloat()
logger.logCreate(name, start)
- var isComplete = true
fun stepToValue(step: TransitionStep): Float? {
val value = (step.value - start) * chunks
@@ -98,17 +98,13 @@
// middle, it is possible this animation is being skipped but we need to inform
// the ViewModels of the last update
STARTED -> {
- isComplete = false
onStart?.invoke()
max(0f, min(1f, value))
}
// Always send a final value of 1. Because of rounding, [value] may never be
// exactly 1.
RUNNING ->
- if (isComplete) {
- null
- } else if (value >= 1f) {
- isComplete = true
+ if (value >= 1f) {
1f
} else if (value >= 0f) {
value
@@ -132,6 +128,7 @@
value
}
.filterNotNull()
+ .distinctUntilChanged()
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
index 4c33d90..7c1368a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
@@ -23,8 +23,8 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.Flags.keyguardBottomAreaRefactor
+import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
import com.android.systemui.statusbar.KeyguardIndicationController
@@ -51,7 +51,7 @@
fun bind(
view: ViewGroup,
viewModel: KeyguardIndicationAreaViewModel,
- keyguardRootViewModel: KeyguardRootViewModel,
+ aodAlphaViewModel: AodAlphaViewModel,
indicationController: KeyguardIndicationController,
): DisposableHandle {
indicationController.setIndicationArea(view)
@@ -69,7 +69,7 @@
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
if (keyguardBottomAreaRefactor()) {
- keyguardRootViewModel.alpha.collect { alpha ->
+ aodAlphaViewModel.alpha.collect { alpha ->
view.apply {
this.importantForAccessibility =
if (alpha == 0f) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index fad0370..2aebd99 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -42,9 +42,9 @@
import com.android.systemui.common.shared.model.TintedIcon
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
-import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
@@ -68,7 +68,10 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/** Bind occludingAppDeviceEntryMessageViewModel to run whenever the keyguard view is attached. */
@@ -81,7 +84,6 @@
view: ViewGroup,
viewModel: KeyguardRootViewModel,
configuration: ConfigurationState,
- featureFlags: FeatureFlagsClassic,
occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
chipbarCoordinator: ChipbarCoordinator,
screenOffAnimationController: ScreenOffAnimationController,
@@ -108,6 +110,8 @@
}
}
+ val burnInParams = MutableStateFlow(BurnInParameters())
+
val disposableHandle =
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {
@@ -164,35 +168,41 @@
// large clock isn't added to burnInLayer due to its scale transition
// so we also need to add translation to it here
// same as translationX
- viewModel.translationY.collect { y ->
- childViews[burnInLayerId]?.translationY = y
- childViews[largeClockId]?.translationY = y
- }
- }
-
- launch {
- viewModel.translationX.collect { x ->
- childViews[burnInLayerId]?.translationX = x
- childViews[largeClockId]?.translationX = x
- }
- }
-
- launch {
- viewModel.scale.collect { (scale, scaleClockOnly) ->
- if (scaleClockOnly) {
- // For clocks except weather clock, we have scale transition
- // besides translate
- childViews[largeClockId]?.let {
- it.scaleX = scale
- it.scaleY = scale
- }
- } else {
- // For weather clock, large clock should have only scale
- // transition with other parts in burnInLayer
- childViews[burnInLayerId]?.scaleX = scale
- childViews[burnInLayerId]?.scaleY = scale
+ burnInParams
+ .flatMapLatest { params -> viewModel.translationY(params) }
+ .collect { y ->
+ childViews[burnInLayerId]?.translationY = y
+ childViews[largeClockId]?.translationY = y
}
- }
+ }
+
+ launch {
+ burnInParams
+ .flatMapLatest { params -> viewModel.translationX(params) }
+ .collect { x ->
+ childViews[burnInLayerId]?.translationX = x
+ childViews[largeClockId]?.translationX = x
+ }
+ }
+
+ launch {
+ burnInParams
+ .flatMapLatest { params -> viewModel.scale(params) }
+ .collect { scaleViewModel ->
+ if (scaleViewModel.scaleClockOnly) {
+ // For clocks except weather clock, we have scale transition
+ // besides translate
+ childViews[largeClockId]?.let {
+ it.scaleX = scaleViewModel.scale
+ it.scaleY = scaleViewModel.scale
+ }
+ } else {
+ // For weather clock, large clock should have only scale
+ // transition with other parts in burnInLayer
+ childViews[burnInLayerId]?.scaleX = scaleViewModel.scale
+ childViews[burnInLayerId]?.scaleY = scaleViewModel.scale
+ }
+ }
}
if (NotificationIconContainerRefactor.isEnabled) {
@@ -274,10 +284,12 @@
}
if (!migrateClocksToBlueprint()) {
- viewModel.clockControllerProvider = clockControllerProvider
+ burnInParams.update { current ->
+ current.copy(clockControllerProvider = clockControllerProvider)
+ }
}
- onLayoutChangeListener = OnLayoutChange(viewModel)
+ onLayoutChangeListener = OnLayoutChange(viewModel, burnInParams)
view.addOnLayoutChangeListener(onLayoutChangeListener)
// Views will be added or removed after the call to bind(). This is needed to avoid many
@@ -296,7 +308,9 @@
view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets ->
val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
- viewModel.topInset = insets.getInsetsIgnoringVisibility(insetTypes).top
+ burnInParams.update { current ->
+ current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top)
+ }
insets
}
@@ -333,8 +347,10 @@
)
}
- private class OnLayoutChange(private val viewModel: KeyguardRootViewModel) :
- OnLayoutChangeListener {
+ private class OnLayoutChange(
+ private val viewModel: KeyguardRootViewModel,
+ private val burnInParams: MutableStateFlow<BurnInParameters>,
+ ) : OnLayoutChangeListener {
override fun onLayoutChange(
view: View,
left: Int,
@@ -355,7 +371,7 @@
}
view.findViewById<View>(R.id.keyguard_status_view)?.let { statusView ->
- viewModel.statusViewTop = statusView.top
+ burnInParams.update { current -> current.copy(statusViewTop = statusView.top) }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 03e45fd..eb3afb7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -367,7 +367,6 @@
keyguardRootView,
keyguardRootViewModel,
configuration,
- featureFlags,
occludingAppDeviceEntryMessageViewModel,
chipbarCoordinator,
screenOffAnimationController,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
index 66c137f..ea05c1d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
@@ -25,8 +25,8 @@
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
+import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.res.R
import com.android.systemui.statusbar.KeyguardIndicationController
import javax.inject.Inject
@@ -37,7 +37,7 @@
constructor(
private val context: Context,
private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel,
- private val keyguardRootViewModel: KeyguardRootViewModel,
+ private val aodAlphaViewModel: AodAlphaViewModel,
private val indicationController: KeyguardIndicationController,
) : KeyguardSection() {
private val indicationAreaViewId = R.id.keyguard_indication_area
@@ -56,7 +56,7 @@
KeyguardIndicationAreaBinder.bind(
constraintLayout.requireViewById(R.id.keyguard_indication_area),
keyguardIndicationAreaViewModel,
- keyguardRootViewModel,
+ aodAlphaViewModel,
indicationController,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
new file mode 100644
index 0000000..d4ea728
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
+
+/** Models UI state for the alpha of the AOD (always-on display). */
+@SysUISingleton
+class AodAlphaViewModel
+@Inject
+constructor(
+ keyguardInteractor: KeyguardInteractor,
+ keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+) {
+
+ /** The alpha level for the entire lockscreen while in AOD. */
+ val alpha: Flow<Float> =
+ combine(
+ keyguardTransitionInteractor.transitionValue(KeyguardState.GONE).onStart {
+ emit(0f)
+ },
+ merge(
+ keyguardInteractor.keyguardAlpha,
+ occludedToLockscreenTransitionViewModel.lockscreenAlpha,
+ )
+ ) { transitionToGone, alpha ->
+ if (transitionToGone == 1f) {
+ // Ensures content is not visible when in GONE state
+ 0f
+ } else {
+ alpha
+ }
+ }
+ .distinctUntilChanged()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
new file mode 100644
index 0000000..780e323
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.util.MathUtils
+import com.android.app.animation.Interpolators
+import com.android.keyguard.KeyguardClockSwitch
+import com.android.systemui.Flags
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.BurnInModel
+import com.android.systemui.plugins.clocks.ClockController
+import com.android.systemui.res.R
+import javax.inject.Inject
+import javax.inject.Provider
+import kotlin.math.max
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
+
+/**
+ * Models UI state for elements that need to apply anti-burn-in tactics when showing in AOD
+ * (always-on display).
+ */
+@SysUISingleton
+class AodBurnInViewModel
+@Inject
+constructor(
+ private val burnInInteractor: BurnInInteractor,
+ private val configurationInteractor: ConfigurationInteractor,
+ private val keyguardInteractor: KeyguardInteractor,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
+ private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+ private val keyguardClockViewModel: KeyguardClockViewModel,
+) {
+ /** Alpha for elements that appear and move during the animation -> AOD */
+ val alpha: Flow<Float> = goneToAodTransitionViewModel.enterFromTopAnimationAlpha
+
+ /** Horizontal translation for elements that need to apply anti-burn-in tactics. */
+ fun translationX(
+ params: BurnInParameters,
+ ): Flow<Float> {
+ return burnIn(params).map { it.translationX.toFloat() }
+ }
+
+ /** Vertical translation for elements that need to apply anti-burn-in tactics. */
+ fun translationY(
+ params: BurnInParameters,
+ ): Flow<Float> {
+ return configurationInteractor
+ .dimensionPixelSize(R.dimen.keyguard_enter_from_top_translation_y)
+ .flatMapLatest { enterFromTopAmount ->
+ combine(
+ keyguardInteractor.keyguardTranslationY.onStart { emit(0f) },
+ burnIn(params).map { it.translationY.toFloat() }.onStart { emit(0f) },
+ goneToAodTransitionViewModel
+ .enterFromTopTranslationY(enterFromTopAmount)
+ .onStart { emit(0f) },
+ occludedToLockscreenTransitionViewModel.lockscreenTranslationY.onStart {
+ emit(0f)
+ },
+ ) {
+ keyguardTransitionY,
+ burnInTranslationY,
+ goneToAodTransitionTranslationY,
+ occludedToLockscreenTransitionTranslationY ->
+
+ // All values need to be combined for a smooth translation
+ keyguardTransitionY +
+ burnInTranslationY +
+ goneToAodTransitionTranslationY +
+ occludedToLockscreenTransitionTranslationY
+ }
+ }
+ .distinctUntilChanged()
+ }
+
+ /** Scale for elements that need to apply anti-burn-in tactics. */
+ fun scale(
+ params: BurnInParameters,
+ ): Flow<BurnInScaleViewModel> {
+ return burnIn(params).map {
+ BurnInScaleViewModel(
+ scale = it.scale,
+ scaleClockOnly = it.scaleClockOnly,
+ )
+ }
+ }
+
+ private fun burnIn(
+ params: BurnInParameters,
+ ): Flow<BurnInModel> {
+ return combine(
+ merge(
+ keyguardTransitionInteractor.goneToAodTransition.map { it.value },
+ keyguardTransitionInteractor.dozeAmountTransition.map { it.value },
+ )
+ .map { dozeAmount -> Interpolators.FAST_OUT_SLOW_IN.getInterpolation(dozeAmount) },
+ burnInInteractor.keyguardBurnIn,
+ ) { interpolated, burnIn ->
+ val useScaleOnly =
+ (clockController(params.clockControllerProvider)
+ ?.get()
+ ?.config
+ ?.useAlternateSmartspaceAODTransition
+ ?: false) && keyguardClockViewModel.clockSize.value == KeyguardClockSwitch.LARGE
+
+ if (useScaleOnly) {
+ BurnInModel(
+ translationX = 0,
+ translationY = 0,
+ scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolated),
+ )
+ } else {
+ // Ensure the desired translation doesn't encroach on the top inset
+ val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt()
+ val translationY =
+ if (Flags.migrateClocksToBlueprint()) {
+ burnInY
+ } else {
+ max(params.topInset, params.statusViewTop + burnInY) - params.statusViewTop
+ }
+
+ BurnInModel(
+ translationX = MathUtils.lerp(0, burnIn.translationX, interpolated).toInt(),
+ translationY = translationY,
+ scale =
+ MathUtils.lerp(
+ /* start= */ burnIn.scale,
+ /* stop= */ 1f,
+ /* amount= */ 1f - interpolated,
+ ),
+ scaleClockOnly = true,
+ )
+ }
+ }
+ }
+
+ private fun clockController(
+ provider: Provider<ClockController>?,
+ ): Provider<ClockController>? {
+ return if (Flags.migrateClocksToBlueprint()) {
+ Provider { keyguardClockViewModel.clock }
+ } else {
+ provider
+ }
+ }
+}
+
+/** UI-sourced parameters to pass into the various methods of [AodBurnInViewModel]. */
+data class BurnInParameters(
+ val clockControllerProvider: Provider<ClockController>? = null,
+ /** System insets that keyguard needs to stay out of */
+ val topInset: Int = 0,
+ /** Status view top, without translation added in */
+ val statusViewTop: Int = 0,
+)
+
+/**
+ * Models UI state of the scaling to apply to elements that need to be scaled for anti-burn-in
+ * purposes.
+ */
+data class BurnInScaleViewModel(
+ val scale: Float = 1f,
+ /** Whether the scale only applies to clock UI elements. */
+ val scaleClockOnly: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index 528a2ee..5bb2782 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.content.Context
import androidx.constraintlayout.helper.widget.Layer
import com.android.keyguard.KeyguardClockSwitch.LARGE
import com.android.keyguard.KeyguardClockSwitch.SMALL
@@ -25,6 +26,9 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.shared.model.SettingsClockSize
import com.android.systemui.plugins.clocks.ClockController
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.SplitShadeStateController
+import com.android.systemui.util.Utils
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
@@ -36,9 +40,10 @@
class KeyguardClockViewModel
@Inject
constructor(
- val keyguardInteractor: KeyguardInteractor,
- val keyguardClockInteractor: KeyguardClockInteractor,
+ keyguardInteractor: KeyguardInteractor,
+ private val keyguardClockInteractor: KeyguardClockInteractor,
@Application private val applicationScope: CoroutineScope,
+ private val splitShadeStateController: SplitShadeStateController,
) {
var burnInLayer: Layer? = null
val useLargeClock: Boolean
@@ -85,4 +90,43 @@
started = SharingStarted.WhileSubscribed(),
initialValue = false
)
+
+ // Needs to use a non application context to get display cutout.
+ fun getSmallClockTopMargin(context: Context) =
+ if (splitShadeStateController.shouldUseSplitNotificationShade(context.resources)) {
+ context.resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin)
+ } else {
+ context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
+ Utils.getStatusBarHeaderHeightKeyguard(context)
+ }
+
+ fun getLargeClockTopMargin(context: Context): Int {
+ var largeClockTopMargin =
+ context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
+ context.resources.getDimensionPixelSize(
+ com.android.systemui.customization.R.dimen.small_clock_padding_top
+ ) +
+ context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset)
+ largeClockTopMargin += getDimen(context, DATE_WEATHER_VIEW_HEIGHT)
+ largeClockTopMargin += getDimen(context, ENHANCED_SMARTSPACE_HEIGHT)
+ if (!useLargeClock) {
+ largeClockTopMargin -=
+ context.resources.getDimensionPixelSize(
+ com.android.systemui.customization.R.dimen.small_clock_height
+ )
+ }
+
+ return largeClockTopMargin
+ }
+
+ private fun getDimen(context: Context, name: String): Int {
+ val res = context.packageManager.getResourcesForApplication(context.packageName)
+ val id = res.getIdentifier(name, "dimen", context.packageName)
+ return res.getDimensionPixelSize(id)
+ }
+
+ companion object {
+ private const val DATE_WEATHER_VIEW_HEIGHT = "date_weather_view_height"
+ private const val ENHANCED_SMARTSPACE_HEIGHT = "enhanced_smartspace_height"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 26dace0..5059e6b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -18,27 +18,17 @@
package com.android.systemui.keyguard.ui.viewmodel
import android.graphics.Point
-import android.util.MathUtils
import android.view.View.VISIBLE
-import com.android.app.animation.Interpolators
-import com.android.keyguard.KeyguardClockSwitch.LARGE
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.Flags.newAodTransition
import com.android.systemui.common.shared.model.NotificationContainerBounds
-import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.BurnInModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
-import com.android.systemui.plugins.clocks.ClockController
-import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
@@ -49,51 +39,29 @@
import com.android.systemui.util.ui.toAnimatedValueFlow
import com.android.systemui.util.ui.zip
import javax.inject.Inject
-import javax.inject.Provider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onStart
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class KeyguardRootViewModel
@Inject
constructor(
- configurationInteractor: ConfigurationInteractor,
private val deviceEntryInteractor: DeviceEntryInteractor,
private val dozeParameters: DozeParameters,
private val keyguardInteractor: KeyguardInteractor,
- private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
- private val burnInInteractor: BurnInInteractor,
- private val keyguardClockViewModel: KeyguardClockViewModel,
- private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
- private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
- private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+ aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
screenOffAnimationController: ScreenOffAnimationController,
- // TODO(b/310989341): remove after changing migrate_clocks_to_blueprint to aconfig
- private val featureFlags: FeatureFlagsClassic,
+ private val aodBurnInViewModel: AodBurnInViewModel,
+ aodAlphaViewModel: AodAlphaViewModel,
) {
- var clockControllerProvider: Provider<ClockController>? = null
- get() {
- if (migrateClocksToBlueprint()) {
- return Provider { keyguardClockViewModel.clock }
- } else {
- return field
- }
- }
-
- /** System insets that keyguard needs to stay out of */
- var topInset: Int = 0
- /** Status view top, without translation added in */
- var statusViewTop: Int = 0
val burnInLayerVisibility: Flow<Int> =
keyguardTransitionInteractor.startedKeyguardState
@@ -110,96 +78,25 @@
keyguardInteractor.notificationContainerBounds
/** An observable for the alpha level for the entire keyguard root view. */
- val alpha: Flow<Float> =
- combine(
- keyguardTransitionInteractor.transitionValue(GONE).onStart { emit(0f) },
- merge(
- keyguardInteractor.keyguardAlpha,
- occludedToLockscreenTransitionViewModel.lockscreenAlpha,
- )
- ) { transitionToGone, alpha ->
- if (transitionToGone == 1f) {
- // Ensures content is not visible when in GONE state
- 0f
- } else {
- alpha
- }
- }
- .distinctUntilChanged()
-
- private fun burnIn(): Flow<BurnInModel> {
- val dozingAmount: Flow<Float> =
- merge(
- keyguardTransitionInteractor.goneToAodTransition.map { it.value },
- keyguardTransitionInteractor.dozeAmountTransition.map { it.value },
- )
-
- return combine(dozingAmount, burnInInteractor.keyguardBurnIn) { dozeAmount, burnIn ->
- val interpolation = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(dozeAmount)
- val useScaleOnly =
- (clockControllerProvider?.get()?.config?.useAlternateSmartspaceAODTransition
- ?: false) && keyguardClockViewModel.clockSize.value == LARGE
- if (useScaleOnly) {
- BurnInModel(
- translationX = 0,
- translationY = 0,
- scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolation),
- )
- } else {
- // Ensure the desired translation doesn't encroach on the top inset
- val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolation).toInt()
- val translationY =
- if (migrateClocksToBlueprint()) {
- burnInY
- } else {
- -(statusViewTop - Math.max(topInset, statusViewTop + burnInY))
- }
- BurnInModel(
- translationX = MathUtils.lerp(0, burnIn.translationX, interpolation).toInt(),
- translationY = translationY,
- scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolation),
- scaleClockOnly = true,
- )
- }
- }
- }
+ val alpha: Flow<Float> = aodAlphaViewModel.alpha
/** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */
val lockscreenStateAlpha: Flow<Float> = aodToLockscreenTransitionViewModel.lockscreenAlpha
/** For elements that appear and move during the animation -> AOD */
- val burnInLayerAlpha: Flow<Float> = goneToAodTransitionViewModel.enterFromTopAnimationAlpha
+ val burnInLayerAlpha: Flow<Float> = aodBurnInViewModel.alpha
- val translationY: Flow<Float> =
- configurationInteractor
- .dimensionPixelSize(R.dimen.keyguard_enter_from_top_translation_y)
- .flatMapLatest { enterFromTopAmount ->
- combine(
- keyguardInteractor.keyguardTranslationY.onStart { emit(0f) },
- burnIn().map { it.translationY.toFloat() }.onStart { emit(0f) },
- goneToAodTransitionViewModel
- .enterFromTopTranslationY(enterFromTopAmount)
- .onStart { emit(0f) },
- occludedToLockscreenTransitionViewModel.lockscreenTranslationY.onStart {
- emit(0f)
- },
- ) {
- keyguardTransitionY,
- burnInTranslationY,
- goneToAodTransitionTranslationY,
- occludedToLockscreenTransitionTranslationY ->
- // All values need to be combined for a smooth translation
- keyguardTransitionY +
- burnInTranslationY +
- goneToAodTransitionTranslationY +
- occludedToLockscreenTransitionTranslationY
- }
- }
- .distinctUntilChanged()
+ fun translationY(params: BurnInParameters): Flow<Float> {
+ return aodBurnInViewModel.translationY(params)
+ }
- val translationX: Flow<Float> = burnIn().map { it.translationX.toFloat() }
+ fun translationX(params: BurnInParameters): Flow<Float> {
+ return aodBurnInViewModel.translationX(params)
+ }
- val scale: Flow<Pair<Float, Boolean>> = burnIn().map { Pair(it.scale, it.scaleClockOnly) }
+ fun scale(params: BurnInParameters): Flow<BurnInScaleViewModel> {
+ return aodBurnInViewModel.scale(params)
+ }
/** Is the notification icon container visible? */
val isNotifIconContainerVisible: Flow<AnimatedValue<Boolean>> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
index 539db7f..2b28a71 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
@@ -38,7 +38,6 @@
deviceEntryInteractor: DeviceEntryInteractor,
communalInteractor: CommunalInteractor,
val longPress: KeyguardLongPressViewModel,
- val keyguardRoot: KeyguardRootViewModel,
val notifications: NotificationsPlaceholderViewModel,
) {
/** The key of the scene we should switch to when swiping up. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
index 1dbf1f1..693e3b7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
@@ -28,13 +28,16 @@
import com.android.systemui.biometrics.shared.model.isDefaultOrientation
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.DozeServiceHost
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
@@ -43,6 +46,7 @@
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
@@ -54,9 +58,13 @@
@Inject
constructor(
private val context: Context,
- private val fpAuthRepository: DeviceEntryFingerprintAuthRepository,
+ private val fpAuthRepository: DeviceEntryFingerprintAuthInteractor,
private val sfpsSensorInteractor: SideFpsSensorInteractor,
+ // todo (b/317432075) Injecting DozeServiceHost directly instead of using it through
+ // DozeInteractor as DozeServiceHost already depends on DozeInteractor.
+ private val dozeServiceHost: DozeServiceHost,
displayStateInteractor: DisplayStateInteractor,
+ @Main private val mainDispatcher: CoroutineDispatcher,
@Application private val applicationScope: CoroutineScope,
) {
private val _progress = MutableStateFlow(0.0f)
@@ -168,18 +176,21 @@
return@collectLatest
}
animatorJob =
- fpAuthRepository.authenticationStatus
- .onEach { authStatus ->
+ combine(
+ sfpsSensorInteractor.authenticationDuration,
+ fpAuthRepository.authenticationStatus,
+ ::Pair
+ )
+ .onEach { (authDuration, authStatus) ->
when (authStatus) {
is AcquiredFingerprintAuthenticationStatus -> {
if (authStatus.fingerprintCaptureStarted) {
_visible.value = true
+ dozeServiceHost.fireSideFpsAcquisitionStarted()
_animator?.cancel()
_animator =
ValueAnimator.ofFloat(0.0f, 1.0f)
- .setDuration(
- sfpsSensorInteractor.authenticationDuration
- )
+ .setDuration(authDuration)
.apply {
addUpdateListener {
_progress.value = it.animatedValue as Float
@@ -209,6 +220,7 @@
else -> Unit
}
}
+ .flowOn(mainDispatcher)
.onCompletion { _animator?.cancel() }
.launchIn(applicationScope)
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt
index 919072a..171656a 100644
--- a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt
@@ -108,4 +108,13 @@
}
)
}
+
+ fun authDurationChanged(duration: Long) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { long1 = duration },
+ { "SideFpsSensor auth duration changed: $long1" }
+ )
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
index a6c6233..7e06f5a 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
@@ -87,6 +87,7 @@
import java.util.Objects;
import javax.inject.Inject;
+import javax.inject.Provider;
/**
*/
@@ -149,6 +150,7 @@
public static final String EXTRA_CONFIRM_ONLY = "extra_confirm_only";
private final Context mContext;
+ private final SystemUIDialog.Factory mSystemUIDialogFactory;
private final NotificationManager mNoMan;
private final PowerManager mPowerMan;
private final KeyguardManager mKeyguard;
@@ -186,11 +188,17 @@
/**
*/
@Inject
- public PowerNotificationWarnings(Context context, ActivityStarter activityStarter,
- BroadcastSender broadcastSender, Lazy<BatteryController> batteryControllerLazy,
- DialogLaunchAnimator dialogLaunchAnimator, UiEventLogger uiEventLogger,
- GlobalSettings globalSettings, UserTracker userTracker) {
+ public PowerNotificationWarnings(
+ Context context,
+ ActivityStarter activityStarter,
+ BroadcastSender broadcastSender,
+ Lazy<BatteryController> batteryControllerLazy,
+ DialogLaunchAnimator dialogLaunchAnimator,
+ UiEventLogger uiEventLogger,
+ UserTracker userTracker,
+ SystemUIDialog.Factory systemUIDialogFactory) {
mContext = context;
+ mSystemUIDialogFactory = systemUIDialogFactory;
mNoMan = mContext.getSystemService(NotificationManager.class);
mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mKeyguard = mContext.getSystemService(KeyguardManager.class);
@@ -444,7 +452,7 @@
private void showHighTemperatureDialog() {
if (mHighTempDialog != null) return;
- final SystemUIDialog d = new SystemUIDialog(mContext);
+ final SystemUIDialog d = mSystemUIDialogFactory.create();
d.setIconAttribute(android.R.attr.alertDialogIcon);
d.setTitle(R.string.high_temp_title);
d.setMessage(R.string.high_temp_dialog_message);
@@ -479,7 +487,7 @@
private void showThermalShutdownDialog() {
if (mThermalShutdownDialog != null) return;
- final SystemUIDialog d = new SystemUIDialog(mContext);
+ final SystemUIDialog d = mSystemUIDialogFactory.create();
d.setIconAttribute(android.R.attr.alertDialogIcon);
d.setTitle(R.string.thermal_shutdown_title);
d.setMessage(R.string.thermal_shutdown_dialog_message);
@@ -643,7 +651,7 @@
private void showStartSaverConfirmation(Bundle extras) {
if (mSaverConfirmation != null || mUseExtraSaverConfirmation) return;
- final SystemUIDialog d = new SystemUIDialog(mContext);
+ final SystemUIDialog d = mSystemUIDialogFactory.create();
final boolean confirmOnly = extras.getBoolean(BatterySaverUtils.EXTRA_CONFIRM_TEXT_ONLY);
final int batterySaverTriggerMode =
extras.getInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 6f35cfb..b5def41 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -148,7 +148,8 @@
private val deviceConfigProxy: DeviceConfigProxy,
private val dialogLaunchAnimator: DialogLaunchAnimator,
private val broadcastDispatcher: BroadcastDispatcher,
- private val dumpManager: DumpManager
+ private val dumpManager: DumpManager,
+ private val systemUIDialogFactory: SystemUIDialog.Factory,
) : Dumpable, FgsManagerController {
companion object {
@@ -375,7 +376,7 @@
override fun showDialog(expandable: Expandable?) {
synchronized(lock) {
if (dialog == null) {
- val dialog = SystemUIDialog(context)
+ val dialog = systemUIDialogFactory.create()
dialog.setTitle(R.string.fgs_manager_dialog_title)
dialog.setMessage(R.string.fgs_manager_dialog_message)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
index ccf7afb..c9b0022 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
@@ -55,6 +55,7 @@
private final DataSaverController mDataSaverController;
private final DialogLaunchAnimator mDialogLaunchAnimator;
+ private final SystemUIDialog.Factory mSystemUIDialogFactory;
@Inject
public DataSaverTile(
@@ -68,12 +69,14 @@
ActivityStarter activityStarter,
QSLogger qsLogger,
DataSaverController dataSaverController,
- DialogLaunchAnimator dialogLaunchAnimator
+ DialogLaunchAnimator dialogLaunchAnimator,
+ SystemUIDialog.Factory systemUIDialogFactory
) {
super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger);
mDataSaverController = dataSaverController;
mDialogLaunchAnimator = dialogLaunchAnimator;
+ mSystemUIDialogFactory = systemUIDialogFactory;
mDataSaverController.observe(getLifecycle(), this);
}
@@ -98,7 +101,7 @@
// Show a dialog to confirm first. Dialogs shown by the DialogLaunchAnimator must be created
// and shown on the main thread, so we post it to the UI handler.
mUiHandler.post(() -> {
- SystemUIDialog dialog = new SystemUIDialog(mContext);
+ SystemUIDialog dialog = mSystemUIDialogFactory.create();
dialog.setTitle(com.android.internal.R.string.data_saver_enable_title);
dialog.setMessage(com.android.internal.R.string.data_saver_description);
dialog.setPositiveButton(com.android.internal.R.string.data_saver_enable_button,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
index acd7510..41cd221 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
@@ -23,7 +23,6 @@
import android.content.Intent
import android.provider.Settings
import android.view.LayoutInflater
-import androidx.annotation.VisibleForTesting
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.UiEventLogger
import com.android.systemui.res.R
@@ -44,31 +43,15 @@
* Controller for [UserDialog].
*/
@SysUISingleton
-class UserSwitchDialogController @VisibleForTesting constructor(
- private val userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
- private val activityStarter: ActivityStarter,
- private val falsingManager: FalsingManager,
- private val dialogLaunchAnimator: DialogLaunchAnimator,
- private val uiEventLogger: UiEventLogger,
- private val dialogFactory: (Context) -> SystemUIDialog
+class UserSwitchDialogController @Inject constructor(
+ private val userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
+ private val activityStarter: ActivityStarter,
+ private val falsingManager: FalsingManager,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val uiEventLogger: UiEventLogger,
+ private val dialogFactory: SystemUIDialog.Factory
) {
- @Inject
- constructor(
- userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
- activityStarter: ActivityStarter,
- falsingManager: FalsingManager,
- dialogLaunchAnimator: DialogLaunchAnimator,
- uiEventLogger: UiEventLogger
- ) : this(
- userDetailViewAdapterProvider,
- activityStarter,
- falsingManager,
- dialogLaunchAnimator,
- uiEventLogger,
- { SystemUIDialog(it) }
- )
-
companion object {
private const val INTERACTION_JANK_TAG = "switch_user"
private val USER_SETTINGS_INTENT = Intent(Settings.ACTION_USER_SETTINGS)
@@ -81,7 +64,7 @@
* [userDetailViewAdapterProvider] and show it as launched from [expandable].
*/
fun showDialog(context: Context, expandable: Expandable) {
- with(dialogFactory(context)) {
+ with(dialogFactory.create()) {
setShowForAllUsers(true)
setCanceledOnTouchOutside(true)
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
index f071623..9076182 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
@@ -21,8 +21,10 @@
import android.annotation.TestApi;
import android.content.Context;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManagerGlobal;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.LinearLayout;
@@ -72,20 +74,27 @@
private DeviceStateManager.DeviceStateCallback mDeviceStateManagerCallback =
new DeviceStateManagerCallback();
- private final Context mContext;
private final CommandQueue mCommandQueue;
private final Executor mExecutor;
+ private final Resources mResources;
+ private final LayoutInflater mLayoutInflater;
+ private final SystemUIDialog.Factory mSystemUIDialogFactory;
- @VisibleForTesting
- SystemUIDialog mRearDisplayEducationDialog;
+ private SystemUIDialog mRearDisplayEducationDialog;
@Nullable LinearLayout mDialogViewContainer;
@Inject
- public RearDisplayDialogController(Context context, CommandQueue commandQueue,
- @Main Executor executor) {
- mContext = context;
+ public RearDisplayDialogController(
+ CommandQueue commandQueue,
+ @Main Executor executor,
+ @Main Resources resources,
+ LayoutInflater layoutInflater,
+ SystemUIDialog.Factory systemUIDialogFactory) {
mCommandQueue = commandQueue;
mExecutor = executor;
+ mResources = resources;
+ mLayoutInflater = layoutInflater;
+ mSystemUIDialogFactory = systemUIDialogFactory;
}
@Override
@@ -104,8 +113,7 @@
if (mRearDisplayEducationDialog != null && mRearDisplayEducationDialog.isShowing()
&& mDialogViewContainer != null) {
// Refresh the dialog view when configuration is changed.
- Context dialogContext = mRearDisplayEducationDialog.getContext();
- View dialogView = createDialogView(dialogContext);
+ View dialogView = createDialogView(mRearDisplayEducationDialog.getContext());
mDialogViewContainer.removeAllViews();
mDialogViewContainer.addView(dialogView);
}
@@ -114,9 +122,7 @@
private void createAndShowDialog() {
mServiceNotified = false;
Context dialogContext = mRearDisplayEducationDialog.getContext();
-
View dialogView = createDialogView(dialogContext);
-
mDialogViewContainer = new LinearLayout(dialogContext);
mDialogViewContainer.setLayoutParams(
new LinearLayout.LayoutParams(
@@ -133,11 +139,11 @@
private View createDialogView(Context context) {
View dialogView;
+ LayoutInflater inflater = mLayoutInflater.cloneInContext(context);
if (mStartedFolded) {
- dialogView = View.inflate(context,
- R.layout.activity_rear_display_education, null);
+ dialogView = inflater.inflate(R.layout.activity_rear_display_education, null);
} else {
- dialogView = View.inflate(context,
+ dialogView = inflater.inflate(
R.layout.activity_rear_display_education_opened, null);
}
LottieAnimationView animationView = dialogView.findViewById(
@@ -172,9 +178,9 @@
* Ensures we're not using old values from when the dialog may have been shown previously.
*/
private void initializeValues(int startingBaseState) {
- mRearDisplayEducationDialog = new SystemUIDialog(mContext);
+ mRearDisplayEducationDialog = mSystemUIDialogFactory.create();
if (mFoldedStates == null) {
- mFoldedStates = mContext.getResources().getIntArray(
+ mFoldedStates = mResources.getIntArray(
com.android.internal.R.array.config_foldedDeviceStates);
}
mStartedFolded = isFoldedState(startingBaseState);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index ab69acb..3be60b7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -157,7 +157,10 @@
// If the hub is fully visible, send all touch events to it.
val communalVisible = hubShowing && !hubOccluded
if (communalVisible) {
- return communalContainerView.dispatchTouchEvent(ev)
+ communalContainerView.dispatchTouchEvent(ev)
+ // Return true regardless of dispatch result as some touches at the start of a gesture
+ // may return false from dispatchTouchEvent.
+ return true
}
if (edgeSwipeRegionWidth == 0) {
@@ -172,13 +175,19 @@
x >= communalContainerView.width - edgeSwipeRegionWidth
if (inOpeningSwipeRegion && !hubOccluded) {
isTrackingOpenGesture = true
- return communalContainerView.dispatchTouchEvent(ev)
+ communalContainerView.dispatchTouchEvent(ev)
+ // Return true regardless of dispatch result as some touches at the start of a
+ // gesture may return false from dispatchTouchEvent.
+ return true
}
} else if (isTrackingOpenGesture) {
if (isUp || isCancel) {
isTrackingOpenGesture = false
}
- return communalContainerView.dispatchTouchEvent(ev)
+ communalContainerView.dispatchTouchEvent(ev)
+ // Return true regardless of dispatch result as some touches at the start of a gesture
+ // may return false from dispatchTouchEvent.
+ return true
}
return false
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 286037e..fb6bc38 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -2478,6 +2478,13 @@
return 0;
}
if (!mKeyguardBypassController.getBypassEnabled()) {
+ if (migrateClocksToBlueprint()) {
+ View nsslPlaceholder = mView.getRootView().findViewById(R.id.nssl_placeholder);
+ if (!mSplitShadeEnabled && nsslPlaceholder != null) {
+ return nsslPlaceholder.getTop();
+ }
+ }
+
return mClockPositionResult.stackScrollerPadding;
}
int collapsedPosition = mHeadsUpInset;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerModule.kt
new file mode 100644
index 0000000..fc3456a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerModule.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import com.android.systemui.CoreStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface ConfigurationControllerModule {
+
+ /** Starts [ConfigurationControllerStartable] */
+ @Binds
+ @IntoMap
+ @ClassKey(ConfigurationControllerStartable::class)
+ fun bindConfigControllerStartable(impl: ConfigurationControllerStartable): CoreStartable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index 600d4af..45005cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -55,11 +55,12 @@
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.util.Assert;
+import dagger.Lazy;
+
import java.util.ArrayList;
import javax.inject.Inject;
-import dagger.Lazy;
import kotlinx.coroutines.ExperimentalCoroutinesApi;
/**
@@ -175,6 +176,16 @@
}
}
+ /**
+ * Notify the registered callback about SPFS fingerprint acquisition started event.
+ */
+ public void fireSideFpsAcquisitionStarted() {
+ Assert.isMainThread();
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).onSideFingerprintAcquisitionStarted();
+ }
+ }
+
void fireNotificationPulse(NotificationEntry entry) {
Runnable pulseSuppressedListener = () -> {
if (NotificationIconContainerRefactor.isEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index af6da3f..3394eac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -149,6 +149,14 @@
return create(new DialogDelegate<>(){}, mContext);
}
+ /** Creates a new instance of {@link SystemUIDialog} with no customized behavior.
+ *
+ * When you just need a dialog created with a specific {@link Context}, call this.
+ */
+ public SystemUIDialog create(Context context) {
+ return create(new DialogDelegate<>(){}, context);
+ }
+
/**
* Creates a new instance of {@link SystemUIDialog} with {@code delegate} as the {@link
* Delegate}.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
index b048da492..942d186 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
@@ -16,16 +16,12 @@
package com.android.systemui.statusbar.phone.dagger;
-import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.CentralSurfacesImpl;
-import com.android.systemui.statusbar.phone.ConfigurationControllerStartable;
import dagger.Binds;
import dagger.Module;
-import dagger.multibindings.ClassKey;
-import dagger.multibindings.IntoMap;
/**
* Dagger Module providing {@link CentralSurfacesImpl}.
@@ -38,12 +34,4 @@
@Binds
@SysUISingleton
CentralSurfaces bindsCentralSurfaces(CentralSurfacesImpl impl);
-
- /**
- * Starts {@link ConfigurationControllerStartable}
- */
- @Binds
- @IntoMap
- @ClassKey(ConfigurationControllerStartable.class)
- CoreStartable bindConfigControllerStartable(ConfigurationControllerStartable impl);
}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
index 8087a87..550a65c 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
@@ -48,6 +48,8 @@
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.leak.LeakDetector;
+import dagger.Lazy;
+
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -87,6 +89,7 @@
// Set of all tunables, used for leak detection.
private final HashSet<Tunable> mTunables = LeakDetector.ENABLED ? new HashSet<>() : null;
private final Context mContext;
+ private final Lazy<SystemUIDialog.Factory> mSystemUIDialogFactoryLazy;
private final LeakDetector mLeakDetector;
private final DemoModeController mDemoModeController;
@@ -104,9 +107,11 @@
@Main Handler mainHandler,
LeakDetector leakDetector,
DemoModeController demoModeController,
- UserTracker userTracker) {
+ UserTracker userTracker,
+ Lazy<SystemUIDialog.Factory> systemUIDialogFactoryLazy) {
super(context);
mContext = context;
+ mSystemUIDialogFactoryLazy = systemUIDialogFactoryLazy;
mContentResolver = mContext.getContentResolver();
mLeakDetector = leakDetector;
mDemoModeController = demoModeController;
@@ -301,7 +306,7 @@
@Override
public void showResetRequest(Runnable onDisabled) {
- SystemUIDialog dialog = new SystemUIDialog(mContext);
+ SystemUIDialog dialog = mSystemUIDialogFactoryLazy.get().create();
dialog.setShowForAllUsers(true);
dialog.setMessage(R.string.remove_from_settings_prompt);
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mContext.getString(R.string.cancel),
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt
index 9fe32f1..b45c894 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt
@@ -16,16 +16,21 @@
package com.android.keyguard.mediator
-import android.os.Handler
import android.os.Looper
+import android.platform.test.flag.junit.SetFlagsRule
+import android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.unfold.FoldAodAnimationController
import com.android.systemui.unfold.SysUIUnfoldComponent
import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation
import com.android.systemui.util.mockito.capture
+import com.android.systemui.utils.os.FakeHandler
+import com.android.systemui.utils.os.FakeHandler.Mode.QUEUEING
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
@@ -52,10 +57,13 @@
@Captor
private lateinit var readyCaptor: ArgumentCaptor<Runnable>
- private val testHandler = Handler(Looper.getMainLooper())
+ private val testHandler = FakeHandler(Looper.getMainLooper()).apply { setMode(QUEUEING) }
private lateinit var screenOnCoordinator: ScreenOnCoordinator
+ @get:Rule
+ val setFlagsRule = SetFlagsRule(DEVICE_DEFAULT)
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@@ -77,7 +85,7 @@
onUnfoldOverlayReady()
onFoldAodReady()
- waitHandlerIdle(testHandler)
+ waitHandlerIdle()
// Should be called when both unfold overlay and keyguard drawn ready
verify(runnable).run()
@@ -90,7 +98,7 @@
onUnfoldOverlayReady()
onFoldAodReady()
- waitHandlerIdle(testHandler)
+ waitHandlerIdle()
// Should be called when both unfold overlay and keyguard drawn ready
verify(runnable).run()
@@ -104,7 +112,8 @@
onUnfoldOverlayReady()
onFoldAodReady()
- waitHandlerIdle(testHandler)
+ waitHandlerIdle()
+
// Should not be called because this screen turning on call is not valid anymore
verify(runnable, never()).run()
@@ -112,13 +121,43 @@
@Test
fun testUnfoldTransitionDisabledDrawnTasksReady_onScreenTurningOn_callsDrawnCallback() {
+ setFlagsRule.disableFlags(Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK)
// Recreate with empty unfoldComponent
screenOnCoordinator = ScreenOnCoordinator(
Optional.empty(),
testHandler
)
screenOnCoordinator.onScreenTurningOn(runnable)
- waitHandlerIdle(testHandler)
+ waitHandlerIdle()
+
+ // Should be called when only keyguard drawn
+ verify(runnable).run()
+ }
+ @Test
+ fun testUnfoldTransitionDisabledDrawnTasksReady_onScreenTurningOn_usesMainHandler() {
+ setFlagsRule.disableFlags(Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK)
+ // Recreate with empty unfoldComponent
+ screenOnCoordinator = ScreenOnCoordinator(
+ Optional.empty(),
+ testHandler
+ )
+ screenOnCoordinator.onScreenTurningOn(runnable)
+
+ // Never called as the main handler didn't schedule it yet.
+ verify(runnable, never()).run()
+ }
+
+ @Test
+ fun unfoldTransitionDisabledDrawnTasksReady_onScreenTurningOn_bgCallback_callsDrawnCallback() {
+ setFlagsRule.enableFlags(Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK)
+ // Recreate with empty unfoldComponent
+ screenOnCoordinator = ScreenOnCoordinator(
+ Optional.empty(),
+ testHandler
+ )
+ screenOnCoordinator.onScreenTurningOn(runnable)
+ // No need to wait for the handler to be idle, as it shouldn't be used
+ // waitHandlerIdle()
// Should be called when only keyguard drawn
verify(runnable).run()
@@ -134,7 +173,7 @@
readyCaptor.value.run()
}
- private fun waitHandlerIdle(handler: Handler) {
- handler.runWithScissors({}, /* timeout= */ 0)
+ private fun waitHandlerIdle() {
+ testHandler.dispatchQueuedMessages()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
index bfb5485..c525711 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
@@ -120,7 +120,7 @@
fontScalingDialogDelegate
)
- whenever(dialogFactory.create(any())).thenReturn(dialog)
+ whenever(dialogFactory.create(any(), any())).thenReturn(dialog)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt
index 640807b..8adee8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt
@@ -36,15 +36,24 @@
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
import com.android.systemui.log.SideFpsLogger
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.res.R
+import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -62,9 +71,10 @@
@SmallTest
@RunWith(JUnit4::class)
class SideFpsSensorInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
@JvmField @Rule var mockitoRule = MockitoJUnit.rule()
- private val testScope = TestScope(StandardTestDispatcher())
+ private val testScope = kosmos.testScope
private val fingerprintRepository = FakeFingerprintPropertyRepository()
@@ -101,6 +111,7 @@
windowManager,
displayStateInteractor,
Optional.of(fingerprintInteractiveToAuthProvider),
+ kosmos.keyguardTransitionInteractor,
SideFpsLogger(logcatLogBuffer("SfpsLogger"))
)
}
@@ -129,11 +140,62 @@
assertThat(isAvailable).isFalse()
}
+ private suspend fun sendTransition(from: KeyguardState, to: KeyguardState) {
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ TransitionStep(
+ from = from,
+ to = to,
+ transitionState = TransitionState.STARTED,
+ ),
+ TransitionStep(
+ from = from,
+ to = to,
+ transitionState = TransitionState.FINISHED,
+ value = 1.0f
+ )
+ ),
+ testScope
+ )
+ }
+
@Test
- fun authenticationDurationIsAvailableWhenSFPSSensorIsAvailable() =
+ fun authenticationDurationIsLongerIfScreenIsOff() =
testScope.runTest {
- assertThat(underTest.authenticationDuration)
- .isEqualTo(context.resources.getInteger(R.integer.config_restToUnlockDuration))
+ val authenticationDuration by collectLastValue(underTest.authenticationDuration)
+ val longDuration =
+ context.resources.getInteger(R.integer.config_restToUnlockDurationScreenOff)
+ sendTransition(LOCKSCREEN, OFF)
+
+ runCurrent()
+ assertThat(authenticationDuration).isEqualTo(longDuration)
+ }
+
+ @Test
+ fun authenticationDurationIsLongerIfScreenIsDozing() =
+ testScope.runTest {
+ val authenticationDuration by collectLastValue(underTest.authenticationDuration)
+ val longDuration =
+ context.resources.getInteger(R.integer.config_restToUnlockDurationScreenOff)
+ sendTransition(LOCKSCREEN, DOZING)
+ runCurrent()
+ assertThat(authenticationDuration).isEqualTo(longDuration)
+ }
+
+ @Test
+ fun authenticationDurationIsShorterIfScreenIsNotDozingOrOff() =
+ testScope.runTest {
+ val authenticationDuration by collectLastValue(underTest.authenticationDuration)
+ val shortDuration =
+ context.resources.getInteger(R.integer.config_restToUnlockDurationDefault)
+ val allOtherKeyguardStates = KeyguardState.entries.filter { it != OFF && it != DOZING }
+
+ allOtherKeyguardStates.forEach { destinationState ->
+ sendTransition(OFF, destinationState)
+
+ runCurrent()
+ assertThat(authenticationDuration).isEqualTo(shortDuration)
+ }
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
index cb26178..755fa02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -75,6 +75,7 @@
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import java.util.Optional
@@ -82,6 +83,7 @@
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -235,15 +237,18 @@
windowManager,
displayStateInteractor,
Optional.of(fingerprintInteractiveToAuthProvider),
+ mock(),
SideFpsLogger(logcatLogBuffer("SfpsLogger"))
)
sideFpsProgressBarViewModel =
SideFpsProgressBarViewModel(
mContext,
- deviceEntryFingerprintAuthRepository,
+ mock(),
sfpsSensorInteractor,
+ mock(),
displayStateInteractor,
+ UnconfinedTestDispatcher(),
testScope.backgroundScope,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
index 823b952..bdca948 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
@@ -72,6 +72,7 @@
import com.android.systemui.unfold.compat.ScreenSizeFoldProvider
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -238,15 +239,18 @@
windowManager,
displayStateInteractor,
Optional.of(fingerprintInteractiveToAuthProvider),
+ mock(),
SideFpsLogger(logcatLogBuffer("SfpsLogger"))
)
sideFpsProgressBarViewModel =
SideFpsProgressBarViewModel(
mContext,
- deviceEntryFingerprintAuthRepository,
+ mock(),
sfpsSensorInteractor,
+ mock(),
displayStateInteractor,
+ StandardTestDispatcher(),
testScope.backgroundScope,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
index 4022d43..3ff43c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
@@ -28,8 +28,6 @@
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
-import android.view.LayoutInflater;
-import android.view.View;
import android.widget.Button;
import android.widget.TextView;
@@ -95,7 +93,7 @@
mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true);
when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
- when(mSystemUIDialogFactory.create(any())).thenReturn(mDialog);
+ when(mSystemUIDialogFactory.create(any(), any())).thenReturn(mDialog);
mBroadcastDialogDelegate = new BroadcastDialogDelegate(
mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt
index 65f68f9..35ac2ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt
@@ -31,6 +31,7 @@
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.model.SysUiState
import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.phone.DialogDelegate
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
@@ -69,7 +70,8 @@
mDependency.injectTestDependency(SysUiState::class.java, sysuiState)
mDependency.injectMockDependency(DialogLaunchAnimator::class.java)
whenever(sysuiState.setFlag(any(), any())).thenReturn(sysuiState)
- whenever(sysuiDialogFactory.create(any())).thenReturn(sysuiDialog)
+ whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java)))
+ .thenReturn(sysuiDialog)
whenever(sysuiDialog.layoutInflater).thenReturn(LayoutInflater.from(mContext))
whenever(mockUserTracker.userId).thenReturn(context.userId)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
index 4e8f866..7f0ea9a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
@@ -17,34 +17,48 @@
package com.android.systemui.controls.management
+import android.content.Context
import android.content.DialogInterface
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
class PanelConfirmationDialogFactoryTest : SysuiTestCase() {
+ @Mock private lateinit var mockDialog : SystemUIDialog
+ @Mock private lateinit var mockDialogFactory : SystemUIDialog.Factory
+ private lateinit var factory : PanelConfirmationDialogFactory
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(mockDialogFactory.create(any(Context::class.java))).thenReturn(mockDialog)
+ whenever(mockDialog.context).thenReturn(mContext)
+ factory = PanelConfirmationDialogFactory(mockDialogFactory)
+ }
+
@Test
fun testDialogHasCorrectInfo() {
- val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
- val factory = PanelConfirmationDialogFactory { mockDialog }
val appName = "appName"
- factory.createConfirmationDialog(context, appName) {}
+ factory.createConfirmationDialog(mContext, appName) {}
verify(mockDialog).setCanceledOnTouchOutside(true)
verify(mockDialog)
@@ -55,12 +69,9 @@
@Test
fun testDialogPositiveButton() {
- val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
- val factory = PanelConfirmationDialogFactory { mockDialog }
-
var response: Boolean? = null
- factory.createConfirmationDialog(context, "") { response = it }
+ factory.createConfirmationDialog(mContext,"") { response = it }
val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor()
verify(mockDialog).setPositiveButton(eq(R.string.controls_dialog_ok), capture(captor))
@@ -72,12 +83,9 @@
@Test
fun testDialogNeutralButton() {
- val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
- val factory = PanelConfirmationDialogFactory { mockDialog }
-
var response: Boolean? = null
- factory.createConfirmationDialog(context, "") { response = it }
+ factory.createConfirmationDialog(mContext, "") { response = it }
val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor()
verify(mockDialog).setNeutralButton(eq(R.string.cancel), capture(captor))
@@ -89,12 +97,9 @@
@Test
fun testDialogCancel() {
- val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
- val factory = PanelConfirmationDialogFactory { mockDialog }
-
var response: Boolean? = null
- factory.createConfirmationDialog(context, "") { response = it }
+ factory.createConfirmationDialog(mContext, "") { response = it }
val captor: ArgumentCaptor<DialogInterface.OnCancelListener> = argumentCaptor()
verify(mockDialog).setOnCancelListener(capture(captor))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt
index 8eebcee..38c6a0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt
@@ -17,17 +17,23 @@
package com.android.systemui.controls.ui
+import android.content.Context
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.FakeSystemUIDialogController
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
import org.mockito.Mockito.eq
import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -37,18 +43,24 @@
const val APP_NAME = "Test App"
}
- private val fakeDialogController = FakeSystemUIDialogController()
+ @Mock
+ private lateinit var mockDialogFactory : SystemUIDialog.Factory
+
+ private val fakeDialogController = FakeSystemUIDialogController(mContext)
private lateinit var underTest: ControlsDialogsFactory
@Before
fun setup() {
- underTest = ControlsDialogsFactory { fakeDialogController.dialog }
+ MockitoAnnotations.initMocks(this)
+ whenever(mockDialogFactory.create(any(Context::class.java)))
+ .thenReturn(fakeDialogController.dialog)
+ underTest = ControlsDialogsFactory(mockDialogFactory)
}
@Test
fun testCreatesRemoveAppDialog() {
- val dialog = underTest.createRemoveAppDialog(context, APP_NAME) {}
+ val dialog = underTest.createRemoveAppDialog(mContext, APP_NAME) {}
verify(dialog)
.setTitle(
@@ -60,7 +72,7 @@
@Test
fun testPositiveClickRemoveAppDialogWorks() {
var dialogResult: Boolean? = null
- underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it }
+ underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it }
fakeDialogController.clickPositive()
@@ -70,7 +82,7 @@
@Test
fun testNeutralClickRemoveAppDialogWorks() {
var dialogResult: Boolean? = null
- underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it }
+ underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it }
fakeDialogController.clickNeutral()
@@ -80,7 +92,7 @@
@Test
fun testCancelRemoveAppDialogWorks() {
var dialogResult: Boolean? = null
- underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it }
+ underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it }
fakeDialogController.cancel()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index 11bd9cb..36ae0c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -51,6 +51,7 @@
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSystemUIDialogController
import com.android.systemui.util.concurrency.FakeExecutor
@@ -97,9 +98,10 @@
@Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
@Mock lateinit var featureFlags: FeatureFlags
@Mock lateinit var packageManager: PackageManager
+ @Mock lateinit var systemUIDialogFactory: SystemUIDialog.Factory
private val preferredPanelRepository = FakeSelectedComponentRepository()
- private val fakeDialogController = FakeSystemUIDialogController()
+ private lateinit var fakeDialogController: FakeSystemUIDialogController
private val uiExecutor = FakeExecutor(FakeSystemClock())
private val bgExecutor = FakeExecutor(FakeSystemClock())
@@ -114,6 +116,9 @@
fun setup() {
MockitoAnnotations.initMocks(this)
+ fakeDialogController = FakeSystemUIDialogController(mContext)
+ whenever(systemUIDialogFactory.create(any(Context::class.java)))
+ .thenReturn(fakeDialogController.dialog)
controlsSettingsRepository = FakeControlsSettingsRepository()
// This way, it won't be cloned every time `LayoutInflater.fromContext` is called, but we
@@ -146,10 +151,7 @@
authorizedPanelsRepository,
preferredPanelRepository,
featureFlags,
- ControlsDialogsFactory {
- isRemoveAppDialogCreated = true
- fakeDialogController.dialog
- },
+ ControlsDialogsFactory(systemUIDialogFactory),
dumpManager,
)
`when`(userTracker.userId).thenReturn(0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
index ab6bc2c..66fdf53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
@@ -19,7 +19,6 @@
import android.os.VibrationAttributes
import android.os.VibrationEffect
import android.view.VelocityTracker
-import android.view.animation.AccelerateInterpolator
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -51,8 +50,6 @@
private val lowTickDuration = 12 // Mocked duration of a low tick
private val dragTextureThresholdMillis =
lowTickDuration * config.numberOfLowTicks + config.deltaMillisForDragInterval
- private val progressInterpolator = AccelerateInterpolator(config.progressInterpolatorFactor)
- private val velocityInterpolator = AccelerateInterpolator(config.velocityInterpolatorFactor)
private lateinit var sliderHapticFeedbackProvider: SliderHapticFeedbackProvider
@Before
@@ -60,7 +57,9 @@
MockitoAnnotations.initMocks(this)
whenever(vibratorHelper.getPrimitiveDurations(any()))
.thenReturn(intArrayOf(lowTickDuration))
- whenever(velocityTracker.xVelocity).thenReturn(config.maxVelocityToScale)
+ whenever(velocityTracker.isAxisSupported(config.velocityAxis)).thenReturn(true)
+ whenever(velocityTracker.getAxisVelocity(config.velocityAxis))
+ .thenReturn(config.maxVelocityToScale)
sliderHapticFeedbackProvider =
SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, config, clock)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
index 8dd33d5..1205dce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
@@ -21,11 +21,11 @@
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
-import com.android.systemui.SysuiTestCase
import com.android.systemui.Flags as AConfigFlags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.res.R
import com.android.systemui.statusbar.KeyguardIndicationController
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -38,8 +38,9 @@
@RunWith(JUnit4::class)
@SmallTest
class DefaultIndicationAreaSectionTest : SysuiTestCase() {
+
@Mock private lateinit var keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel
- @Mock private lateinit var keyguardRootViewModel: KeyguardRootViewModel
+ @Mock private lateinit var aodAlphaViewModel: AodAlphaViewModel
@Mock private lateinit var indicationController: KeyguardIndicationController
private lateinit var underTest: DefaultIndicationAreaSection
@@ -51,7 +52,7 @@
DefaultIndicationAreaSection(
context,
keyguardIndicationAreaViewModel,
- keyguardRootViewModel,
+ aodAlphaViewModel,
indicationController,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
index d421004..1b4573d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
@@ -34,6 +34,7 @@
import com.android.systemui.plugins.clocks.ClockFaceConfig
import com.android.systemui.plugins.clocks.ClockFaceController
import com.android.systemui.shared.clocks.ClockRegistry
+import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
@@ -66,6 +67,8 @@
@Mock private lateinit var largeClock: ClockFaceController
@Mock private lateinit var clockFaceConfig: ClockFaceConfig
@Mock private lateinit var eventController: ClockEventController
+ @Mock private lateinit var splitShadeStateController: SplitShadeStateController
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
@@ -92,6 +95,7 @@
keyguardInteractor,
keyguardClockInteractor,
scope.backgroundScope,
+ splitShadeStateController,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
deleted file mode 100644
index ee1be10..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ /dev/null
@@ -1,498 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package com.android.systemui.keyguard.ui.viewmodel
-
-import android.view.View
-import androidx.test.filters.SmallTest
-import com.android.systemui.Flags as AConfigFlags
-import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
-import com.android.systemui.common.ui.domain.interactor.configurationInteractor
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
-import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
-import com.android.systemui.flags.featureFlagsClassic
-import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.BurnInModel
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.plugins.clocks.ClockController
-import com.android.systemui.statusbar.notification.data.repository.fakeNotificationsKeyguardViewStateRepository
-import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor
-import com.android.systemui.statusbar.phone.dozeParameters
-import com.android.systemui.statusbar.phone.screenOffAnimationController
-import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.ui.isAnimating
-import com.android.systemui.util.ui.stopAnimating
-import com.android.systemui.util.ui.value
-import com.google.common.truth.Truth.assertThat
-import javax.inject.Provider
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Answers
-import org.mockito.Mock
-import org.mockito.Mockito.RETURNS_DEEP_STUBS
-import org.mockito.Mockito.anyInt
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(JUnit4::class)
-class KeyguardRootViewModelTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
- private val repository = kosmos.fakeKeyguardRepository
- private val configurationRepository = kosmos.fakeConfigurationRepository
- private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
- private val screenOffAnimationController = kosmos.screenOffAnimationController
- private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
- private val notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor
- private val fakeNotificationsKeyguardViewStateRepository =
- kosmos.fakeNotificationsKeyguardViewStateRepository
- private val dozeParameters = kosmos.dozeParameters
- private lateinit var underTest: KeyguardRootViewModel
-
- @Mock private lateinit var burnInInteractor: BurnInInteractor
- private val burnInFlow = MutableStateFlow(BurnInModel())
-
- @Mock private lateinit var goneToAodTransitionViewModel: GoneToAodTransitionViewModel
- private val enterFromTopAnimationAlpha = MutableStateFlow(0f)
-
- @Mock
- private lateinit var aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel
- @Mock
- private lateinit var occludedToLockscreenTransitionViewModel:
- OccludedToLockscreenTransitionViewModel
- private val occludedToLockscreenTranslationY = MutableStateFlow(0f)
- private val occludedToLockscreenAlpha = MutableStateFlow(0f)
-
- @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var clockController: ClockController
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
-
- mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
- mSetFlagsRule.enableFlags(FLAG_NEW_AOD_TRANSITION)
- mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-
- whenever(goneToAodTransitionViewModel.enterFromTopTranslationY(anyInt()))
- .thenReturn(emptyFlow<Float>())
- whenever(goneToAodTransitionViewModel.enterFromTopAnimationAlpha)
- .thenReturn(enterFromTopAnimationAlpha)
-
- whenever(burnInInteractor.keyguardBurnIn).thenReturn(burnInFlow)
-
- whenever(occludedToLockscreenTransitionViewModel.lockscreenTranslationY)
- .thenReturn(occludedToLockscreenTranslationY)
- whenever(occludedToLockscreenTransitionViewModel.lockscreenAlpha)
- .thenReturn(occludedToLockscreenAlpha)
-
- underTest =
- KeyguardRootViewModel(
- configurationInteractor = kosmos.configurationInteractor,
- deviceEntryInteractor = kosmos.deviceEntryInteractor,
- dozeParameters = kosmos.dozeParameters,
- keyguardInteractor = kosmos.keyguardInteractor,
- keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
- notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor,
- burnInInteractor = burnInInteractor,
- keyguardClockViewModel = kosmos.keyguardClockViewModel,
- goneToAodTransitionViewModel = goneToAodTransitionViewModel,
- aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
- occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
- screenOffAnimationController = screenOffAnimationController,
- // TODO(b/310989341): remove after change to aconfig
- featureFlags = kosmos.featureFlagsClassic
- )
-
- underTest.clockControllerProvider = Provider { clockController }
- }
-
- @Test
- fun alpha() =
- testScope.runTest {
- val alpha by collectLastValue(underTest.alpha)
-
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.OFF,
- to = KeyguardState.LOCKSCREEN,
- testScope = testScope,
- )
-
- repository.setKeyguardAlpha(0.1f)
- assertThat(alpha).isEqualTo(0.1f)
- repository.setKeyguardAlpha(0.5f)
- assertThat(alpha).isEqualTo(0.5f)
- repository.setKeyguardAlpha(0.2f)
- assertThat(alpha).isEqualTo(0.2f)
- repository.setKeyguardAlpha(0f)
- assertThat(alpha).isEqualTo(0f)
- occludedToLockscreenAlpha.value = 0.8f
- assertThat(alpha).isEqualTo(0.8f)
- }
-
- @Test
- fun alphaWhenGoneEqualsZero() =
- testScope.runTest {
- val alpha by collectLastValue(underTest.alpha)
-
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- testScope = testScope,
- )
-
- repository.setKeyguardAlpha(0.1f)
- assertThat(alpha).isEqualTo(0f)
- repository.setKeyguardAlpha(0.5f)
- assertThat(alpha).isEqualTo(0f)
- repository.setKeyguardAlpha(1f)
- assertThat(alpha).isEqualTo(0f)
- }
-
- @Test
- fun translationYInitialValueIsZero() =
- testScope.runTest {
- val translationY by collectLastValue(underTest.translationY)
- assertThat(translationY).isEqualTo(0)
- }
-
- @Test
- fun translationAndScaleFromBurnInNotDozing() =
- testScope.runTest {
- val translationX by collectLastValue(underTest.translationX)
- val translationY by collectLastValue(underTest.translationY)
- val scale by collectLastValue(underTest.scale)
-
- // Set to not dozing (on lockscreen)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.AOD,
- to = KeyguardState.LOCKSCREEN,
- value = 1f,
- transitionState = TransitionState.FINISHED
- ),
- validateStep = false,
- )
-
- // Trigger a change to the burn-in model
- burnInFlow.value =
- BurnInModel(
- translationX = 20,
- translationY = 30,
- scale = 0.5f,
- )
-
- assertThat(translationX).isEqualTo(0)
- assertThat(translationY).isEqualTo(0)
- assertThat(scale).isEqualTo(Pair(1f, true /* scaleClockOnly */))
- }
-
- @Test
- fun translationAndScaleFromBurnFullyDozing() =
- testScope.runTest {
- val translationX by collectLastValue(underTest.translationX)
- val translationY by collectLastValue(underTest.translationY)
- val scale by collectLastValue(underTest.scale)
-
- underTest.statusViewTop = 100
-
- // Set to dozing (on AOD)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.GONE,
- to = KeyguardState.AOD,
- value = 1f,
- transitionState = TransitionState.FINISHED
- ),
- validateStep = false,
- )
- // Trigger a change to the burn-in model
- burnInFlow.value =
- BurnInModel(
- translationX = 20,
- translationY = 30,
- scale = 0.5f,
- )
-
- assertThat(translationX).isEqualTo(20)
- assertThat(translationY).isEqualTo(30)
- assertThat(scale).isEqualTo(Pair(0.5f, true /* scaleClockOnly */))
-
- // Set to the beginning of GONE->AOD transition
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.GONE,
- to = KeyguardState.AOD,
- value = 0f,
- transitionState = TransitionState.STARTED
- ),
- validateStep = false,
- )
- assertThat(translationX).isEqualTo(0)
- assertThat(translationY).isEqualTo(0)
- assertThat(scale).isEqualTo(Pair(1f, true /* scaleClockOnly */))
- }
-
- @Test
- fun translationAndScaleFromBurnFullyDozingStaysOutOfTopInset() =
- testScope.runTest {
- val translationX by collectLastValue(underTest.translationX)
- val translationY by collectLastValue(underTest.translationY)
- val scale by collectLastValue(underTest.scale)
-
- underTest.statusViewTop = 100
- underTest.topInset = 80
-
- // Set to dozing (on AOD)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.GONE,
- to = KeyguardState.AOD,
- value = 1f,
- transitionState = TransitionState.FINISHED
- ),
- validateStep = false,
- )
-
- // Trigger a change to the burn-in model
- burnInFlow.value =
- BurnInModel(
- translationX = 20,
- translationY = -30,
- scale = 0.5f,
- )
- assertThat(translationX).isEqualTo(20)
- // -20 instead of -30, due to inset of 80
- assertThat(translationY).isEqualTo(-20)
- assertThat(scale).isEqualTo(Pair(0.5f, true /* scaleClockOnly */))
-
- // Set to the beginning of GONE->AOD transition
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.GONE,
- to = KeyguardState.AOD,
- value = 0f,
- transitionState = TransitionState.STARTED
- ),
- validateStep = false,
- )
- assertThat(translationX).isEqualTo(0)
- assertThat(translationY).isEqualTo(0)
- assertThat(scale).isEqualTo(Pair(1f, true /* scaleClockOnly */))
- }
-
- @Test
- fun translationAndScaleFromBurnInUseScaleOnly() =
- testScope.runTest {
- whenever(clockController.config.useAlternateSmartspaceAODTransition).thenReturn(true)
-
- val translationX by collectLastValue(underTest.translationX)
- val translationY by collectLastValue(underTest.translationY)
- val scale by collectLastValue(underTest.scale)
-
- // Set to dozing (on AOD)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.GONE,
- to = KeyguardState.AOD,
- value = 1f,
- transitionState = TransitionState.FINISHED
- ),
- validateStep = false,
- )
-
- // Trigger a change to the burn-in model
- burnInFlow.value =
- BurnInModel(
- translationX = 20,
- translationY = 30,
- scale = 0.5f,
- )
-
- assertThat(translationX).isEqualTo(0)
- assertThat(translationY).isEqualTo(0)
- assertThat(scale).isEqualTo(Pair(0.5f, false /* scaleClockOnly */))
- }
-
- @Test
- fun burnInLayerVisibility() =
- testScope.runTest {
- val burnInLayerVisibility by collectLastValue(underTest.burnInLayerVisibility)
-
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.AOD,
- value = 0f,
- transitionState = TransitionState.STARTED
- ),
- validateStep = false,
- )
- assertThat(burnInLayerVisibility).isEqualTo(View.VISIBLE)
- }
-
- @Test
- fun burnInLayerAlpha() =
- testScope.runTest {
- val burnInLayerAlpha by collectLastValue(underTest.burnInLayerAlpha)
-
- enterFromTopAnimationAlpha.value = 0.2f
- assertThat(burnInLayerAlpha).isEqualTo(0.2f)
-
- enterFromTopAnimationAlpha.value = 1f
- assertThat(burnInLayerAlpha).isEqualTo(1f)
- }
-
- @Test
- fun iconContainer_isNotVisible_notOnKeyguard_dontShowAodIconsWhenShade() =
- testScope.runTest {
- val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
- runCurrent()
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.OFF,
- to = KeyguardState.GONE,
- testScope,
- )
- whenever(screenOffAnimationController.shouldShowAodIconsWhenShade()).thenReturn(false)
- runCurrent()
-
- assertThat(isVisible?.value).isFalse()
- assertThat(isVisible?.isAnimating).isFalse()
- }
-
- @Test
- fun iconContainer_isVisible_bypassEnabled() =
- testScope.runTest {
- val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
- runCurrent()
- deviceEntryRepository.setBypassEnabled(true)
- runCurrent()
-
- assertThat(isVisible?.value).isTrue()
- }
-
- @Test
- fun iconContainer_isNotVisible_pulseExpanding_notBypassing() =
- testScope.runTest {
- val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
- runCurrent()
- fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(true)
- deviceEntryRepository.setBypassEnabled(false)
- runCurrent()
-
- assertThat(isVisible?.value).isEqualTo(false)
- }
-
- @Test
- fun iconContainer_isVisible_notifsFullyHidden_bypassEnabled() =
- testScope.runTest {
- val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
- runCurrent()
- fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
- deviceEntryRepository.setBypassEnabled(true)
- fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
- runCurrent()
-
- assertThat(isVisible?.value).isTrue()
- assertThat(isVisible?.isAnimating).isTrue()
- }
-
- @Test
- fun iconContainer_isVisible_notifsFullyHidden_bypassDisabled_aodDisabled() =
- testScope.runTest {
- val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
- runCurrent()
- fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
- deviceEntryRepository.setBypassEnabled(false)
- whenever(dozeParameters.alwaysOn).thenReturn(false)
- fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
- runCurrent()
-
- assertThat(isVisible?.value).isTrue()
- assertThat(isVisible?.isAnimating).isFalse()
- }
-
- @Test
- fun iconContainer_isVisible_notifsFullyHidden_bypassDisabled_displayNeedsBlanking() =
- testScope.runTest {
- val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
- runCurrent()
- fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
- deviceEntryRepository.setBypassEnabled(false)
- whenever(dozeParameters.alwaysOn).thenReturn(true)
- whenever(dozeParameters.displayNeedsBlanking).thenReturn(true)
- fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
- runCurrent()
-
- assertThat(isVisible?.value).isTrue()
- assertThat(isVisible?.isAnimating).isFalse()
- }
-
- @Test
- fun iconContainer_isVisible_notifsFullyHidden_bypassDisabled() =
- testScope.runTest {
- val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
- runCurrent()
- fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
- deviceEntryRepository.setBypassEnabled(false)
- whenever(dozeParameters.alwaysOn).thenReturn(true)
- whenever(dozeParameters.displayNeedsBlanking).thenReturn(false)
- fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
- runCurrent()
-
- assertThat(isVisible?.value).isTrue()
- assertThat(isVisible?.isAnimating).isTrue()
- }
-
- @Test
- fun isIconContainerVisible_stopAnimation() =
- testScope.runTest {
- val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
- runCurrent()
- fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false)
- deviceEntryRepository.setBypassEnabled(false)
- whenever(dozeParameters.alwaysOn).thenReturn(true)
- whenever(dozeParameters.displayNeedsBlanking).thenReturn(false)
- fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true)
- runCurrent()
-
- assertThat(isVisible?.isAnimating).isEqualTo(true)
- isVisible?.stopAnimating()
- runCurrent()
-
- assertThat(isVisible?.isAnimating).isEqualTo(false)
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
index 6248bb1..1a303b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
@@ -55,6 +55,7 @@
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.util.NotificationChannels;
import com.android.systemui.util.settings.FakeGlobalSettings;
@@ -77,7 +78,6 @@
public static final String FORMATTED_45M = "0h 45m";
public static final String FORMATTED_HOUR = "1h 0m";
private final NotificationManager mMockNotificationManager = mock(NotificationManager.class);
- private final GlobalSettings mGlobalSettings = new FakeGlobalSettings();
private PowerNotificationWarnings mPowerNotificationWarnings;
@Mock
@@ -90,6 +90,10 @@
private UserTracker mUserTracker;
@Mock
private View mView;
+ @Mock
+ private SystemUIDialog.Factory mSystemUIDialogFactory;
+ @Mock
+ private SystemUIDialog mSystemUIDialog;
private BroadcastReceiver mReceiver;
@@ -113,9 +117,16 @@
when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
when(mUserTracker.getUserHandle()).thenReturn(
UserHandle.of(ActivityManager.getCurrentUser()));
- mPowerNotificationWarnings = new PowerNotificationWarnings(wrapper, starter,
- broadcastSender, () -> mBatteryController, mDialogLaunchAnimator, mUiEventLogger,
- mGlobalSettings, mUserTracker);
+ when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog);
+ mPowerNotificationWarnings = new PowerNotificationWarnings(
+ wrapper,
+ starter,
+ broadcastSender,
+ () -> mBatteryController,
+ mDialogLaunchAnimator,
+ mUiEventLogger,
+ mUserTracker,
+ mSystemUIDialogFactory);
BatteryStateSnapshot snapshot = new BatteryStateSnapshot(100, false, false, 1,
BatteryManager.BATTERY_HEALTH_GOOD, 5, 15);
mPowerNotificationWarnings.updateSnapshot(snapshot);
@@ -251,7 +262,7 @@
verify(mDialogLaunchAnimator, never()).showFromView(any(), any());
- assertThat(mPowerNotificationWarnings.getSaverConfirmationDialog().isShowing()).isTrue();
+ verify(mPowerNotificationWarnings.getSaverConfirmationDialog()).show();
mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss();
}
@@ -266,7 +277,7 @@
verify(mDialogLaunchAnimator, never()).showFromView(any(), any());
- assertThat(mPowerNotificationWarnings.getSaverConfirmationDialog().isShowing()).isTrue();
+ verify(mPowerNotificationWarnings.getSaverConfirmationDialog()).show();
mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss();
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
index f5a3bec..698868d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
@@ -27,6 +27,7 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.IActivityManager;
import android.app.IForegroundServiceObserver;
@@ -53,6 +54,7 @@
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.DeviceConfigProxyFake;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
@@ -95,6 +97,10 @@
BroadcastDispatcher mBroadcastDispatcher;
@Mock
DumpManager mDumpManager;
+ @Mock
+ SystemUIDialog.Factory mSystemUIDialogFactory;
+ @Mock
+ SystemUIDialog mSystemUIDialog;
private FgsManagerController mFmc;
@@ -114,6 +120,7 @@
mSystemClock = new FakeSystemClock();
mMainExecutor = new FakeExecutor(mSystemClock);
mBackgroundExecutor = new FakeExecutor(mSystemClock);
+ when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog);
mUserProfiles = new ArrayList<>();
Mockito.doReturn(mUserProfiles).when(mUserTracker).getUserProfiles();
@@ -325,7 +332,8 @@
mDeviceConfigProxyFake,
mDialogLaunchAnimator,
mBroadcastDispatcher,
- mDumpManager
+ mDumpManager,
+ mSystemUIDialogFactory
);
fmc.init();
Assert.assertTrue(fmc.getIncludesUserVisibleJobs());
@@ -351,7 +359,8 @@
mDeviceConfigProxyFake,
mDialogLaunchAnimator,
mBroadcastDispatcher,
- mDumpManager
+ mDumpManager,
+ mSystemUIDialogFactory
);
fmc.init();
Assert.assertFalse(fmc.getIncludesUserVisibleJobs());
@@ -457,7 +466,8 @@
mDeviceConfigProxyFake,
mDialogLaunchAnimator,
mBroadcastDispatcher,
- mDumpManager
+ mDumpManager,
+ mSystemUIDialogFactory
);
result.init();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
index 51e95be..c109a1e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
@@ -32,7 +32,9 @@
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.policy.DataSaverController
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -49,8 +51,6 @@
@Mock private lateinit var mHost: QSHost
@Mock private lateinit var mMetricsLogger: MetricsLogger
- @Mock private lateinit var mStatusBarStateController: StatusBarStateController
- @Mock private lateinit var mActivityStarter: ActivityStarter
@Mock private lateinit var mQsLogger: QSLogger
private val falsingManager = FalsingManagerFake()
@Mock private lateinit var statusBarStateController: StatusBarStateController
@@ -58,6 +58,8 @@
@Mock private lateinit var dataSaverController: DataSaverController
@Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
@Mock private lateinit var uiEventLogger: QsEventLogger
+ @Mock private lateinit var systemUIDialogFactory: SystemUIDialog.Factory
+ @Mock private lateinit var systemUIDialog: SystemUIDialog
private lateinit var testableLooper: TestableLooper
private lateinit var tile: DataSaverTile
@@ -67,7 +69,8 @@
MockitoAnnotations.initMocks(this)
testableLooper = TestableLooper.get(this)
- Mockito.`when`(mHost.context).thenReturn(mContext)
+ whenever(mHost.context).thenReturn(mContext)
+ whenever(systemUIDialogFactory.create()).thenReturn(systemUIDialog)
tile =
DataSaverTile(
@@ -81,7 +84,8 @@
activityStarter,
mQsLogger,
dataSaverController,
- dialogLaunchAnimator
+ dialogLaunchAnimator,
+ systemUIDialogFactory
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
index 0a34810..945490f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
@@ -36,6 +36,7 @@
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -56,6 +57,8 @@
class UserSwitchDialogControllerTest : SysuiTestCase() {
@Mock
+ private lateinit var dialogFactory: SystemUIDialog.Factory
+ @Mock
private lateinit var dialog: SystemUIDialog
@Mock
private lateinit var falsingManager: FalsingManager
@@ -80,7 +83,8 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
- `when`(dialog.context).thenReturn(mContext)
+ whenever(dialog.context).thenReturn(mContext)
+ whenever(dialogFactory.create()).thenReturn(dialog)
controller = UserSwitchDialogController(
{ userDetailViewAdapter },
@@ -88,7 +92,7 @@
falsingManager,
dialogLaunchAnimator,
uiEventLogger,
- { dialog }
+ dialogFactory
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
index 273ce85..35bf775 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
@@ -18,25 +18,42 @@
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotSame;
-import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.hardware.devicestate.DeviceStateManager;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.view.LayoutInflater;
+import android.view.View;
import android.widget.TextView;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.res.R;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.model.SysUiState;
+import com.android.systemui.res.R;
import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -45,24 +62,49 @@
@Mock
private CommandQueue mCommandQueue;
+ @Mock
+ private SystemUIDialog.Factory mSystemUIDialogFactory;
+ @Mock
+ private SystemUIDialog mSystemUIDialog;
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+ @Mock
+ private SysUiState mSysUiState;
+ @Mock
+ private Resources mResources;
- private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
+ LayoutInflater mLayoutInflater = LayoutInflater.from(mContext);
+ private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
private static final int CLOSED_BASE_STATE = 0;
private static final int OPEN_BASE_STATE = 1;
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true);
+ when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
+ when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog);
+ when(mSystemUIDialog.getContext()).thenReturn(mContext);
+ }
@Test
public void testClosedDialogIsShown() {
- RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
- mCommandQueue, mFakeExecutor);
+ RearDisplayDialogController controller = new RearDisplayDialogController(
+ mCommandQueue,
+ mFakeExecutor,
+ mResources,
+ mLayoutInflater,
+ mSystemUIDialogFactory);
controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback());
controller.setFoldedStates(new int[]{0});
controller.setAnimationRepeatCount(0);
controller.showRearDisplayDialog(CLOSED_BASE_STATE);
- assertTrue(controller.mRearDisplayEducationDialog.isShowing());
- TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
+ verify(mSystemUIDialog).show();
+
+ View container = getDialogViewContainer();
+ TextView deviceClosedTitleTextView = container.findViewById(
R.id.rear_display_title_text_view);
assertEquals(deviceClosedTitleTextView.getText().toString(),
getContext().getResources().getString(
@@ -71,20 +113,28 @@
@Test
public void testClosedDialogIsRefreshedOnConfigurationChange() {
- RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
- mCommandQueue, mFakeExecutor);
+ RearDisplayDialogController controller = new RearDisplayDialogController(
+ mCommandQueue,
+ mFakeExecutor,
+ mResources,
+ mLayoutInflater,
+ mSystemUIDialogFactory);
controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback());
controller.setFoldedStates(new int[]{0});
controller.setAnimationRepeatCount(0);
controller.showRearDisplayDialog(CLOSED_BASE_STATE);
- assertTrue(controller.mRearDisplayEducationDialog.isShowing());
- TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
+ verify(mSystemUIDialog).show();
+ View container = getDialogViewContainer();
+ TextView deviceClosedTitleTextView = container.findViewById(
R.id.rear_display_title_text_view);
+ reset(mSystemUIDialog);
+ when(mSystemUIDialog.isShowing()).thenReturn(true);
+ when(mSystemUIDialog.getContext()).thenReturn(mContext);
+
controller.onConfigChanged(new Configuration());
- assertTrue(controller.mRearDisplayEducationDialog.isShowing());
- TextView deviceClosedTitleTextView2 = controller.mRearDisplayEducationDialog.findViewById(
+ TextView deviceClosedTitleTextView2 = container.findViewById(
R.id.rear_display_title_text_view);
assertNotSame(deviceClosedTitleTextView, deviceClosedTitleTextView2);
@@ -92,22 +142,33 @@
@Test
public void testOpenDialogIsShown() {
- RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
- mCommandQueue, mFakeExecutor);
+ RearDisplayDialogController controller = new RearDisplayDialogController(
+ mCommandQueue,
+ mFakeExecutor,
+ mResources,
+ mLayoutInflater,
+ mSystemUIDialogFactory);
controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback());
controller.setFoldedStates(new int[]{0});
controller.setAnimationRepeatCount(0);
controller.showRearDisplayDialog(OPEN_BASE_STATE);
- assertTrue(controller.mRearDisplayEducationDialog.isShowing());
- TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
+ verify(mSystemUIDialog).show();
+ View container = getDialogViewContainer();
+ TextView deviceClosedTitleTextView = container.findViewById(
R.id.rear_display_title_text_view);
assertEquals(deviceClosedTitleTextView.getText().toString(),
getContext().getResources().getString(
R.string.rear_display_unfolded_bottom_sheet_title));
}
+ private View getDialogViewContainer() {
+ ArgumentCaptor<View> viewCaptor = ArgumentCaptor.forClass(View.class);
+ verify(mSystemUIDialog).setView(viewCaptor.capture());
+
+ return viewCaptor.getValue();
+ }
/**
* Empty device state manager callbacks, so we can verify that the correct
* dialogs are being created regardless of device state of the test device.
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorKosmos.kt
index b0d941d..a9d89a3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorKosmos.kt
@@ -26,7 +26,7 @@
import com.android.systemui.kosmos.applicationCoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
-val Kosmos.burnInInteractor by Fixture {
+var Kosmos.burnInInteractor by Fixture {
BurnInInteractor(
context = applicationContext,
burnInHelperWrapper = burnInHelperWrapper,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractorKosmos.kt
new file mode 100644
index 0000000..a3955f7
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractorKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.keyguardBottomAreaInteractor by Fixture {
+ KeyguardBottomAreaInteractor(
+ repository = keyguardRepository,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelKosmos.kt
new file mode 100644
index 0000000..6b89e0f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.aodAlphaViewModel by Fixture {
+ AodAlphaViewModel(
+ keyguardInteractor = keyguardInteractor,
+ keyguardTransitionInteractor = keyguardTransitionInteractor,
+ occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
new file mode 100644
index 0000000..35cfa89
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.keyguard.domain.interactor.burnInInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.aodBurnInViewModel by Fixture {
+ AodBurnInViewModel(
+ burnInInteractor = burnInInteractor,
+ configurationInteractor = configurationInteractor,
+ keyguardInteractor = keyguardInteractor,
+ keyguardTransitionInteractor = keyguardTransitionInteractor,
+ goneToAodTransitionViewModel = goneToAodTransitionViewModel,
+ occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
+ keyguardClockViewModel = keyguardClockViewModel,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelKosmos.kt
index 14e2cff..00ece14 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelKosmos.kt
@@ -25,7 +25,7 @@
import com.android.systemui.kosmos.Kosmos.Fixture
import kotlinx.coroutines.ExperimentalCoroutinesApi
-val Kosmos.goneToAodTransitionViewModel by Fixture {
+var Kosmos.goneToAodTransitionViewModel by Fixture {
GoneToAodTransitionViewModel(
interactor = keyguardTransitionInteractor,
deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt
index d878683..5ca0439 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt
@@ -20,6 +20,7 @@
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.splitShadeStateController
val Kosmos.keyguardClockViewModel by
Kosmos.Fixture {
@@ -27,5 +28,6 @@
keyguardInteractor = keyguardInteractor,
keyguardClockInteractor = keyguardClockInteractor,
applicationScope = applicationCoroutineScope,
+ splitShadeStateController = splitShadeStateController,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 13ee747..933f50c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -18,10 +18,7 @@
package com.android.systemui.keyguard.ui.viewmodel
-import com.android.systemui.common.ui.domain.interactor.configurationInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
-import com.android.systemui.flags.FakeFeatureFlagsClassic
-import com.android.systemui.keyguard.domain.interactor.burnInInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
@@ -33,18 +30,14 @@
val Kosmos.keyguardRootViewModel by Fixture {
KeyguardRootViewModel(
- configurationInteractor = configurationInteractor,
deviceEntryInteractor = deviceEntryInteractor,
dozeParameters = dozeParameters,
keyguardInteractor = keyguardInteractor,
keyguardTransitionInteractor = keyguardTransitionInteractor,
notificationsKeyguardInteractor = notificationsKeyguardInteractor,
- burnInInteractor = burnInInteractor,
- goneToAodTransitionViewModel = goneToAodTransitionViewModel,
aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
- occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
screenOffAnimationController = screenOffAnimationController,
- keyguardClockViewModel = keyguardClockViewModel,
- featureFlags = FakeFeatureFlagsClassic(),
+ aodBurnInViewModel = aodBurnInViewModel,
+ aodAlphaViewModel = aodAlphaViewModel,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt
index 5bbde2b..93ecb79 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt
@@ -26,7 +26,7 @@
import com.android.systemui.kosmos.Kosmos.Fixture
import kotlinx.coroutines.ExperimentalCoroutinesApi
-val Kosmos.occludedToLockscreenTransitionViewModel by Fixture {
+var Kosmos.occludedToLockscreenTransitionViewModel by Fixture {
OccludedToLockscreenTransitionViewModel(
interactor = keyguardTransitionInteractor,
deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt
index 0c9ce0f..697b508 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt
@@ -17,6 +17,7 @@
package com.android.systemui.util
+import android.content.Context
import android.content.DialogInterface
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.mockito.any
@@ -27,13 +28,15 @@
import org.mockito.Mockito.verify
import org.mockito.stubbing.Stubber
-class FakeSystemUIDialogController {
+class FakeSystemUIDialogController(context: Context) {
val dialog: SystemUIDialog = mock()
+
private val clickListeners: MutableMap<Int, DialogInterface.OnClickListener> = mutableMapOf()
init {
+ whenever(dialog.context).thenReturn(context)
saveListener(DialogInterface.BUTTON_POSITIVE)
.whenever(dialog)
.setPositiveButton(any(), any())
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt
index 843cc3b..54d8054 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt
@@ -41,8 +41,6 @@
if (inProgress) {
logCounter({ "$TAG#filtered_progress" }, newProgress)
listener.onTransitionProgress(newProgress)
- } else {
- Log.e(TAG, "Filtered progress received received while animation not in progress.")
}
field = newProgress
}
diff --git a/packages/overlays/Android.bp b/packages/overlays/Android.bp
new file mode 100644
index 0000000..5e001fb
--- /dev/null
+++ b/packages/overlays/Android.bp
@@ -0,0 +1,44 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: [
+ "frameworks_base_license",
+ ],
+}
+
+phony {
+ name: "frameworks-base-overlays",
+ required: [
+ "DisplayCutoutEmulationCornerOverlay",
+ "DisplayCutoutEmulationDoubleOverlay",
+ "DisplayCutoutEmulationHoleOverlay",
+ "DisplayCutoutEmulationTallOverlay",
+ "DisplayCutoutEmulationWaterfallOverlay",
+ "FontNotoSerifSourceOverlay",
+ "NavigationBarMode3ButtonOverlay",
+ "NavigationBarModeGesturalOverlay",
+ "NavigationBarModeGesturalOverlayNarrowBack",
+ "NavigationBarModeGesturalOverlayWideBack",
+ "NavigationBarModeGesturalOverlayExtraWideBack",
+ "TransparentNavigationBarOverlay",
+ "NotesRoleEnabledOverlay",
+ "preinstalled-packages-platform-overlays.xml",
+ ],
+}
+
+phony {
+ name: "frameworks-base-overlays-debug",
+}
diff --git a/packages/overlays/Android.mk b/packages/overlays/Android.mk
deleted file mode 100644
index a41d0e5..0000000
--- a/packages/overlays/Android.mk
+++ /dev/null
@@ -1,47 +0,0 @@
-# Copyright (C) 2019 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := frameworks-base-overlays
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../NOTICE
-LOCAL_REQUIRED_MODULES := \
- DisplayCutoutEmulationCornerOverlay \
- DisplayCutoutEmulationDoubleOverlay \
- DisplayCutoutEmulationHoleOverlay \
- DisplayCutoutEmulationTallOverlay \
- DisplayCutoutEmulationWaterfallOverlay \
- FontNotoSerifSourceOverlay \
- NavigationBarMode3ButtonOverlay \
- NavigationBarModeGesturalOverlay \
- NavigationBarModeGesturalOverlayNarrowBack \
- NavigationBarModeGesturalOverlayWideBack \
- NavigationBarModeGesturalOverlayExtraWideBack \
- TransparentNavigationBarOverlay \
- NotesRoleEnabledOverlay \
- preinstalled-packages-platform-overlays.xml
-
-include $(BUILD_PHONY_PACKAGE)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := frameworks-base-overlays-debug
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../NOTICE
-
-include $(BUILD_PHONY_PACKAGE)
-include $(call first-makefiles-under,$(LOCAL_PATH))
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index a19920f..993b254 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -59,13 +59,6 @@
}
flag {
- name: "reduce_touch_exploration_sensitivity"
- namespace: "accessibility"
- description: "Reduces touch exploration sensitivity by only sending a hover event when the ifnger has moved the amount of pixels defined by the system's touch slop."
- bug: "303677860"
-}
-
-flag {
name: "scan_packages_without_lock"
namespace: "accessibility"
description: "Scans packages for accessibility service/activity info without holding the A11yMS lock"
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index fc8d4f8..c418485 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -882,22 +882,10 @@
final int pointerIndex = event.findPointerIndex(pointerId);
switch (event.getPointerCount()) {
case 1:
- // Touch exploration.
+ // Touch exploration.
sendTouchExplorationGestureStartAndHoverEnterIfNeeded(policyFlags);
- if (Flags.reduceTouchExplorationSensitivity()
- && mState.getLastInjectedHoverEvent() != null) {
- final MotionEvent lastEvent = mState.getLastInjectedHoverEvent();
- final float deltaX = lastEvent.getX() - rawEvent.getX();
- final float deltaY = lastEvent.getY() - rawEvent.getY();
- final double moveDelta = Math.hypot(deltaX, deltaY);
- if (moveDelta > mTouchSlop) {
- mDispatcher.sendMotionEvent(
- event, ACTION_HOVER_MOVE, rawEvent, pointerIdBits, policyFlags);
- }
- } else {
- mDispatcher.sendMotionEvent(
- event, ACTION_HOVER_MOVE, rawEvent, pointerIdBits, policyFlags);
- }
+ mDispatcher.sendMotionEvent(
+ event, ACTION_HOVER_MOVE, rawEvent, pointerIdBits, policyFlags);
break;
case 2:
if (mGestureDetector.isMultiFingerGesturesEnabled()
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index 6964763..a6ed846 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -37,6 +37,7 @@
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
+import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.companion.AssociatedDevice;
import android.companion.AssociationInfo;
@@ -285,32 +286,33 @@
selfManaged, /* notifyOnDeviceNearby */ false, /* revoked */ false,
timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0);
- if (deviceProfile != null) {
- // If the "Device Profile" is specified, make the companion application a holder of the
- // corresponding role.
- addRoleHolderForAssociation(mService.getContext(), association, success -> {
- if (success) {
- addAssociationToStore(association, deviceProfile);
-
- sendCallbackAndFinish(association, callback, resultReceiver);
- } else {
- Slog.e(TAG, "Failed to add u" + userId + "\\" + packageName
- + " to the list of " + deviceProfile + " holders.");
-
- sendCallbackAndFinish(null, callback, resultReceiver);
- }
- });
- } else {
- addAssociationToStore(association, null);
-
- sendCallbackAndFinish(association, callback, resultReceiver);
- }
+ // Add role holder for association (if specified) and add new association to store.
+ maybeGrantRoleAndStoreAssociation(association, callback, resultReceiver);
// Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since
// maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case
// that there are other devices with the same profile, so the role holder won't be removed.
}
+ public void maybeGrantRoleAndStoreAssociation(@NonNull AssociationInfo association,
+ @Nullable IAssociationRequestCallback callback,
+ @Nullable ResultReceiver resultReceiver) {
+ // If the "Device Profile" is specified, make the companion application a holder of the
+ // corresponding role.
+ // If it is null, then the operation will succeed without granting any role.
+ addRoleHolderForAssociation(mService.getContext(), association, success -> {
+ if (success) {
+ addAssociationToStore(association);
+ sendCallbackAndFinish(association, callback, resultReceiver);
+ } else {
+ Slog.e(TAG, "Failed to add u" + association.getUserId()
+ + "\\" + association.getPackageName()
+ + " to the list of " + association.getDeviceProfile() + " holders.");
+ sendCallbackAndFinish(null, callback, resultReceiver);
+ }
+ });
+ }
+
public void enableSystemDataSync(int associationId, int flags) {
AssociationInfo association = mAssociationStore.getAssociationById(associationId);
AssociationInfo updated = (new AssociationInfo.Builder(association))
@@ -325,15 +327,14 @@
mAssociationStore.updateAssociation(updated);
}
- private void addAssociationToStore(@NonNull AssociationInfo association,
- @Nullable String deviceProfile) {
+ private void addAssociationToStore(@NonNull AssociationInfo association) {
Slog.i(TAG, "New CDM association created=" + association);
mAssociationStore.addAssociation(association);
mService.updateSpecialAccessPermissionForAssociatedPackage(association);
- logCreateAssociation(deviceProfile);
+ logCreateAssociation(association.getDeviceProfile());
}
private void sendCallbackAndFinish(@Nullable AssociationInfo association,
@@ -398,7 +399,11 @@
pendingIntent = PendingIntent.getActivityAsUser(
mContext, /*requestCode */ packageUid, intent,
FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE,
- /* options= */ null, UserHandle.CURRENT);
+ ActivityOptions.makeBasic()
+ .setPendingIntentCreatorBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+ .toBundle(),
+ UserHandle.CURRENT);
} finally {
Binder.restoreCallingIdentity(token);
}
diff --git a/services/companion/java/com/android/server/companion/RolesUtils.java b/services/companion/java/com/android/server/companion/RolesUtils.java
index 163f614..af9d2d7 100644
--- a/services/companion/java/com/android/server/companion/RolesUtils.java
+++ b/services/companion/java/com/android/server/companion/RolesUtils.java
@@ -47,6 +47,17 @@
return roleHolders.contains(packageName);
}
+ /**
+ * Attempt to add the association's companion app as the role holder for the device profile
+ * specified in the association. If the association does not have any device profile specified,
+ * then the operation will always be successful as a no-op.
+ *
+ * @param context
+ * @param associationInfo the association for which the role should be granted to the app
+ * @param roleGrantResult the result callback for adding role holder. True if successful, and
+ * false if failed. If the association does not have any device profile
+ * specified, then the operation will always be successful as a no-op.
+ */
static void addRoleHolderForAssociation(
@NonNull Context context, @NonNull AssociationInfo associationInfo,
@NonNull Consumer<Boolean> roleGrantResult) {
@@ -55,7 +66,11 @@
}
final String deviceProfile = associationInfo.getDeviceProfile();
- if (deviceProfile == null) return;
+ if (deviceProfile == null) {
+ // If no device profile is specified, then no-op and resolve callback with success.
+ roleGrantResult.accept(true);
+ return;
+ }
final RoleManager roleManager = context.getSystemService(RoleManager.class);
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
index bd646fa..4e471f5 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
@@ -27,6 +27,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.companion.AssociationInfo;
import android.companion.DeviceNotAssociatedException;
@@ -186,7 +187,11 @@
final long token = Binder.clearCallingIdentity();
try {
return PendingIntent.getActivityAsUser(mContext, /*requestCode */ associationId, intent,
- FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, /* options= */ null,
+ FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE,
+ ActivityOptions.makeBasic()
+ .setPendingIntentCreatorBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+ .toBundle(),
UserHandle.CURRENT);
} finally {
Binder.restoreCallingIdentity(token);
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 8ad60e6..72e62c3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -243,7 +243,7 @@
/**
* The default value to {@link #KEY_ENABLE_NEW_OOMADJ}.
*/
- private static final boolean DEFAULT_ENABLE_NEW_OOM_ADJ = Flags.oomadjusterCorrectnessRewrite();
+ private static final boolean DEFAULT_ENABLE_NEW_OOM_ADJ = false;
/**
* Same as {@link TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED}
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 848a2b0..57c52c2 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -402,7 +402,7 @@
case "get-bg-restriction-level":
return runGetBgRestrictionLevel(pw);
case "observe-foreground-process":
- return runGetCurrentForegroundProcess(pw, mInternal, mTaskInterface);
+ return runGetCurrentForegroundProcess(pw, mInternal);
case "reset-dropbox-rate-limiter":
return runResetDropboxRateLimiter();
case "list-displays-for-starting-users":
@@ -3690,11 +3690,10 @@
return -1;
}
- private int runGetCurrentForegroundProcess(PrintWriter pw,
- IActivityManager iam, IActivityTaskManager iatm)
+ private int runGetCurrentForegroundProcess(PrintWriter pw, IActivityManager iam)
throws RemoteException {
- ProcessObserver observer = new ProcessObserver(pw, iam, iatm, mInternal);
+ ProcessObserver observer = new ProcessObserver(pw, iam);
iam.registerProcessObserver(observer);
final InputStream mInput = getRawInputStream();
@@ -3729,15 +3728,10 @@
private PrintWriter mPw;
private IActivityManager mIam;
- private IActivityTaskManager mIatm;
- private ActivityManagerService mInternal;
- ProcessObserver(PrintWriter mPw, IActivityManager mIam,
- IActivityTaskManager mIatm, ActivityManagerService ams) {
+ ProcessObserver(PrintWriter mPw, IActivityManager mIam) {
this.mPw = mPw;
this.mIam = mIam;
- this.mIatm = mIatm;
- this.mInternal = ams;
}
@Override
diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING
index 575db01..e90910a 100644
--- a/services/core/java/com/android/server/am/TEST_MAPPING
+++ b/services/core/java/com/android/server/am/TEST_MAPPING
@@ -146,6 +146,15 @@
{ "include-filter": "android.app.cts.ServiceTest" },
{ "include-filter": "android.app.cts.ActivityManagerFgsBgStartTest" }
]
+ },
+ {
+ "name": "CtsStatsdAtomHostTestCases",
+ "options": [
+ { "include-filter": "android.cts.statsdatom.appexit.AppExitHostTest" },
+ { "exclude-annotation": "androidx.test.filters.LargeTest" },
+ { "exclude-annotation": "androidx.test.filters.FlakyTest" },
+ { "exclude-annotation": "org.junit.Ignore" }
+ ]
}
]
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
index 0bb61415..90da74c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
@@ -147,7 +147,7 @@
gestureAvailabilityDispatcher, () -> mCurrentUserId, getUserSwitchCallback()));
mLockoutTracker = new LockoutFrameworkImpl(getContext(),
userId -> mLockoutResetDispatcher.notifyLockoutResetCallbacks(
- getSensorProperties().sensorId));
+ getSensorProperties().sensorId), getHandler());
}
@Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
index 2f77275..0e05a79 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission.RESET_FINGERPRINT_LOCKOUT;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
@@ -81,19 +82,30 @@
@NonNull LockoutResetCallback lockoutResetCallback) {
this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId,
new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId),
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE),
+ null /* handler */);
+ }
+
+ public LockoutFrameworkImpl(@NonNull Context context,
+ @NonNull LockoutResetCallback lockoutResetCallback,
+ @NonNull Handler handler) {
+ this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId,
+ new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId),
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE),
+ handler);
}
@VisibleForTesting
LockoutFrameworkImpl(@NonNull Context context,
@NonNull LockoutResetCallback lockoutResetCallback,
- @NonNull Function<Integer, PendingIntent> lockoutResetIntent) {
+ @NonNull Function<Integer, PendingIntent> lockoutResetIntent,
+ @Nullable Handler handler) {
mLockoutResetCallback = lockoutResetCallback;
mTimedLockoutCleared = new SparseBooleanArray();
mFailedAttempts = new SparseIntArray();
mAlarmManager = context.getSystemService(AlarmManager.class);
mLockoutReceiver = new LockoutReceiver();
- mHandler = new Handler(Looper.getMainLooper());
+ mHandler = handler == null ? new Handler(Looper.getMainLooper()) : handler;
mLockoutResetIntent = lockoutResetIntent;
context.registerReceiver(mLockoutReceiver, new IntentFilter(ACTION_LOCKOUT_RESET),
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index 56a94ec0..49f6070 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -1424,7 +1424,11 @@
String defaultIme = Settings.Secure.getStringForUser(getContext().getContentResolver(),
Settings.Secure.DEFAULT_INPUT_METHOD, userId);
if (!TextUtils.isEmpty(defaultIme)) {
- final String imePkg = ComponentName.unflattenFromString(defaultIme).getPackageName();
+ final ComponentName imeComponent = ComponentName.unflattenFromString(defaultIme);
+ if (imeComponent == null) {
+ return false;
+ }
+ final String imePkg = imeComponent.getPackageName();
return imePkg.equals(packageName);
}
return false;
diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
index 66807ae..f96bb8fb 100644
--- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
+++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
@@ -52,6 +52,7 @@
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
+import java.util.function.IntConsumer;
// TODO(b/210039666): See if we can make this class thread-safe.
final class HandwritingModeController {
@@ -84,14 +85,14 @@
private boolean mDelegatorFromDefaultHomePackage;
private Runnable mDelegationIdleTimeoutRunnable;
private Handler mDelegationIdleTimeoutHandler;
-
+ private IntConsumer mPointerToolTypeConsumer;
private HandwritingEventReceiverSurface mHandwritingSurface;
private int mCurrentRequestId;
@AnyThread
HandwritingModeController(Context context, Looper uiThreadLooper,
- Runnable inkWindowInitRunnable) {
+ Runnable inkWindowInitRunnable, IntConsumer toolTypeConsumer) {
mContext = context;
mLooper = uiThreadLooper;
mCurrentDisplayId = Display.INVALID_DISPLAY;
@@ -100,6 +101,7 @@
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mCurrentRequestId = 0;
mInkWindowInitRunnable = inkWindowInitRunnable;
+ mPointerToolTypeConsumer = toolTypeConsumer;
}
/**
@@ -355,6 +357,11 @@
return false;
}
final MotionEvent event = (MotionEvent) ev;
+ if (mPointerToolTypeConsumer != null && event.getAction() == MotionEvent.ACTION_DOWN) {
+ int toolType = event.getToolType(event.getActionIndex());
+ // notify IME of change in tool type.
+ mPointerToolTypeConsumer.accept(toolType);
+ }
if (!event.isStylusPointer()) {
return false;
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index c4d94ee..24bcb4e 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -124,6 +124,7 @@
import android.view.WindowManager.LayoutParams;
import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
import android.view.inputmethod.InputConnection;
@@ -206,6 +207,7 @@
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.IntConsumer;
/**
* This class provides a system service that manages input methods.
@@ -276,7 +278,7 @@
final Context mContext;
final Resources mRes;
private final Handler mHandler;
- final InputMethodSettings mSettings;
+ private final InputMethodSettings mSettings;
final SettingsObserver mSettingsObserver;
private final SparseBooleanArray mLoggedDeniedGetInputMethodWindowVisibleHeightForUid =
new SparseBooleanArray(0);
@@ -316,7 +318,7 @@
// Mapping from deviceId to the device-specific imeId for that device.
private final SparseArray<String> mVirtualDeviceMethodMap = new SparseArray<>();
- final InputMethodSubtypeSwitchingController mSwitchingController;
+ private final InputMethodSubtypeSwitchingController mSwitchingController;
final HardwareKeyboardShortcutController mHardwareKeyboardShortcutController =
new HardwareKeyboardShortcutController();
@@ -1713,8 +1715,11 @@
com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
mNonPreemptibleInputMethods = mRes.getStringArray(
com.android.internal.R.array.config_nonPreemptibleInputMethods);
+ IntConsumer toolTypeConsumer =
+ Flags.useHandwritingListenerForTooltype()
+ ? toolType -> onUpdateEditorToolType(toolType) : null;
mHwController = new HandwritingModeController(mContext, thread.getLooper(),
- new InkWindowInitializer());
+ new InkWindowInitializer(), toolTypeConsumer);
registerDeviceListenerAndCheckStylusSupport();
}
@@ -1735,6 +1740,15 @@
}
}
+ private void onUpdateEditorToolType(int toolType) {
+ synchronized (ImfLock.class) {
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod != null) {
+ curMethod.updateEditorToolType(toolType);
+ }
+ }
+ }
+
@GuardedBy("ImfLock.class")
private void resetDefaultImeLocked(Context context) {
// Do not reset the default (current) IME when it is a 3rd-party IME
@@ -3525,7 +3539,8 @@
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME);
mCurStatsToken = null;
- if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) {
+ if (!Flags.useHandwritingListenerForTooltype()
+ && lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) {
curMethod.updateEditorToolType(lastClickToolType);
}
mVisibilityApplier.performShowIme(windowToken, statsToken,
@@ -4812,7 +4827,20 @@
Slog.e(TAG, "Unknown subtype picker mode = " + msg.arg1);
return false;
}
- mMenuController.showInputMethodMenu(showAuxSubtypes, displayId);
+ synchronized (ImfLock.class) {
+ final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked()
+ && mWindowManagerInternal.isKeyguardSecure(
+ mSettings.getCurrentUserId());
+ final String lastInputMethodId = mSettings.getSelectedInputMethod();
+ int lastInputMethodSubtypeId =
+ mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId);
+
+ final List<ImeSubtypeListItem> imList = mSwitchingController
+ .getSortedInputMethodAndSubtypeListForImeMenuLocked(
+ showAuxSubtypes, isScreenLocked);
+ mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
+ lastInputMethodId, lastInputMethodSubtypeId, imList);
+ }
return true;
// ---------------------------------------------------------
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
index efa1e0d..6ed4848 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
@@ -19,12 +19,14 @@
import static com.android.server.inputmethod.InputMethodManagerService.DEBUG;
import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
+import android.provider.Settings;
import android.text.TextUtils;
import android.util.Slog;
import android.view.LayoutInflater;
@@ -51,8 +53,6 @@
private static final String TAG = InputMethodMenuController.class.getSimpleName();
private final InputMethodManagerService mService;
- private final InputMethodUtils.InputMethodSettings mSettings;
- private final InputMethodSubtypeSwitchingController mSwitchingController;
private final WindowManagerInternal mWindowManagerInternal;
private AlertDialog.Builder mDialogBuilder;
@@ -69,145 +69,141 @@
InputMethodMenuController(InputMethodManagerService service) {
mService = service;
- mSettings = mService.mSettings;
- mSwitchingController = mService.mSwitchingController;
mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
}
- void showInputMethodMenu(boolean showAuxSubtypes, int displayId) {
+ @GuardedBy("ImfLock.class")
+ void showInputMethodMenuLocked(boolean showAuxSubtypes, int displayId,
+ String preferredInputMethodId, int preferredInputMethodSubtypeId,
+ @NonNull List<ImeSubtypeListItem> imList) {
if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes);
- synchronized (ImfLock.class) {
- final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked()
- && mWindowManagerInternal.isKeyguardSecure(
- mService.getCurrentImeUserIdLocked());
- final String lastInputMethodId = mSettings.getSelectedInputMethod();
- int lastInputMethodSubtypeId =
- mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId);
- if (DEBUG) Slog.v(TAG, "Current IME: " + lastInputMethodId);
+ final int userId = mService.getCurrentImeUserIdLocked();
- final List<ImeSubtypeListItem> imList = mSwitchingController
- .getSortedInputMethodAndSubtypeListForImeMenuLocked(
- showAuxSubtypes, isScreenLocked);
- if (imList.isEmpty()) {
- return;
- }
-
- hideInputMethodMenuLocked();
-
- if (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID) {
- final InputMethodSubtype currentSubtype =
- mService.getCurrentInputMethodSubtypeLocked();
- if (currentSubtype != null) {
- final String curMethodId = mService.getSelectedMethodIdLocked();
- final InputMethodInfo currentImi =
- mService.queryInputMethodForCurrentUserLocked(curMethodId);
- lastInputMethodSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(
- currentImi, currentSubtype.hashCode());
- }
- }
-
- final int size = imList.size();
- mIms = new InputMethodInfo[size];
- mSubtypeIds = new int[size];
- int checkedItem = 0;
- for (int i = 0; i < size; ++i) {
- final ImeSubtypeListItem item = imList.get(i);
- mIms[i] = item.mImi;
- mSubtypeIds[i] = item.mSubtypeId;
- if (mIms[i].getId().equals(lastInputMethodId)) {
- int subtypeId = mSubtypeIds[i];
- if ((subtypeId == NOT_A_SUBTYPE_ID)
- || (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0)
- || (subtypeId == lastInputMethodSubtypeId)) {
- checkedItem = i;
- }
- }
- }
-
- if (mDialogWindowContext == null) {
- mDialogWindowContext = new InputMethodDialogWindowContext();
- }
- final Context dialogWindowContext = mDialogWindowContext.get(displayId);
- mDialogBuilder = new AlertDialog.Builder(dialogWindowContext);
- mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu());
-
- final Context dialogContext = mDialogBuilder.getContext();
- final TypedArray a = dialogContext.obtainStyledAttributes(null,
- com.android.internal.R.styleable.DialogPreference,
- com.android.internal.R.attr.alertDialogStyle, 0);
- final Drawable dialogIcon = a.getDrawable(
- com.android.internal.R.styleable.DialogPreference_dialogIcon);
- a.recycle();
-
- mDialogBuilder.setIcon(dialogIcon);
-
- final LayoutInflater inflater = dialogContext.getSystemService(LayoutInflater.class);
- final View tv = inflater.inflate(
- com.android.internal.R.layout.input_method_switch_dialog_title, null);
- mDialogBuilder.setCustomTitle(tv);
-
- // Setup layout for a toggle switch of the hardware keyboard
- mSwitchingDialogTitleView = tv;
- mSwitchingDialogTitleView
- .findViewById(com.android.internal.R.id.hard_keyboard_section)
- .setVisibility(mWindowManagerInternal.isHardKeyboardAvailable()
- ? View.VISIBLE : View.GONE);
- final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById(
- com.android.internal.R.id.hard_keyboard_switch);
- hardKeySwitch.setChecked(mShowImeWithHardKeyboard);
- hardKeySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
- mSettings.setShowImeWithHardKeyboard(isChecked);
- // Ensure that the input method dialog is dismissed when changing
- // the hardware keyboard state.
- hideInputMethodMenu();
- });
-
- final ImeSubtypeListAdapter adapter = new ImeSubtypeListAdapter(dialogContext,
- com.android.internal.R.layout.input_method_switch_item, imList, checkedItem);
- final DialogInterface.OnClickListener choiceListener = (dialog, which) -> {
- synchronized (ImfLock.class) {
- if (mIms == null || mIms.length <= which || mSubtypeIds == null
- || mSubtypeIds.length <= which) {
- return;
- }
- final InputMethodInfo im = mIms[which];
- int subtypeId = mSubtypeIds[which];
- adapter.mCheckedItem = which;
- adapter.notifyDataSetChanged();
- if (im != null) {
- if (subtypeId < 0 || subtypeId >= im.getSubtypeCount()) {
- subtypeId = NOT_A_SUBTYPE_ID;
- }
- mService.setInputMethodLocked(im.getId(), subtypeId);
- }
- hideInputMethodMenuLocked();
- }
- };
- mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, choiceListener);
-
- mSwitchingDialog = mDialogBuilder.create();
- mSwitchingDialog.setCanceledOnTouchOutside(true);
- final Window w = mSwitchingDialog.getWindow();
- final WindowManager.LayoutParams attrs = w.getAttributes();
- w.setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG);
- w.setHideOverlayWindows(true);
- // Use an alternate token for the dialog for that window manager can group the token
- // with other IME windows based on type vs. grouping based on whichever token happens
- // to get selected by the system later on.
- attrs.token = dialogWindowContext.getWindowContextToken();
- attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
- attrs.setTitle("Select input method");
- w.setAttributes(attrs);
- mService.updateSystemUiLocked();
- mService.sendOnNavButtonFlagsChangedLocked();
- mSwitchingDialog.show();
-
+ if (imList.isEmpty()) {
+ return;
}
+
+ hideInputMethodMenuLocked();
+
+ if (preferredInputMethodSubtypeId == NOT_A_SUBTYPE_ID) {
+ final InputMethodSubtype currentSubtype =
+ mService.getCurrentInputMethodSubtypeLocked();
+ if (currentSubtype != null) {
+ final String curMethodId = mService.getSelectedMethodIdLocked();
+ final InputMethodInfo currentImi =
+ mService.queryInputMethodForCurrentUserLocked(curMethodId);
+ preferredInputMethodSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(
+ currentImi, currentSubtype.hashCode());
+ }
+ }
+
+ // Find out which item should be checked by default.
+ final int size = imList.size();
+ mIms = new InputMethodInfo[size];
+ mSubtypeIds = new int[size];
+ int checkedItem = 0;
+ for (int i = 0; i < size; ++i) {
+ final ImeSubtypeListItem item = imList.get(i);
+ mIms[i] = item.mImi;
+ mSubtypeIds[i] = item.mSubtypeId;
+ if (mIms[i].getId().equals(preferredInputMethodId)) {
+ int subtypeId = mSubtypeIds[i];
+ if ((subtypeId == NOT_A_SUBTYPE_ID)
+ || (preferredInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0)
+ || (subtypeId == preferredInputMethodSubtypeId)) {
+ checkedItem = i;
+ }
+ }
+ }
+
+ if (mDialogWindowContext == null) {
+ mDialogWindowContext = new InputMethodDialogWindowContext();
+ }
+ final Context dialogWindowContext = mDialogWindowContext.get(displayId);
+ mDialogBuilder = new AlertDialog.Builder(dialogWindowContext);
+ mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu());
+
+ final Context dialogContext = mDialogBuilder.getContext();
+ final TypedArray a = dialogContext.obtainStyledAttributes(null,
+ com.android.internal.R.styleable.DialogPreference,
+ com.android.internal.R.attr.alertDialogStyle, 0);
+ final Drawable dialogIcon = a.getDrawable(
+ com.android.internal.R.styleable.DialogPreference_dialogIcon);
+ a.recycle();
+
+ mDialogBuilder.setIcon(dialogIcon);
+
+ final LayoutInflater inflater = dialogContext.getSystemService(LayoutInflater.class);
+ final View tv = inflater.inflate(
+ com.android.internal.R.layout.input_method_switch_dialog_title, null);
+ mDialogBuilder.setCustomTitle(tv);
+
+ // Setup layout for a toggle switch of the hardware keyboard
+ mSwitchingDialogTitleView = tv;
+ mSwitchingDialogTitleView
+ .findViewById(com.android.internal.R.id.hard_keyboard_section)
+ .setVisibility(mWindowManagerInternal.isHardKeyboardAvailable()
+ ? View.VISIBLE : View.GONE);
+ final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById(
+ com.android.internal.R.id.hard_keyboard_switch);
+ hardKeySwitch.setChecked(mShowImeWithHardKeyboard);
+ hardKeySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ SecureSettingsWrapper.putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
+ isChecked, userId);
+ // Ensure that the input method dialog is dismissed when changing
+ // the hardware keyboard state.
+ hideInputMethodMenu();
+ });
+
+ // Fill the list items with onClick listener, which takes care of IME (and subtype)
+ // switching when clicked.
+ final ImeSubtypeListAdapter adapter = new ImeSubtypeListAdapter(dialogContext,
+ com.android.internal.R.layout.input_method_switch_item, imList, checkedItem);
+ final DialogInterface.OnClickListener choiceListener = (dialog, which) -> {
+ synchronized (ImfLock.class) {
+ if (mIms == null || mIms.length <= which || mSubtypeIds == null
+ || mSubtypeIds.length <= which) {
+ return;
+ }
+ final InputMethodInfo im = mIms[which];
+ int subtypeId = mSubtypeIds[which];
+ adapter.mCheckedItem = which;
+ adapter.notifyDataSetChanged();
+ if (im != null) {
+ if (subtypeId < 0 || subtypeId >= im.getSubtypeCount()) {
+ subtypeId = NOT_A_SUBTYPE_ID;
+ }
+ mService.setInputMethodLocked(im.getId(), subtypeId);
+ }
+ hideInputMethodMenuLocked();
+ }
+ };
+ mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, choiceListener);
+
+ // Final steps to instantiate a dialog to show it up.
+ mSwitchingDialog = mDialogBuilder.create();
+ mSwitchingDialog.setCanceledOnTouchOutside(true);
+ final Window w = mSwitchingDialog.getWindow();
+ final WindowManager.LayoutParams attrs = w.getAttributes();
+ w.setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG);
+ w.setHideOverlayWindows(true);
+ // Use an alternate token for the dialog for that window manager can group the token
+ // with other IME windows based on type vs. grouping based on whichever token happens
+ // to get selected by the system later on.
+ attrs.token = dialogWindowContext.getWindowContextToken();
+ attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ attrs.setTitle("Select input method");
+ w.setAttributes(attrs);
+ mService.updateSystemUiLocked();
+ mService.sendOnNavButtonFlagsChangedLocked();
+ mSwitchingDialog.show();
}
void updateKeyboardFromSettingsLocked() {
- mShowImeWithHardKeyboard = mSettings.isShowImeWithHardKeyboardEnabled();
+ mShowImeWithHardKeyboard =
+ SecureSettingsWrapper.getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
+ false, mService.getCurrentImeUserIdLocked());
if (mSwitchingDialog != null && mSwitchingDialogTitleView != null
&& mSwitchingDialog.isShowing()) {
final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById(
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
index 547fd2f..a0b55ed 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
@@ -660,14 +660,6 @@
return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID);
}
- boolean isShowImeWithHardKeyboardEnabled() {
- return getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, false);
- }
-
- void setShowImeWithHardKeyboard(boolean show) {
- putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, show);
- }
-
@UserIdInt
public int getCurrentUserId() {
return mCurrentUserId;
diff --git a/services/core/java/com/android/server/location/gnss/GnssConfiguration.java b/services/core/java/com/android/server/location/gnss/GnssConfiguration.java
index 5ef89ad..a5939e9 100644
--- a/services/core/java/com/android/server/location/gnss/GnssConfiguration.java
+++ b/services/core/java/com/android/server/location/gnss/GnssConfiguration.java
@@ -17,6 +17,7 @@
package com.android.server.location.gnss;
import android.content.Context;
+import android.location.flags.Flags;
import android.os.PersistableBundle;
import android.os.SystemProperties;
import android.telephony.CarrierConfigManager;
@@ -36,6 +37,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
@@ -275,6 +277,11 @@
}
loadPropertiesFromCarrierConfig(inEmergency, activeSubId);
+ if (Flags.gnssConfigurationFromResource()) {
+ // Overlay carrier properties from resources.
+ loadPropertiesFromResource(mContext, mProperties);
+ }
+
if (isSimAbsent(mContext)) {
// Use the default SIM's LPP profile when SIM is absent.
String lpp_prof = SystemProperties.get(LPP_PROFILE);
@@ -382,7 +389,7 @@
if (configKey.startsWith(CarrierConfigManager.Gps.KEY_PREFIX)) {
String key = configKey
.substring(CarrierConfigManager.Gps.KEY_PREFIX.length())
- .toUpperCase();
+ .toUpperCase(Locale.ROOT);
Object value = configs.get(configKey);
if (DEBUG) Log.d(TAG, "Gps config: " + key + " = " + value);
if (value instanceof String) {
@@ -410,6 +417,24 @@
}
}
+ private void loadPropertiesFromResource(Context context,
+ Properties properties) {
+ String[] configValues = context.getResources().getStringArray(
+ com.android.internal.R.array.config_gnssParameters);
+ for (String item : configValues) {
+ if (DEBUG) Log.d(TAG, "GnssParamsResource: " + item);
+ // We need to support "KEY =", but not "=VALUE".
+ int index = item.indexOf("=");
+ if (index > 0 && index + 1 < item.length()) {
+ String key = item.substring(0, index);
+ String value = item.substring(index + 1);
+ properties.setProperty(key.trim().toUpperCase(Locale.ROOT), value);
+ } else {
+ Log.w(TAG, "malformed contents: " + item);
+ }
+ }
+ }
+
private int getRangeCheckedConfigEsExtensionSec() {
int emergencyExtensionSeconds = getIntConfig(CONFIG_ES_EXTENSION_SEC, 0);
if (emergencyExtensionSeconds > MAX_EMERGENCY_MODE_EXTENSION_SECONDS) {
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
index ae889d8..21e7bef 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
@@ -231,18 +231,21 @@
}
private boolean shouldBind() {
- if (mRunning) {
- boolean shouldBind =
- mLastDiscoveryPreference != null
- && !mLastDiscoveryPreference.getPreferredFeatures().isEmpty();
- if (mIsSelfScanOnlyProvider) {
- shouldBind &= mLastDiscoveryPreferenceIncludesThisPackage;
- }
- shouldBind |= mIsManagerScanning;
- shouldBind |= !getSessionInfos().isEmpty();
- return shouldBind;
+ if (!mRunning) {
+ return false;
}
- return false;
+ if (!getSessionInfos().isEmpty() || mIsManagerScanning) {
+ // We bind if any manager is scanning (regardless of whether an app is scanning) to give
+ // the opportunity for providers to publish routing sessions that were established
+ // directly between the app and the provider (typically via AndroidX MediaRouter). See
+ // b/176774510#comment20 for more information.
+ return true;
+ }
+ boolean anAppIsScanning =
+ mLastDiscoveryPreference != null
+ && !mLastDiscoveryPreference.getPreferredFeatures().isEmpty();
+ return anAppIsScanning
+ && (mLastDiscoveryPreferenceIncludesThisPackage || !mIsSelfScanOnlyProvider);
}
private void bind() {
diff --git a/services/core/java/com/android/server/pdb/TEST_MAPPING b/services/core/java/com/android/server/pdb/TEST_MAPPING
index 1aa8601..9e98023 100644
--- a/services/core/java/com/android/server/pdb/TEST_MAPPING
+++ b/services/core/java/com/android/server/pdb/TEST_MAPPING
@@ -1,5 +1,5 @@
{
- "postsubmit": [
+ "presubmit": [
{
"name": "FrameworksServicesTests",
"options": [
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 2305d6c..75b4531 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2285,6 +2285,11 @@
throw new SecurityException("You need MANAGE_USERS permission to query if u=" + userId
+ " is a demo user");
}
+
+ if (SystemProperties.getBoolean("ro.boot.arc_demo_mode", false)) {
+ return true;
+ }
+
synchronized (mUsersLock) {
UserInfo userInfo = getUserInfoLU(userId);
return userInfo != null && userInfo.isDemo();
diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java
index 7b5192c..e3aba0f 100644
--- a/services/core/java/com/android/server/utils/AnrTimer.java
+++ b/services/core/java/com/android/server/utils/AnrTimer.java
@@ -16,21 +16,30 @@
package com.android.server.utils;
+import static android.text.TextUtils.formatSimple;
+
import android.annotation.NonNull;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.os.Trace;
+import android.text.TextUtils;
import android.text.format.TimeMigrationUtils;
+import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.Keep;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.RingBuffer;
+import java.lang.ref.WeakReference;
import java.io.PrintWriter;
import java.util.Arrays;
+import java.util.ArrayList;
import java.util.Objects;
/**
@@ -60,9 +69,14 @@
* is restarted with the extension timeout. If extensions are disabled or if the extension is zero,
* the client process is notified of the expiration.
*
+ * <p>Instances use native resources but not system resources when the feature is enabled.
+ * Instances should be explicitly closed unless they are being closed as part of process
+ * exit. (So, instances in system server generally need not be explicitly closed since they are
+ * created during process start and will last until process exit.)
+ *
* @hide
*/
-public class AnrTimer<V> {
+public class AnrTimer<V> implements AutoCloseable {
/**
* The log tag.
@@ -87,6 +101,12 @@
private static final long TRACE_TAG = Trace.TRACE_TAG_ACTIVITY_MANAGER;
/**
+ * Enable tracing from the time a timer expires until it is accepted or discarded. This is
+ * used to diagnose long latencies in the client.
+ */
+ private static final boolean ENABLE_TRACING = false;
+
+ /**
* Return true if the feature is enabled. By default, the value is take from the Flags class
* but it can be changed for local testing.
*/
@@ -103,6 +123,9 @@
}
}
+ /** The default injector. */
+ private static final Injector sDefaultInjector = new Injector();
+
/**
* An error is defined by its issue, the operation that detected the error, the tag of the
* affected service, a short stack of the bad call, and the stringified arg associated with
@@ -160,41 +183,46 @@
/** A lock for the AnrTimer instance. */
private final Object mLock = new Object();
- /**
- * The total number of timers started.
- */
+ /** The map from client argument to the associated timer ID. */
+ @GuardedBy("mLock")
+ private final ArrayMap<V, Integer> mTimerIdMap = new ArrayMap<>();
+
+ /** Reverse map from timer ID to client argument. */
+ @GuardedBy("mLock")
+ private final SparseArray<V> mTimerArgMap = new SparseArray<>();
+
+ /** The highwater mark of started, but not closed, timers. */
+ @GuardedBy("mLock")
+ private int mMaxStarted = 0;
+
+ /** The total number of timers started. */
@GuardedBy("mLock")
private int mTotalStarted = 0;
- /**
- * The total number of errors detected.
- */
+ /** The total number of errors detected. */
@GuardedBy("mLock")
private int mTotalErrors = 0;
- /**
- * The handler for messages sent from this instance.
- */
+ /** The total number of timers that have expired. */
+ @GuardedBy("mLock")
+ private int mTotalExpired = 0;
+
+ /** The handler for messages sent from this instance. */
private final Handler mHandler;
- /**
- * The message type for messages sent from this interface.
- */
+ /** The message type for messages sent from this interface. */
private final int mWhat;
- /**
- * A label that identifies the AnrTimer associated with a Timer in log messages.
- */
+ /** A label that identifies the AnrTimer associated with a Timer in log messages. */
private final String mLabel;
- /**
- * Whether this timer instance supports extending timeouts.
- */
+ /** Whether this timer instance supports extending timeouts. */
private final boolean mExtend;
- /**
- * The top-level switch for the feature enabled or disabled.
- */
+ /** The injector used to create this instance. This is only used for testing. */
+ private final Injector mInjector;
+
+ /** The top-level switch for the feature enabled or disabled. */
private final FeatureSwitch mFeature;
/**
@@ -223,7 +251,27 @@
mWhat = what;
mLabel = label;
mExtend = extend;
- mFeature = new FeatureDisabled();
+ mInjector = injector;
+ boolean enabled = mInjector.anrTimerServiceEnabled() && nativeTimersSupported();
+ mFeature = createFeatureSwitch(enabled);
+ }
+
+ // Return the correct feature. FeatureEnabled is returned if and only if the feature is
+ // flag-enabled and if the native shadow was successfully created. Otherwise, FeatureDisabled
+ // is returned.
+ private FeatureSwitch createFeatureSwitch(boolean enabled) {
+ if (!enabled) {
+ return new FeatureDisabled();
+ } else {
+ try {
+ return new FeatureEnabled();
+ } catch (RuntimeException e) {
+ // Something went wrong in the native layer. Log the error and fall back on the
+ // feature-disabled logic.
+ Log.e(TAG, e.toString());
+ return new FeatureDisabled();
+ }
+ }
}
/**
@@ -245,7 +293,7 @@
* @param extend A flag to indicate if expired timers can be granted extensions.
*/
public AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend) {
- this(handler, what, label, extend, new Injector());
+ this(handler, what, label, extend, sDefaultInjector);
}
/**
@@ -272,19 +320,44 @@
}
/**
+ * Start a trace on the timer. The trace is laid down in the AnrTimerTrack.
+ */
+ private void traceBegin(int timerId, int pid, int uid, String what) {
+ if (ENABLE_TRACING) {
+ final String label = formatSimple("%s(%d,%d,%s)", what, pid, uid, mLabel);
+ final int cookie = timerId;
+ Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK, label, cookie);
+ }
+ }
+
+ /**
+ * End a trace on the timer.
+ */
+ private void traceEnd(int timerId) {
+ if (ENABLE_TRACING) {
+ final int cookie = timerId;
+ Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK, cookie);
+ }
+ }
+
+ /**
* The FeatureSwitch class provides a quick switch between feature-enabled behavior and
* feature-disabled behavior.
*/
private abstract class FeatureSwitch {
abstract void start(@NonNull V arg, int pid, int uid, long timeoutMs);
- abstract void cancel(@NonNull V arg);
+ abstract boolean cancel(@NonNull V arg);
- abstract void accept(@NonNull V arg);
+ abstract boolean accept(@NonNull V arg);
- abstract void discard(@NonNull V arg);
+ abstract boolean discard(@NonNull V arg);
abstract boolean enabled();
+
+ abstract void dump(PrintWriter pw, boolean verbose);
+
+ abstract void close();
}
/**
@@ -301,18 +374,21 @@
/** Cancel a timer by removing the message from the client's handler. */
@Override
- void cancel(@NonNull V arg) {
+ boolean cancel(@NonNull V arg) {
mHandler.removeMessages(mWhat, arg);
+ return true;
}
/** accept() is a no-op when the feature is disabled. */
@Override
- void accept(@NonNull V arg) {
+ boolean accept(@NonNull V arg) {
+ return true;
}
/** discard() is a no-op when the feature is disabled. */
@Override
- void discard(@NonNull V arg) {
+ boolean discard(@NonNull V arg) {
+ return true;
}
/** The feature is not enabled. */
@@ -320,12 +396,179 @@
boolean enabled() {
return false;
}
+
+ /** dump() is a no-op when the feature is disabled. */
+ @Override
+ void dump(PrintWriter pw, boolean verbose) {
+ }
+
+ /** close() is a no-op when the feature is disabled. */
+ @Override
+ void close() {
+ }
+ }
+
+ /**
+ * A static list of AnrTimer instances. The list is traversed by dumpsys. Only instances
+ * using native resources are included.
+ */
+ @GuardedBy("sAnrTimerList")
+ private static final LongSparseArray<WeakReference<AnrTimer>> sAnrTimerList =
+ new LongSparseArray<>();
+
+ /**
+ * The FeatureEnabled class enables the AnrTimer logic. It is used when the AnrTimer service
+ * is enabled via Flags.anrTimerServiceEnabled.
+ */
+ private class FeatureEnabled extends FeatureSwitch {
+
+ /**
+ * The native timer that supports this instance. The value is set to non-zero when the
+ * native timer is created and it is set back to zero when the native timer is freed.
+ */
+ private long mNative = 0;
+
+ /** Fetch the native tag (an integer) for the given label. */
+ FeatureEnabled() {
+ mNative = nativeAnrTimerCreate(mLabel);
+ if (mNative == 0) throw new IllegalArgumentException("unable to create native timer");
+ synchronized (sAnrTimerList) {
+ sAnrTimerList.put(mNative, new WeakReference(AnrTimer.this));
+ }
+ }
+
+ /**
+ * Start a timer.
+ */
+ @Override
+ void start(@NonNull V arg, int pid, int uid, long timeoutMs) {
+ synchronized (mLock) {
+ if (mTimerIdMap.containsKey(arg)) {
+ // There is an existing timer. Cancel it.
+ cancel(arg);
+ }
+ int timerId = nativeAnrTimerStart(mNative, pid, uid, timeoutMs, mExtend);
+ if (timerId > 0) {
+ mTimerIdMap.put(arg, timerId);
+ mTimerArgMap.put(timerId, arg);
+ mTotalStarted++;
+ mMaxStarted = Math.max(mMaxStarted, mTimerIdMap.size());
+ } else {
+ throw new RuntimeException("unable to start timer");
+ }
+ }
+ }
+
+ /**
+ * Cancel a timer. No error is reported if the timer is not found because some clients
+ * cancel timers from common code that runs even if a timer was never started.
+ */
+ @Override
+ boolean cancel(@NonNull V arg) {
+ synchronized (mLock) {
+ Integer timer = removeLocked(arg);
+ if (timer == null) {
+ return false;
+ }
+ if (!nativeAnrTimerCancel(mNative, timer)) {
+ // There may be an expiration message in flight. Cancel it.
+ mHandler.removeMessages(mWhat, arg);
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Accept a timer in the framework-level handler. The timeout has been accepted and the
+ * timeout handler is executing.
+ */
+ @Override
+ boolean accept(@NonNull V arg) {
+ synchronized (mLock) {
+ Integer timer = removeLocked(arg);
+ if (timer == null) {
+ notFoundLocked("accept", arg);
+ return false;
+ }
+ nativeAnrTimerAccept(mNative, timer);
+ traceEnd(timer);
+ return true;
+ }
+ }
+
+ /**
+ * Discard a timer in the framework-level handler. For whatever reason, the timer is no
+ * longer interesting. No statistics are collected. Return false if the time was not
+ * found.
+ */
+ @Override
+ boolean discard(@NonNull V arg) {
+ synchronized (mLock) {
+ Integer timer = removeLocked(arg);
+ if (timer == null) {
+ notFoundLocked("discard", arg);
+ return false;
+ }
+ nativeAnrTimerDiscard(mNative, timer);
+ traceEnd(timer);
+ return true;
+ }
+ }
+
+ /** The feature is enabled. */
+ @Override
+ boolean enabled() {
+ return true;
+ }
+
+ /** Dump statistics from the native layer. */
+ @Override
+ void dump(PrintWriter pw, boolean verbose) {
+ synchronized (mLock) {
+ if (mNative != 0) {
+ nativeAnrTimerDump(mNative, verbose);
+ } else {
+ pw.println("closed");
+ }
+ }
+ }
+
+ /** Free native resources. */
+ @Override
+ void close() {
+ // Remove self from the list of active timers.
+ synchronized (sAnrTimerList) {
+ sAnrTimerList.remove(mNative);
+ }
+ synchronized (mLock) {
+ if (mNative != 0) nativeAnrTimerClose(mNative);
+ mNative = 0;
+ }
+ }
+
+ /**
+ * Delete the entries associated with arg from the maps and return the ID of the timer, if
+ * any.
+ */
+ @GuardedBy("mLock")
+ private Integer removeLocked(V arg) {
+ Integer r = mTimerIdMap.remove(arg);
+ if (r != null) {
+ synchronized (mTimerArgMap) {
+ mTimerArgMap.remove(r);
+ }
+ }
+ return r;
+ }
}
/**
* Start a timer associated with arg. The same object must be used to cancel, accept, or
* discard a timer later. If a timer already exists with the same arg, then the existing timer
- * is canceled and a new timer is created.
+ * is canceled and a new timer is created. The timeout is signed but negative delays are
+ * nonsensical. Rather than throw an exception, timeouts less than 0ms are forced to 0ms. This
+ * allows a client to deliver an immediate timeout via the AnrTimer.
*
* @param arg The key by which the timer is known. This is never examined or modified.
* @param pid The Linux process ID of the target being timed.
@@ -333,25 +576,39 @@
* @param timeoutMs The timer timeout, in milliseconds.
*/
public void start(@NonNull V arg, int pid, int uid, long timeoutMs) {
+ if (timeoutMs < 0) timeoutMs = 0;
mFeature.start(arg, pid, uid, timeoutMs);
}
/**
* Cancel the running timer associated with arg. The timer is forgotten. If the timer has
- * expired, the call is treated as a discard. No errors are reported if the timer does not
- * exist or if the timer has expired.
+ * expired, the call is treated as a discard. The function returns true if a running timer was
+ * found, and false if an expired timer was found or if no timer was found. After this call,
+ * the timer does not exist.
+ *
+ * Note: the return value is always true if the feature is not enabled.
+ *
+ * @param arg The key by which the timer is known. This is never examined or modified.
+ * @return True if a running timer was canceled.
*/
- public void cancel(@NonNull V arg) {
- mFeature.cancel(arg);
+ public boolean cancel(@NonNull V arg) {
+ return mFeature.cancel(arg);
}
/**
* Accept the expired timer associated with arg. This indicates that the caller considers the
- * timer expiration to be a true ANR. (See {@link #discard} for an alternate response.) It is
- * an error to accept a running timer, however the running timer will be canceled.
+ * timer expiration to be a true ANR. (See {@link #discard} for an alternate response.) The
+ * function returns true if an expired timer was found and false if a running timer was found or
+ * if no timer was found. After this call, the timer does not exist. It is an error to accept
+ * a running timer, however, the running timer will be canceled.
+ *
+ * Note: the return value is always true if the feature is not enabled.
+ *
+ * @param arg The key by which the timer is known. This is never examined or modified.
+ * @return True if an expired timer was accepted.
*/
- public void accept(@NonNull V arg) {
- mFeature.accept(arg);
+ public boolean accept(@NonNull V arg) {
+ return mFeature.accept(arg);
}
/**
@@ -359,11 +616,57 @@
* timer expiration to be a false ANR. ((See {@link #accept} for an alternate response.) One
* reason to discard an expired timer is if the process being timed was also being debugged:
* such a process could be stopped at a breakpoint and its failure to respond would not be an
- * error. It is an error to discard a running timer, however the running timer will be
- * canceled.
+ * error. After this call thie timer does not exist. It is an error to discard a running timer,
+ * however the running timer will be canceled.
+ *
+ * Note: the return value is always true if the feature is not enabled.
+ *
+ * @param arg The key by which the timer is known. This is never examined or modified.
+ * @return True if an expired timer was discarded.
*/
- public void discard(@NonNull V arg) {
- mFeature.discard(arg);
+ public boolean discard(@NonNull V arg) {
+ return mFeature.discard(arg);
+ }
+
+ /**
+ * The notifier that a timer has fired. The timerId and original pid/uid are supplied. This
+ * method is called from native code. This method takes mLock so that a timer cannot expire
+ * in the middle of another operation (like start or cancel).
+ */
+ @Keep
+ private boolean expire(int timerId, int pid, int uid) {
+ traceBegin(timerId, pid, uid, "expired");
+ V arg = null;
+ synchronized (mLock) {
+ arg = mTimerArgMap.get(timerId);
+ if (arg == null) {
+ Log.e(TAG, formatSimple("failed to expire timer %s:%d : arg not found",
+ mLabel, timerId));
+ mTotalErrors++;
+ return false;
+ }
+ mTotalExpired++;
+ }
+ mHandler.sendMessage(Message.obtain(mHandler, mWhat, arg));
+ return true;
+ }
+
+ /**
+ * Close the object and free any native resources.
+ */
+ public void close() {
+ mFeature.close();
+ }
+
+ /**
+ * Ensure any native resources are freed when the object is GC'ed. Best practice is to close
+ * the object explicitly, but overriding finalize() avoids accidental leaks.
+ */
+ @SuppressWarnings("Finalize")
+ @Override
+ protected void finalize() throws Throwable {
+ close();
+ super.finalize();
}
/**
@@ -373,8 +676,11 @@
synchronized (mLock) {
pw.format("timer: %s\n", mLabel);
pw.increaseIndent();
- pw.format("started=%d errors=%d\n", mTotalStarted, mTotalErrors);
+ pw.format("started=%d maxStarted=%d running=%d expired=%d errors=%d\n",
+ mTotalStarted, mMaxStarted, mTimerIdMap.size(),
+ mTotalExpired, mTotalErrors);
pw.decreaseIndent();
+ mFeature.dump(pw, false);
}
}
@@ -386,6 +692,13 @@
}
/**
+ * The current time in milliseconds.
+ */
+ private static long now() {
+ return SystemClock.uptimeMillis();
+ }
+
+ /**
* Dump all errors to the output stream.
*/
private static void dumpErrors(IndentingPrintWriter ipw) {
@@ -422,23 +735,89 @@
mTotalErrors++;
}
- /**
- * Log an error about a timer not found.
- */
+ /** Record an error about a timer not found. */
@GuardedBy("mLock")
private void notFoundLocked(String operation, Object arg) {
recordErrorLocked(operation, "notFound", arg);
}
- /**
- * Dumpsys output.
- */
- public static void dump(@NonNull PrintWriter pw, boolean verbose) {
+ /** Dumpsys output, allowing for overrides. */
+ @VisibleForTesting
+ static void dump(@NonNull PrintWriter pw, boolean verbose, @NonNull Injector injector) {
+ if (!injector.anrTimerServiceEnabled()) return;
+
final IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
ipw.println("AnrTimer statistics");
ipw.increaseIndent();
+ synchronized (sAnrTimerList) {
+ final int size = sAnrTimerList.size();
+ ipw.println("reporting " + size + " timers");
+ for (int i = 0; i < size; i++) {
+ AnrTimer a = sAnrTimerList.valueAt(i).get();
+ if (a != null) a.dump(ipw);
+ }
+ }
if (verbose) dumpErrors(ipw);
ipw.format("AnrTimerEnd\n");
ipw.decreaseIndent();
}
+
+ /** Dumpsys output. There is no output if the feature is not enabled. */
+ public static void dump(@NonNull PrintWriter pw, boolean verbose) {
+ dump(pw, verbose, sDefaultInjector);
+ }
+
+ /**
+ * Return true if the native timers are supported. Native timers are supported if the method
+ * nativeAnrTimerSupported() can be executed and it returns true.
+ */
+ private static boolean nativeTimersSupported() {
+ try {
+ return nativeAnrTimerSupported();
+ } catch (java.lang.UnsatisfiedLinkError e) {
+ return false;
+ }
+ }
+
+ /**
+ * Native methods
+ */
+
+ /** Return true if the native AnrTimer code is operational. */
+ private static native boolean nativeAnrTimerSupported();
+
+ /**
+ * Create a new native timer with the given key and name. The key is not used by the native
+ * code but it is returned to the Java layer in the expiration handler. The name is only for
+ * logging. Unlike the other methods, this is an instance method: the "this" parameter is
+ * passed into the native layer.
+ */
+ private native long nativeAnrTimerCreate(String name);
+
+ /** Release the native resources. No further operations are premitted. */
+ private static native int nativeAnrTimerClose(long service);
+
+ /** Start a timer and return its ID. Zero is returned on error. */
+ private static native int nativeAnrTimerStart(long service, int pid, int uid, long timeoutMs,
+ boolean extend);
+
+ /**
+ * Cancel a timer by ID. Return true if the timer was running and canceled. Return false if
+ * the timer was not found or if the timer had already expired.
+ */
+ private static native boolean nativeAnrTimerCancel(long service, int timerId);
+
+ /** Accept an expired timer by ID. Return true if the timer was found. */
+ private static native boolean nativeAnrTimerAccept(long service, int timerId);
+
+ /** Discard an expired timer by ID. Return true if the timer was found. */
+ private static native boolean nativeAnrTimerDiscard(long service, int timerId);
+
+ /** Prod the native library to log a few statistics. */
+ private static native void nativeAnrTimerDump(long service, boolean verbose);
+
+ // This is not a native method but it is a native interface, in the sense that it is called from
+ // the native layer to report timer expiration. The function must return true if the expiration
+ // message is delivered to the upper layers and false if it could not be delivered.
+ // private boolean expire(int timerId, int pid, int uid);
}
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 77b4a74..1577cef 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -448,16 +448,16 @@
}
}
- void drawMagnifiedRegionBorderIfNeeded(int displayId) {
+ void drawMagnifiedRegionBorderIfNeeded(int displayId, SurfaceControl.Transaction t) {
if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
mAccessibilityTracing.logTrace(
TAG + ".drawMagnifiedRegionBorderIfNeeded",
FLAGS_MAGNIFICATION_CALLBACK,
- "displayId=" + displayId);
+ "displayId=" + displayId + "; transaction={" + t + "}");
}
final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId);
if (displayMagnifier != null) {
- displayMagnifier.drawMagnifiedRegionBorderIfNeeded();
+ displayMagnifier.drawMagnifiedRegionBorderIfNeeded(t);
}
// Not relevant for the window observer.
}
@@ -855,12 +855,12 @@
.sendToTarget();
}
- void drawMagnifiedRegionBorderIfNeeded() {
+ void drawMagnifiedRegionBorderIfNeeded(SurfaceControl.Transaction t) {
if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
mAccessibilityTracing.logTrace(LOG_TAG + ".drawMagnifiedRegionBorderIfNeeded",
- FLAGS_MAGNIFICATION_CALLBACK);
+ FLAGS_MAGNIFICATION_CALLBACK, "transition={" + t + "}");
}
- mMagnifedViewport.drawWindowIfNeeded();
+ mMagnifedViewport.drawWindowIfNeeded(t);
}
void dump(PrintWriter pw, String prefix) {
@@ -1106,11 +1106,11 @@
}
void setMagnifiedRegionBorderShown(boolean shown, boolean animate) {
- if (mWindow.setShown(shown, animate)) {
+ if (shown) {
mFullRedrawNeeded = true;
- // Clear the old region, so recomputeBounds will refresh the current region.
mOldMagnificationRegion.set(0, 0, 0, 0);
}
+ mWindow.setShown(shown, animate);
}
void getMagnifiedFrameInContentCoords(Rect rect) {
@@ -1128,9 +1128,9 @@
return mMagnificationSpec;
}
- void drawWindowIfNeeded() {
+ void drawWindowIfNeeded(SurfaceControl.Transaction t) {
recomputeBounds();
- mWindow.postDrawIfNeeded();
+ mWindow.drawIfNeeded(t);
}
void destroyWindow() {
@@ -1158,7 +1158,7 @@
mWindow.dump(pw, prefix);
}
- private final class ViewportWindow implements Runnable {
+ private final class ViewportWindow {
private static final String SURFACE_TITLE = "Magnification Overlay";
private final Region mBounds = new Region();
@@ -1166,18 +1166,15 @@
private final Paint mPaint = new Paint();
private final SurfaceControl mSurfaceControl;
- /** After initialization, it should only be accessed from animation thread. */
- private final SurfaceControl.Transaction mTransaction;
private final BLASTBufferQueue mBlastBufferQueue;
private final Surface mSurface;
private final AnimationController mAnimationController;
private boolean mShown;
- private boolean mLastSurfaceShown;
private int mAlpha;
- private volatile boolean mInvalidated;
+ private boolean mInvalidated;
ViewportWindow(Context context) {
SurfaceControl surfaceControl = null;
@@ -1205,7 +1202,6 @@
InputMonitor.setTrustedOverlayInputInfo(mSurfaceControl, t,
mDisplayContent.getDisplayId(), "Magnification Overlay");
t.apply();
- mTransaction = t;
mSurface = mBlastBufferQueue.createSurface();
mAnimationController = new AnimationController(context,
@@ -1223,11 +1219,10 @@
mInvalidated = true;
}
- /** Returns {@code true} if the shown state is changed. */
- boolean setShown(boolean shown, boolean animate) {
+ void setShown(boolean shown, boolean animate) {
synchronized (mService.mGlobalLock) {
if (mShown == shown) {
- return false;
+ return;
}
mShown = shown;
mAnimationController.onFrameShownStateChanged(shown, animate);
@@ -1235,7 +1230,6 @@
Slog.i(LOG_TAG, "ViewportWindow shown: " + mShown);
}
}
- return true;
}
@SuppressWarnings("unused")
@@ -1291,22 +1285,7 @@
mService.scheduleAnimationLocked();
}
- void postDrawIfNeeded() {
- if (mInvalidated) {
- mService.mAnimationHandler.post(this);
- }
- }
-
- @Override
- public void run() {
- drawIfNeeded();
- }
-
- /**
- * This method must only be called by animation handler directly to make sure
- * thread safe and there is no lock held outside.
- */
- private void drawIfNeeded() {
+ void drawIfNeeded(SurfaceControl.Transaction t) {
// Drawing variables (alpha, dirty rect, and bounds) access is synchronized
// using WindowManagerGlobalLock. Grab copies of these values before
// drawing on the canvas so that drawing can be performed outside of the lock.
@@ -1335,7 +1314,6 @@
}
}
- final boolean showSurface;
// Draw without holding WindowManagerGlobalLock.
if (alpha > 0) {
Canvas canvas = null;
@@ -1351,17 +1329,9 @@
mPaint.setAlpha(alpha);
canvas.drawPath(drawingBounds.getBoundaryPath(), mPaint);
mSurface.unlockCanvasAndPost(canvas);
- showSurface = true;
+ t.show(mSurfaceControl);
} else {
- showSurface = false;
- }
-
- if (showSurface && !mLastSurfaceShown) {
- mTransaction.show(mSurfaceControl).apply();
- mLastSurfaceShown = true;
- } else if (!showSurface && mLastSurfaceShown) {
- mTransaction.hide(mSurfaceControl).apply();
- mLastSurfaceShown = false;
+ t.hide(mSurfaceControl);
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index a43e7d5..b8a92bb 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2492,7 +2492,14 @@
ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Creating SnapshotStartingData");
mStartingData = new SnapshotStartingData(mWmService, snapshot, typeParams);
- if (task.forAllLeafTaskFragments(TaskFragment::isEmbedded)) {
+ if ((!mStyleFillsParent && task.getChildCount() > 1)
+ || task.forAllLeafTaskFragments(TaskFragment::isEmbedded)) {
+ // Case 1:
+ // If it is moving a Task{[0]=main activity, [1]=translucent activity} to front, use
+ // shared starting window so that the transition doesn't need to wait for the activity
+ // behind the translucent activity. Also, onFirstWindowDrawn will check all visible
+ // activities are drawn in the task to remove the snapshot starting window.
+ // Case 2:
// Associate with the task so if this activity is resized by task fragment later, the
// starting window can keep the same bounds as the task.
associateStartingDataWithTask();
@@ -4312,7 +4319,6 @@
mTaskSupervisor.getActivityMetricsLogger().notifyActivityRemoved(this);
mTaskSupervisor.mStoppingActivities.remove(this);
mLetterboxUiController.destroy();
- waitingToShow = false;
// Defer removal of this activity when either a child is animating, or app transition is on
// going. App transition animation might be applied on the parent task not on the activity,
@@ -5386,7 +5392,6 @@
final DisplayContent displayContent = getDisplayContent();
displayContent.mOpeningApps.remove(this);
displayContent.mClosingApps.remove(this);
- waitingToShow = false;
setVisibleRequested(visible);
mLastDeferHidingClient = deferHidingClient;
@@ -5411,25 +5416,16 @@
// stopped, then we need to set up to wait for its windows to be ready.
if (!isVisible() || mAppStopped) {
clearAllDrawn();
-
- // If the app was already visible, don't reset the waitingToShow state.
- if (!isVisible()) {
- waitingToShow = true;
-
- // If the client isn't hidden, we don't need to reset the drawing state.
- if (!isClientVisible()) {
- // Let's reset the draw state in order to prevent the starting window to be
- // immediately dismissed when the app still has the surface.
- forAllWindows(w -> {
- if (w.mWinAnimator.mDrawState == HAS_DRAWN) {
- w.mWinAnimator.resetDrawState();
-
- // Force add to mResizingWindows, so that we are guaranteed to get
- // another reportDrawn callback.
- w.forceReportingResized();
- }
- }, true /* traverseTopToBottom */);
- }
+ // Reset the draw state in order to prevent the starting window to be immediately
+ // dismissed when the app still has the surface.
+ if (!isVisible() && !isClientVisible()) {
+ forAllWindows(w -> {
+ if (w.mWinAnimator.mDrawState == HAS_DRAWN) {
+ w.mWinAnimator.resetDrawState();
+ // Force add to mResizingWindows, so the window will report drawn.
+ w.forceReportingResized();
+ }
+ }, true /* traverseTopToBottom */);
}
}
@@ -10626,6 +10622,13 @@
@Override
boolean isSyncFinished(BLASTSyncEngine.SyncGroup group) {
+ if (task != null && task.mSharedStartingData != null) {
+ final WindowState startingWin = task.topStartingWindow();
+ if (startingWin != null && startingWin.isSyncFinished(group)) {
+ // The sync is ready if a drawn starting window covered the task.
+ return true;
+ }
+ }
if (!super.isSyncFinished(group)) return false;
if (mDisplayContent != null && mDisplayContent.mUnknownAppVisibilityController
.isVisibilityUnknown(this)) {
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index d90d017..13f7152 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -993,17 +993,6 @@
}
}
- if (Flags.archiving()) {
- PackageArchiver packageArchiver = mService
- .getPackageManagerInternalLocked()
- .getPackageArchiver();
- if (packageArchiver.isIntentResolvedToArchivedApp(intent, mRequest.userId)) {
- return packageArchiver
- .requestUnarchiveOnActivityStart(
- intent, callingPackage, mRequest.userId, realCallingUid);
- }
- }
-
final int launchFlags = intent.getFlags();
if ((launchFlags & Intent.FLAG_ACTIVITY_FORWARD_RESULT) != 0 && sourceRecord != null) {
// Transfer the result target from the source activity to the new one being started,
@@ -1045,6 +1034,17 @@
}
if (err == ActivityManager.START_SUCCESS && aInfo == null) {
+ if (Flags.archiving()) {
+ PackageArchiver packageArchiver = mService
+ .getPackageManagerInternalLocked()
+ .getPackageArchiver();
+ if (packageArchiver.isIntentResolvedToArchivedApp(intent, mRequest.userId)) {
+ return packageArchiver
+ .requestUnarchiveOnActivityStart(
+ intent, callingPackage, mRequest.userId, realCallingUid);
+ }
+ }
+
// We couldn't find the specific class specified in the Intent.
// Also the end of the line.
err = ActivityManager.START_CLASS_NOT_FOUND;
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index f43c1b0..3959a5e 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -3691,19 +3691,13 @@
return false;
}
- // If the app is using legacy-entry (not auto-enter), then we will get a client-request
- // that was actually a server-request (via pause(userLeaving=true)). This happens when
- // the app is PAUSING, so detect that case here.
- boolean originallyFromClient = fromClient
- && (!r.isState(PAUSING) || params.isAutoEnterEnabled());
-
- // If PiP2 flag is on and client-request to enter PiP came via onUserLeaveHint(),
- // we request a direct transition from Shell to TRANSIT_PIP_LEGACY to get the startWct
- // with the right entry bounds.
- if (isPip2ExperimentEnabled() && !originallyFromClient && !params.isAutoEnterEnabled()) {
+ // If PiP2 flag is on and client-request to enter PiP comes in,
+ // we request a direct transition from Shell to TRANSIT_PIP to get the startWct
+ // with the right entry bounds. So PiP activity isn't moved to a pinned task until after
+ // Shell calls back into Core with the entry bounds passed through.
+ if (isPip2ExperimentEnabled()) {
final Transition legacyEnterPipTransition = new Transition(TRANSIT_PIP,
- 0 /* flags */, getTransitionController(),
- mWindowManager.mSyncEngine);
+ 0 /* flags */, getTransitionController(), mWindowManager.mSyncEngine);
legacyEnterPipTransition.setPipActivity(r);
getTransitionController().startCollectOrQueue(legacyEnterPipTransition, (deferred) -> {
getTransitionController().requestStartTransition(legacyEnterPipTransition,
@@ -3712,6 +3706,12 @@
return true;
}
+ // If the app is using legacy-entry (not auto-enter), then we will get a client-request
+ // that was actually a server-request (via pause(userLeaving=true)). This happens when
+ // the app is PAUSING, so detect that case here.
+ boolean originallyFromClient = fromClient
+ && (!r.isState(PAUSING) || params.isAutoEnterEnabled());
+
// Create a transition only for this pip entry if it is coming from the app without the
// system requesting that the app enter-pip. If the system requested it, that means it
// should be part of that transition if possible.
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index 05087f8..939babc 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -1176,7 +1176,6 @@
mDisplayContent.mNoAnimationNotifyOnTransitionFinished.add(app.token);
}
app.updateReportedVisibilityLocked();
- app.waitingToShow = false;
app.showAllWindowsLocked();
if (mDisplayContent.mAppTransition.isNextAppTransitionThumbnailUp()) {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index d556f09..3b06343 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -1411,12 +1411,13 @@
return isUidPresent;
}
+ WindowState topStartingWindow() {
+ return getWindow(w -> w.mAttrs.type == TYPE_APPLICATION_STARTING);
+ }
+
ActivityRecord topActivityContainsStartingWindow() {
- if (getParent() == null) {
- return null;
- }
- return getActivity((r) -> r.getWindow(window ->
- window.getBaseType() == TYPE_APPLICATION_STARTING) != null);
+ final WindowState startingWindow = topStartingWindow();
+ return startingWindow != null ? startingWindow.mActivityRecord : null;
}
/**
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index d425bdf..93cce2a 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -926,10 +926,14 @@
boolean sleepIfPossible(boolean shuttingDown) {
boolean shouldSleep = true;
if (mResumedActivity != null) {
- // Still have something resumed; can't sleep until it is paused.
- ProtoLog.v(WM_DEBUG_STATES, "Sleep needs to pause %s", mResumedActivity);
- startPausing(false /* userLeaving */, true /* uiSleeping */, null /* resuming */,
- "sleep");
+ if (!shuttingDown && mResumedActivity.canTurnScreenOn()) {
+ ProtoLog.v(WM_DEBUG_STATES, "Waiting for screen on due to %s", mResumedActivity);
+ } else {
+ // Still have something resumed; can't sleep until it is paused.
+ ProtoLog.v(WM_DEBUG_STATES, "Sleep needs to pause %s", mResumedActivity);
+ startPausing(false /* userLeaving */, true /* uiSleeping */, null /* resuming */,
+ "sleep");
+ }
shouldSleep = false;
} else if (mPausingActivity != null) {
// Still waiting for something to pause; can't sleep yet.
@@ -2980,7 +2984,7 @@
@Override
Dimmer getDimmer() {
// If this is in an embedded TaskFragment and we want the dim applies on the TaskFragment.
- if (mIsEmbedded && mEmbeddedDimArea == EMBEDDED_DIM_AREA_TASK_FRAGMENT) {
+ if (mIsEmbedded && !isDimmingOnParentTask()) {
return mDimmer;
}
@@ -2989,7 +2993,9 @@
/** Bounds to be used for dimming, as well as touch related tests. */
void getDimBounds(@NonNull Rect out) {
- if (mIsEmbedded && mEmbeddedDimArea == EMBEDDED_DIM_AREA_PARENT_TASK) {
+ if (mIsEmbedded && isDimmingOnParentTask() && getDimmer().getDimBounds() != null) {
+ // Return the task bounds if the dimmer is showing and should cover on the Task (not
+ // just on this embedded TaskFragment).
out.set(getTask().getBounds());
} else {
out.set(getBounds());
@@ -3000,6 +3006,11 @@
mEmbeddedDimArea = embeddedDimArea;
}
+ @VisibleForTesting
+ boolean isDimmingOnParentTask() {
+ return mEmbeddedDimArea == EMBEDDED_DIM_AREA_PARENT_TASK;
+ }
+
@Override
void prepareSurfaces() {
if (asTask() != null) {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index b12855e..56bef33 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1906,7 +1906,6 @@
for (int i = mParticipants.size() - 1; i >= 0; --i) {
final WallpaperWindowToken wallpaper = mParticipants.valueAt(i).asWallpaperToken();
if (wallpaper != null) {
- wallpaper.waitingToShow = false;
if (!wallpaper.isVisible() && wallpaper.isVisibleRequested()) {
wallpaper.commitVisibility(showWallpaper);
}
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index c4e1d6e..750fd50 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -148,7 +148,8 @@
dc.checkAppWindowsReadyToShow();
if (accessibilityController.hasCallbacks()) {
- accessibilityController.drawMagnifiedRegionBorderIfNeeded(dc.mDisplayId);
+ accessibilityController.drawMagnifiedRegionBorderIfNeeded(dc.mDisplayId,
+ mTransaction);
}
if (dc.isAnimating(animationFlags, ANIMATION_TYPE_ALL)) {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 9e4a31c..59d0210 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -34,6 +34,7 @@
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_RELATIVE_BOUNDS;
import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
@@ -68,6 +69,8 @@
import static com.android.server.wm.ActivityTaskSupervisor.REMOVE_FROM_RECENTS;
import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK;
import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG;
+import static com.android.server.wm.TaskFragment.EMBEDDED_DIM_AREA_PARENT_TASK;
+import static com.android.server.wm.TaskFragment.EMBEDDED_DIM_AREA_TASK_FRAGMENT;
import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED;
import static com.android.server.wm.TaskFragment.FLAG_FORCE_HIDDEN_FOR_TASK_FRAGMENT_ORG;
import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
@@ -1493,6 +1496,12 @@
task.removeDecorSurface();
break;
}
+ case OP_TYPE_SET_DIM_ON_TASK: {
+ final boolean dimOnTask = operation.isDimOnTask();
+ taskFragment.setEmbeddedDimArea(dimOnTask ? EMBEDDED_DIM_AREA_PARENT_TASK
+ : EMBEDDED_DIM_AREA_TASK_FRAGMENT);
+ break;
+ }
}
return effects;
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 315c00f..0b43be7 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1929,9 +1929,6 @@
* of a transition that has not yet been started.
*/
boolean isReadyForDisplay() {
- if (mToken.waitingToShow && getDisplayContent().mAppTransition.isTransitionSet()) {
- return false;
- }
final boolean parentAndClientVisible = !isParentWindowHidden()
&& mViewVisibility == View.VISIBLE && mToken.isVisible();
return mHasSurface && isVisibleByPolicy() && !mDestroying
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 7d21dbf..5048cef 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -28,7 +28,6 @@
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
import static com.android.server.wm.WindowTokenProto.HASH_CODE;
import static com.android.server.wm.WindowTokenProto.PAUSED;
-import static com.android.server.wm.WindowTokenProto.WAITING_TO_SHOW;
import static com.android.server.wm.WindowTokenProto.WINDOW_CONTAINER;
import android.annotation.CallSuper;
@@ -91,10 +90,6 @@
// Is key dispatching paused for this token?
boolean paused = false;
- // Set to true when this token is in a pending transaction where it
- // will be shown.
- boolean waitingToShow;
-
/** The owner has {@link android.Manifest.permission#MANAGE_APP_TOKENS} */
final boolean mOwnerCanManageAppTokens;
@@ -702,7 +697,6 @@
final long token = proto.start(fieldId);
super.dumpDebug(proto, WINDOW_CONTAINER, logLevel);
proto.write(HASH_CODE, System.identityHashCode(this));
- proto.write(WAITING_TO_SHOW, waitingToShow);
proto.write(PAUSED, paused);
proto.end(token);
}
@@ -716,9 +710,6 @@
super.dump(pw, prefix, dumpAll);
pw.print(prefix); pw.print("windows="); pw.println(mChildren);
pw.print(prefix); pw.print("windowType="); pw.print(windowType);
- if (waitingToShow) {
- pw.print(" waitingToShow=true");
- }
pw.println();
if (hasFixedRotationTransform()) {
pw.print(prefix);
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index b19f3d8..dfa9dce 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -79,6 +79,7 @@
":lib_cachedAppOptimizer_native",
":lib_gameManagerService_native",
":lib_oomConnection_native",
+ ":lib_anrTimer_native",
],
include_dirs: [
@@ -246,3 +247,10 @@
name: "lib_oomConnection_native",
srcs: ["com_android_server_am_OomConnection.cpp"],
}
+
+filegroup {
+ name: "lib_anrTimer_native",
+ srcs: [
+ "com_android_server_utils_AnrTimer.cpp",
+ ],
+}
diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp
new file mode 100644
index 0000000..97b18fa
--- /dev/null
+++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp
@@ -0,0 +1,918 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <time.h>
+#include <pthread.h>
+#include <sys/timerfd.h>
+#include <inttypes.h>
+
+#include <algorithm>
+#include <list>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+#define LOG_TAG "AnrTimerService"
+
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include "android_runtime/AndroidRuntime.h"
+#include "core_jni_helpers.h"
+
+#include <utils/Mutex.h>
+#include <utils/Timers.h>
+
+#include <utils/Log.h>
+#include <utils/Timers.h>
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <android-base/unique_fd.h>
+
+using ::android::base::StringPrintf;
+
+
+// Native support is unavailable on WIN32 platforms. This macro preemptively disables it.
+#ifdef _WIN32
+#define NATIVE_SUPPORT 0
+#else
+#define NATIVE_SUPPORT 1
+#endif
+
+namespace android {
+
+// using namespace android;
+
+// Almost nothing in this module needs to be in the android namespace.
+namespace {
+
+// If not on a Posix system, create stub timerfd methods. These are defined to allow
+// compilation. They are not functional. Also, they do not leak outside this compilation unit.
+#ifdef _WIN32
+int timer_create() {
+ return -1;
+}
+int timer_settime(int, int, void const *, void *) {
+ return -1;
+}
+#else
+int timer_create() {
+ return timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
+}
+int timer_settime(int fd, int flags, const struct itimerspec *new_value,
+ struct itimerspec *_Nullable old_value) {
+ return timerfd_settime(fd, flags, new_value, old_value);
+}
+#endif
+
+// A local debug flag that gates a set of log messages for debug only. This is normally const
+// false so the debug statements are not included in the image. The flag can be set true in a
+// unit test image to debug test failures.
+const bool DEBUG = false;
+
+// Return the current time in nanoseconds. This time is relative to system boot.
+nsecs_t now() {
+ return systemTime(SYSTEM_TIME_MONOTONIC);
+}
+
+/**
+ * This class encapsulates the anr timer service. The service manages a list of individual
+ * timers. A timer is either Running or Expired. Once started, a timer may be canceled or
+ * accepted. Both actions collect statistics about the timer and then delete it. An expired
+ * timer may also be discarded, which deletes the timer without collecting any statistics.
+ *
+ * All public methods in this class are thread-safe.
+ */
+class AnrTimerService {
+ private:
+ class ProcessStats;
+ class Timer;
+
+ public:
+
+ // The class that actually runs the clock.
+ class Ticker;
+
+ // A timer is identified by a timer_id_t. Timer IDs are unique in the moment.
+ using timer_id_t = uint32_t;
+
+ // A manifest constant. No timer is ever created with this ID.
+ static const timer_id_t NOTIMER = 0;
+
+ // A notifier is called with a timer ID, the timer's tag, and the client's cookie. The pid
+ // and uid that were originally assigned to the timer are passed as well.
+ using notifier_t = bool (*)(timer_id_t, int pid, int uid, void* cookie, jweak object);
+
+ enum Status {
+ Invalid,
+ Running,
+ Expired,
+ Canceled
+ };
+
+ /**
+ * Create a timer service. The service is initialized with a name used for logging. The
+ * constructor is also given the notifier callback, and two cookies for the callback: the
+ * traditional void* and an int.
+ */
+ AnrTimerService(char const* label, notifier_t notifier, void* cookie, jweak jtimer, Ticker*);
+
+ // Delete the service and clean up memory.
+ ~AnrTimerService();
+
+ // Start a timer and return the associated timer ID. It does not matter if the same pid/uid
+ // are already in the running list. Once start() is called, one of cancel(), accept(), or
+ // discard() must be called to clean up the internal data structures.
+ timer_id_t start(int pid, int uid, nsecs_t timeout, bool extend);
+
+ // Cancel a timer and remove it from all lists. This is called when the event being timed
+ // has occurred. If the timer was Running, the function returns true. The other
+ // possibilities are that the timer was Expired or non-existent; in both cases, the function
+ // returns false.
+ bool cancel(timer_id_t timerId);
+
+ // Accept a timer and remove it from all lists. This is called when the upper layers accept
+ // that a timer has expired. If the timer was Expired, the function returns true. The
+ // other possibilities are tha the timer was Running or non-existing; in both cases, the
+ // function returns false.
+ bool accept(timer_id_t timerId);
+
+ // Discard a timer without collecting any statistics. This is called when the upper layers
+ // recognize that a timer expired but decide the expiration is not significant. If the
+ // timer was Expired, the function returns true. The other possibilities are tha the timer
+ // was Running or non-existing; in both cases, the function returns false.
+ bool discard(timer_id_t timerId);
+
+ // A timer has expired.
+ void expire(timer_id_t);
+
+ // Dump a small amount of state to the log file.
+ void dump(bool verbose) const;
+
+ // Return the Java object associated with this instance.
+ jweak jtimer() const {
+ return notifierObject_;
+ }
+
+ private:
+ // The service cannot be copied.
+ AnrTimerService(AnrTimerService const &) = delete;
+
+ // Insert a timer into the running list. The lock must be held by the caller.
+ void insert(const Timer&);
+
+ // Remove a timer from the lists and return it. The lock must be held by the caller.
+ Timer remove(timer_id_t timerId);
+
+ // Return a string representation of a status value.
+ static char const *statusString(Status);
+
+ // The name of this service, for logging.
+ std::string const label_;
+
+ // The callback that is invoked when a timer expires.
+ notifier_t const notifier_;
+
+ // The two cookies passed to the notifier.
+ void* notifierCookie_;
+ jweak notifierObject_;
+
+ // The global lock
+ mutable Mutex lock_;
+
+ // The list of all timers that are still running. This is sorted by ID for fast lookup.
+ std::set<Timer> running_;
+
+ // The maximum number of active timers.
+ size_t maxActive_;
+
+ // Simple counters
+ struct Counters {
+ // The number of timers started, canceled, accepted, discarded, and expired.
+ size_t started;
+ size_t canceled;
+ size_t accepted;
+ size_t discarded;
+ size_t expired;
+
+ // The number of times there were zero active timers.
+ size_t drained;
+
+ // The number of times a protocol error was seen.
+ size_t error;
+ };
+
+ Counters counters_;
+
+ // The clock used by this AnrTimerService.
+ Ticker *ticker_;
+};
+
+class AnrTimerService::ProcessStats {
+ public:
+ nsecs_t cpu_time;
+ nsecs_t cpu_delay;
+
+ ProcessStats() :
+ cpu_time(0),
+ cpu_delay(0) {
+ }
+
+ // Collect all statistics for a process. Return true if the fill succeeded and false if it
+ // did not. If there is any problem, the statistics are zeroed.
+ bool fill(int pid) {
+ cpu_time = 0;
+ cpu_delay = 0;
+
+ char path[PATH_MAX];
+ snprintf(path, sizeof(path), "/proc/%u/schedstat", pid);
+ ::android::base::unique_fd fd(open(path, O_RDONLY | O_CLOEXEC));
+ if (!fd.ok()) {
+ return false;
+ }
+ char buffer[128];
+ ssize_t len = read(fd, buffer, sizeof(buffer));
+ if (len <= 0) {
+ return false;
+ }
+ if (len >= sizeof(buffer)) {
+ ALOGE("proc file too big: %s", path);
+ return false;
+ }
+ buffer[len] = 0;
+ unsigned long t1;
+ unsigned long t2;
+ if (sscanf(buffer, "%lu %lu", &t1, &t2) != 2) {
+ return false;
+ }
+ cpu_time = t1;
+ cpu_delay = t2;
+ return true;
+ }
+};
+
+class AnrTimerService::Timer {
+ public:
+ // A unique ID assigned when the Timer is created.
+ timer_id_t const id;
+
+ // The creation parameters. The timeout is the original, relative timeout.
+ int const pid;
+ int const uid;
+ nsecs_t const timeout;
+ bool const extend;
+
+ // The state of this timer.
+ Status status;
+
+ // The scheduled timeout. This is an absolute time. It may be extended.
+ nsecs_t scheduled;
+
+ // True if this timer has been extended.
+ bool extended;
+
+ // Bookkeeping for extensions. The initial state of the process. This is collected only if
+ // the timer is extensible.
+ ProcessStats initial;
+
+ // The default constructor is used to create timers that are Invalid, representing the "not
+ // found" condition when a collection is searched.
+ Timer() :
+ id(NOTIMER),
+ pid(0),
+ uid(0),
+ timeout(0),
+ extend(false),
+ status(Invalid),
+ scheduled(0),
+ extended(false) {
+ }
+
+ // This constructor creates a timer with the specified id. This can be used as the argument
+ // to find().
+ Timer(timer_id_t id) :
+ id(id),
+ pid(0),
+ uid(0),
+ timeout(0),
+ extend(false),
+ status(Invalid),
+ scheduled(0),
+ extended(false) {
+ }
+
+ // Create a new timer. This starts the timer.
+ Timer(int pid, int uid, nsecs_t timeout, bool extend) :
+ id(nextId()),
+ pid(pid),
+ uid(uid),
+ timeout(timeout),
+ extend(extend),
+ status(Running),
+ scheduled(now() + timeout),
+ extended(false) {
+ if (extend && pid != 0) {
+ initial.fill(pid);
+ }
+ }
+
+ // Cancel a timer. Return the headroom (which may be negative). This does not, as yet,
+ // account for extensions.
+ void cancel() {
+ ALOGW_IF(DEBUG && status != Running, "cancel %s", toString().c_str());
+ status = Canceled;
+ }
+
+ // Expire a timer. Return true if the timer is expired and false otherwise. The function
+ // returns false if the timer is eligible for extension. If the function returns false, the
+ // scheduled time is updated.
+ bool expire() {
+ ALOGI_IF(DEBUG, "expire %s", toString().c_str());
+ nsecs_t extension = 0;
+ if (extend && !extended) {
+ // Only one extension is permitted.
+ extended = true;
+ ProcessStats current;
+ current.fill(pid);
+ extension = current.cpu_delay - initial.cpu_delay;
+ if (extension < 0) extension = 0;
+ if (extension > timeout) extension = timeout;
+ }
+ if (extension == 0) {
+ status = Expired;
+ } else {
+ scheduled += extension;
+ }
+ return status == Expired;
+ }
+
+ // Accept a timeout.
+ void accept() {
+ }
+
+ // Discard a timeout.
+ void discard() {
+ }
+
+ // Timers are sorted by id, which is unique. This provides fast lookups.
+ bool operator<(Timer const &r) const {
+ return id < r.id;
+ }
+
+ bool operator==(timer_id_t r) const {
+ return id == r;
+ }
+
+ std::string toString() const {
+ return StringPrintf("timer id=%d pid=%d status=%s", id, pid, statusString(status));
+ }
+
+ std::string toString(nsecs_t now) const {
+ uint32_t ms = nanoseconds_to_milliseconds(now - scheduled);
+ return StringPrintf("timer id=%d pid=%d status=%s scheduled=%ums",
+ id, pid, statusString(status), -ms);
+ }
+
+ static int maxId() {
+ return idGen;
+ }
+
+ private:
+ // Get the next free ID. NOTIMER is never returned.
+ static timer_id_t nextId() {
+ timer_id_t id = idGen.fetch_add(1);
+ while (id == NOTIMER) {
+ id = idGen.fetch_add(1);
+ }
+ return id;
+ }
+
+ // IDs start at 1. A zero ID is invalid.
+ static std::atomic<timer_id_t> idGen;
+};
+
+// IDs start at 1.
+std::atomic<AnrTimerService::timer_id_t> AnrTimerService::Timer::idGen(1);
+
+/**
+ * Manage a set of timers and notify clients when there is a timeout.
+ */
+class AnrTimerService::Ticker {
+ private:
+ struct Entry {
+ const nsecs_t scheduled;
+ const timer_id_t id;
+ AnrTimerService* const service;
+
+ Entry(nsecs_t scheduled, timer_id_t id, AnrTimerService* service) :
+ scheduled(scheduled), id(id), service(service) {};
+
+ bool operator<(const Entry &r) const {
+ return scheduled == r.scheduled ? id < r.id : scheduled < r.scheduled;
+ }
+ };
+
+ public:
+
+ // Construct the ticker. This creates the timerfd file descriptor and starts the monitor
+ // thread. The monitor thread is given a unique name.
+ Ticker() {
+ timerFd_ = timer_create();
+ if (timerFd_ < 0) {
+ ALOGE("failed to create timerFd: %s", strerror(errno));
+ return;
+ }
+
+ if (pthread_create(&watcher_, 0, run, this) != 0) {
+ ALOGE("failed to start thread: %s", strerror(errno));
+ watcher_ = 0;
+ ::close(timerFd_);
+ return;
+ }
+
+ // 16 is a magic number from the kernel. Thread names may not be longer than this many
+ // bytes, including the terminating null. The snprintf() method will truncate properly.
+ char name[16];
+ snprintf(name, sizeof(name), "AnrTimerService");
+ pthread_setname_np(watcher_, name);
+
+ ready_ = true;
+ }
+
+ ~Ticker() {
+ // Closing the file descriptor will close the monitor process, if any.
+ if (timerFd_ >= 0) ::close(timerFd_);
+ timerFd_ = -1;
+ watcher_ = 0;
+ }
+
+ // Insert a timer. Unless canceled, the timer will expire at the scheduled time. If it
+ // expires, the service will be notified with the id.
+ void insert(nsecs_t scheduled, timer_id_t id, AnrTimerService *service) {
+ Entry e(scheduled, id, service);
+ AutoMutex _l(lock_);
+ timer_id_t front = headTimerId();
+ running_.insert(e);
+ if (front != headTimerId()) restartLocked();
+ maxRunning_ = std::max(maxRunning_, running_.size());
+ }
+
+ // Remove a timer. The timer is identified by its scheduled timeout and id. Technically,
+ // the id is sufficient (because timer IDs are unique) but using the timeout is more
+ // efficient.
+ void remove(nsecs_t scheduled, timer_id_t id) {
+ Entry key(scheduled, id, 0);
+ AutoMutex _l(lock_);
+ timer_id_t front = headTimerId();
+ auto found = running_.find(key);
+ if (found != running_.end()) running_.erase(found);
+ if (front != headTimerId()) restartLocked();
+ }
+
+ // Remove every timer associated with the service.
+ void remove(AnrTimerService const* service) {
+ AutoMutex _l(lock_);
+ timer_id_t front = headTimerId();
+ for (auto i = running_.begin(); i != running_.end(); i++) {
+ if (i->service == service) {
+ running_.erase(i);
+ }
+ }
+ if (front != headTimerId()) restartLocked();
+ }
+
+ // Return the number of timers still running.
+ size_t running() const {
+ AutoMutex _l(lock_);
+ return running_.size();
+ }
+
+ // Return the high-water mark of timers running.
+ size_t maxRunning() const {
+ AutoMutex _l(lock_);
+ return maxRunning_;
+ }
+
+ private:
+
+ // Return the head of the running list. The lock must be held by the caller.
+ timer_id_t headTimerId() {
+ return running_.empty() ? NOTIMER : running_.cbegin()->id;
+ }
+
+ // A simple wrapper that meets the requirements of pthread_create.
+ static void* run(void* arg) {
+ reinterpret_cast<Ticker*>(arg)->monitor();
+ ALOGI("monitor exited");
+ return 0;
+ }
+
+ // Loop (almost) forever. Whenever the timerfd expires, expire as many entries as
+ // possible. The loop terminates when the read fails; this generally indicates that the
+ // file descriptor has been closed and the thread can exit.
+ void monitor() {
+ uint64_t token = 0;
+ while (read(timerFd_, &token, sizeof(token)) == sizeof(token)) {
+ // Move expired timers into the local ready list. This is done inside
+ // the lock. Then, outside the lock, expire them.
+ nsecs_t current = now();
+ std::vector<Entry> ready;
+ {
+ AutoMutex _l(lock_);
+ while (!running_.empty()) {
+ Entry timer = *(running_.begin());
+ if (timer.scheduled <= current) {
+ ready.push_back(timer);
+ running_.erase(running_.cbegin());
+ } else {
+ break;
+ }
+ }
+ restartLocked();
+ }
+ // Call the notifiers outside the lock. Calling the notifiers with the lock held
+ // can lead to deadlock, if the Java-side handler also takes a lock. Note that the
+ // timerfd is already running.
+ for (auto i = ready.begin(); i != ready.end(); i++) {
+ Entry e = *i;
+ e.service->expire(e.id);
+ }
+ }
+ }
+
+ // Restart the ticker. The caller must be holding the lock. This method updates the
+ // timerFd_ to expire at the time of the first Entry in the running list. This method does
+ // not check to see if the currently programmed expiration time is different from the
+ // scheduled expiration time of the first entry.
+ void restartLocked() {
+ if (!running_.empty()) {
+ Entry const x = *(running_.cbegin());
+ nsecs_t delay = x.scheduled - now();
+ // Force a minimum timeout of 10ns.
+ if (delay < 10) delay = 10;
+ time_t sec = nanoseconds_to_seconds(delay);
+ time_t ns = delay - seconds_to_nanoseconds(sec);
+ struct itimerspec setting = {
+ .it_interval = { 0, 0 },
+ .it_value = { sec, ns },
+ };
+ timer_settime(timerFd_, 0, &setting, nullptr);
+ restarted_++;
+ ALOGI_IF(DEBUG, "restarted timerfd for %ld.%09ld", sec, ns);
+ } else {
+ const struct itimerspec setting = {
+ .it_interval = { 0, 0 },
+ .it_value = { 0, 0 },
+ };
+ timer_settime(timerFd_, 0, &setting, nullptr);
+ drained_++;
+ ALOGI_IF(DEBUG, "drained timer list");
+ }
+ }
+
+ // The usual lock.
+ mutable Mutex lock_;
+
+ // True if the object was initialized properly. Android does not support throwing C++
+ // exceptions, so clients should check this flag after constructing the object. This is
+ // effectively const after the instance has been created.
+ bool ready_ = false;
+
+ // The file descriptor of the timer.
+ int timerFd_ = -1;
+
+ // The thread that monitors the timer.
+ pthread_t watcher_ = 0;
+
+ // The number of times the timer was restarted.
+ size_t restarted_ = 0;
+
+ // The number of times the timer list was exhausted.
+ size_t drained_ = 0;
+
+ // The highwater mark of timers that are running.
+ size_t maxRunning_ = 0;
+
+ // The list of timers that are scheduled. This set is sorted by timeout and then by timer
+ // ID. A set is sufficient (as opposed to a multiset) because timer IDs are unique.
+ std::set<Entry> running_;
+};
+
+
+AnrTimerService::AnrTimerService(char const* label,
+ notifier_t notifier, void* cookie, jweak jtimer, Ticker* ticker) :
+ label_(label),
+ notifier_(notifier),
+ notifierCookie_(cookie),
+ notifierObject_(jtimer),
+ ticker_(ticker) {
+
+ // Zero the statistics
+ maxActive_ = 0;
+ memset(&counters_, 0, sizeof(counters_));
+
+ ALOGI_IF(DEBUG, "initialized %s", label);
+}
+
+AnrTimerService::~AnrTimerService() {
+ AutoMutex _l(lock_);
+ ticker_->remove(this);
+}
+
+char const *AnrTimerService::statusString(Status s) {
+ switch (s) {
+ case Invalid: return "invalid";
+ case Running: return "running";
+ case Expired: return "expired";
+ case Canceled: return "canceled";
+ }
+ return "unknown";
+}
+
+AnrTimerService::timer_id_t AnrTimerService::start(int pid, int uid,
+ nsecs_t timeout, bool extend) {
+ ALOGI_IF(DEBUG, "starting");
+ AutoMutex _l(lock_);
+ Timer t(pid, uid, timeout, extend);
+ insert(t);
+ counters_.started++;
+
+ ALOGI_IF(DEBUG, "started timer %u timeout=%zu", t.id, static_cast<size_t>(timeout));
+ return t.id;
+}
+
+bool AnrTimerService::cancel(timer_id_t timerId) {
+ ALOGI_IF(DEBUG, "canceling %u", timerId);
+ if (timerId == NOTIMER) return false;
+ AutoMutex _l(lock_);
+ Timer timer = remove(timerId);
+
+ bool result = timer.status == Running;
+ if (timer.status != Invalid) {
+ timer.cancel();
+ } else {
+ counters_.error++;
+ }
+ counters_.canceled++;
+ ALOGI_IF(DEBUG, "canceled timer %u", timerId);
+ return result;
+}
+
+bool AnrTimerService::accept(timer_id_t timerId) {
+ ALOGI_IF(DEBUG, "accepting %u", timerId);
+ if (timerId == NOTIMER) return false;
+ AutoMutex _l(lock_);
+ Timer timer = remove(timerId);
+
+ bool result = timer.status == Expired;
+ if (timer.status == Expired) {
+ timer.accept();
+ } else {
+ counters_.error++;
+ }
+ counters_.accepted++;
+ ALOGI_IF(DEBUG, "accepted timer %u", timerId);
+ return result;
+}
+
+bool AnrTimerService::discard(timer_id_t timerId) {
+ ALOGI_IF(DEBUG, "discarding %u", timerId);
+ if (timerId == NOTIMER) return false;
+ AutoMutex _l(lock_);
+ Timer timer = remove(timerId);
+
+ bool result = timer.status == Expired;
+ if (timer.status == Expired) {
+ timer.discard();
+ } else {
+ counters_.error++;
+ }
+ counters_.discarded++;
+ ALOGI_IF(DEBUG, "discarded timer %u", timerId);
+ return result;
+}
+
+// Hold the lock in order to manage the running list.
+// the listener.
+void AnrTimerService::expire(timer_id_t timerId) {
+ ALOGI_IF(DEBUG, "expiring %u", timerId);
+ // Save the timer attributes for the notification
+ int pid = 0;
+ int uid = 0;
+ bool expired = false;
+ {
+ AutoMutex _l(lock_);
+ Timer t = remove(timerId);
+ expired = t.expire();
+ if (t.status == Invalid) {
+ ALOGW_IF(DEBUG, "error: expired invalid timer %u", timerId);
+ return;
+ } else {
+ // The timer is either Running (because it was extended) or expired (and is awaiting an
+ // accept or discard).
+ insert(t);
+ }
+ }
+
+ // Deliver the notification outside of the lock.
+ if (expired) {
+ if (!notifier_(timerId, pid, uid, notifierCookie_, notifierObject_)) {
+ AutoMutex _l(lock_);
+ // Notification failed, which means the listener will never call accept() or
+ // discard(). Do not reinsert the timer.
+ remove(timerId);
+ }
+ }
+ ALOGI_IF(DEBUG, "expired timer %u", timerId);
+}
+
+void AnrTimerService::insert(const Timer& t) {
+ running_.insert(t);
+ if (t.status == Running) {
+ // Only forward running timers to the ticker. Expired timers are handled separately.
+ ticker_->insert(t.scheduled, t.id, this);
+ maxActive_ = std::max(maxActive_, running_.size());
+ }
+}
+
+AnrTimerService::Timer AnrTimerService::remove(timer_id_t timerId) {
+ Timer key(timerId);
+ auto found = running_.find(key);
+ if (found != running_.end()) {
+ Timer result = *found;
+ running_.erase(found);
+ ticker_->remove(result.scheduled, result.id);
+ return result;
+ }
+ return Timer();
+}
+
+void AnrTimerService::dump(bool verbose) const {
+ AutoMutex _l(lock_);
+ ALOGI("timer %s ops started=%zu canceled=%zu accepted=%zu discarded=%zu expired=%zu",
+ label_.c_str(),
+ counters_.started, counters_.canceled, counters_.accepted,
+ counters_.discarded, counters_.expired);
+ ALOGI("timer %s stats max-active=%zu/%zu running=%zu/%zu errors=%zu",
+ label_.c_str(),
+ maxActive_, ticker_->maxRunning(), running_.size(), ticker_->running(),
+ counters_.error);
+
+ if (verbose) {
+ nsecs_t time = now();
+ for (auto i = running_.begin(); i != running_.end(); i++) {
+ Timer t = *i;
+ ALOGI(" running %s", t.toString(time).c_str());
+ }
+ }
+}
+
+/**
+ * True if the native methods are supported in this process. Native methods are supported only
+ * if the initialization succeeds.
+ */
+bool nativeSupportEnabled = false;
+
+/**
+ * Singleton/globals for the anr timer. Among other things, this includes a Ticker* and a use
+ * count. The JNI layer creates a single Ticker for all operational AnrTimers. The Ticker is
+ * created when the first AnrTimer is created, and is deleted when the last AnrTimer is closed.
+ */
+static Mutex gAnrLock;
+struct AnrArgs {
+ jclass clazz = NULL;
+ jmethodID func = NULL;
+ JavaVM* vm = NULL;
+ AnrTimerService::Ticker* ticker = nullptr;
+ int tickerUseCount = 0;;
+};
+static AnrArgs gAnrArgs;
+
+// The cookie is the address of the AnrArgs object to which the notification should be sent.
+static bool anrNotify(AnrTimerService::timer_id_t timerId, int pid, int uid,
+ void* cookie, jweak jtimer) {
+ AutoMutex _l(gAnrLock);
+ AnrArgs* target = reinterpret_cast<AnrArgs* >(cookie);
+ JNIEnv *env;
+ if (target->vm->AttachCurrentThread(&env, 0) != JNI_OK) {
+ ALOGE("failed to attach thread to JavaVM");
+ return false;
+ }
+ jboolean r = false;
+ jobject timer = env->NewGlobalRef(jtimer);
+ if (timer != nullptr) {
+ r = env->CallBooleanMethod(timer, target->func, timerId, pid, uid);
+ env->DeleteGlobalRef(timer);
+ }
+ target->vm->DetachCurrentThread();
+ return r;
+}
+
+jboolean anrTimerSupported(JNIEnv* env, jclass) {
+ return nativeSupportEnabled;
+}
+
+jlong anrTimerCreate(JNIEnv* env, jobject jtimer, jstring jname) {
+ if (!nativeSupportEnabled) return 0;
+ AutoMutex _l(gAnrLock);
+ if (!gAnrArgs.ticker) {
+ gAnrArgs.ticker = new AnrTimerService::Ticker();
+ }
+ gAnrArgs.tickerUseCount++;
+
+ ScopedUtfChars name(env, jname);
+ jobject timer = env->NewWeakGlobalRef(jtimer);
+ AnrTimerService* service =
+ new AnrTimerService(name.c_str(), anrNotify, &gAnrArgs, timer, gAnrArgs.ticker);
+ return reinterpret_cast<jlong>(service);
+}
+
+AnrTimerService *toService(jlong pointer) {
+ return reinterpret_cast<AnrTimerService*>(pointer);
+}
+
+jint anrTimerClose(JNIEnv* env, jclass, jlong ptr) {
+ if (!nativeSupportEnabled) return -1;
+ if (ptr == 0) return -1;
+ AutoMutex _l(gAnrLock);
+ AnrTimerService *s = toService(ptr);
+ env->DeleteWeakGlobalRef(s->jtimer());
+ delete s;
+ if (--gAnrArgs.tickerUseCount <= 0) {
+ delete gAnrArgs.ticker;
+ gAnrArgs.ticker = nullptr;
+ }
+ return 0;
+}
+
+jint anrTimerStart(JNIEnv* env, jclass, jlong ptr,
+ jint pid, jint uid, jlong timeout, jboolean extend) {
+ if (!nativeSupportEnabled) return 0;
+ // On the Java side, timeouts are expressed in milliseconds and must be converted to
+ // nanoseconds before being passed to the library code.
+ return toService(ptr)->start(pid, uid, milliseconds_to_nanoseconds(timeout), extend);
+}
+
+jboolean anrTimerCancel(JNIEnv* env, jclass, jlong ptr, jint timerId) {
+ if (!nativeSupportEnabled) return false;
+ return toService(ptr)->cancel(timerId);
+}
+
+jboolean anrTimerAccept(JNIEnv* env, jclass, jlong ptr, jint timerId) {
+ if (!nativeSupportEnabled) return false;
+ return toService(ptr)->accept(timerId);
+}
+
+jboolean anrTimerDiscard(JNIEnv* env, jclass, jlong ptr, jint timerId) {
+ if (!nativeSupportEnabled) return false;
+ return toService(ptr)->discard(timerId);
+}
+
+jint anrTimerDump(JNIEnv *env, jclass, jlong ptr, jboolean verbose) {
+ if (!nativeSupportEnabled) return -1;
+ toService(ptr)->dump(verbose);
+ return 0;
+}
+
+static const JNINativeMethod methods[] = {
+ {"nativeAnrTimerSupported", "()Z", (void*) anrTimerSupported},
+ {"nativeAnrTimerCreate", "(Ljava/lang/String;)J", (void*) anrTimerCreate},
+ {"nativeAnrTimerClose", "(J)I", (void*) anrTimerClose},
+ {"nativeAnrTimerStart", "(JIIJZ)I", (void*) anrTimerStart},
+ {"nativeAnrTimerCancel", "(JI)Z", (void*) anrTimerCancel},
+ {"nativeAnrTimerAccept", "(JI)Z", (void*) anrTimerAccept},
+ {"nativeAnrTimerDiscard", "(JI)Z", (void*) anrTimerDiscard},
+ {"nativeAnrTimerDump", "(JZ)V", (void*) anrTimerDump},
+};
+
+} // anonymous namespace
+
+int register_android_server_utils_AnrTimer(JNIEnv* env)
+{
+ static const char *className = "com/android/server/utils/AnrTimer";
+ jniRegisterNativeMethods(env, className, methods, NELEM(methods));
+
+ jclass service = FindClassOrDie(env, className);
+ gAnrArgs.clazz = MakeGlobalRefOrDie(env, service);
+ gAnrArgs.func = env->GetMethodID(gAnrArgs.clazz, "expire", "(III)Z");
+ env->GetJavaVM(&gAnrArgs.vm);
+
+ nativeSupportEnabled = NATIVE_SUPPORT;
+
+ return 0;
+}
+
+} // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 11734da..f3158d1 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -52,6 +52,7 @@
int register_android_server_HardwarePropertiesManagerService(JNIEnv* env);
int register_android_server_SyntheticPasswordManager(JNIEnv* env);
int register_android_hardware_display_DisplayViewport(JNIEnv* env);
+int register_android_server_utils_AnrTimer(JNIEnv *env);
int register_android_server_am_OomConnection(JNIEnv* env);
int register_android_server_am_CachedAppOptimizer(JNIEnv* env);
int register_android_server_am_LowMemDetector(JNIEnv* env);
@@ -113,6 +114,7 @@
register_android_server_storage_AppFuse(env);
register_android_server_SyntheticPasswordManager(env);
register_android_hardware_display_DisplayViewport(env);
+ register_android_server_utils_AnrTimer(env);
register_android_server_am_OomConnection(env);
register_android_server_am_CachedAppOptimizer(env);
register_android_server_am_LowMemDetector(env);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index a490013..f288103 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -6243,9 +6243,8 @@
final long id = mInjector.binderClearCallingIdentity();
try {
- final KeyChainConnection keyChainConnection =
- KeyChain.bindAsUser(mContext, caller.getUserHandle());
- try {
+ try (KeyChainConnection keyChainConnection =
+ KeyChain.bindAsUser(mContext, caller.getUserHandle())) {
IKeyChainService keyChain = keyChainConnection.getService();
if (!keyChain.installKeyPair(privKey, cert, chain, alias, KeyStore.UID_SELF)) {
logInstallKeyPairFailure(caller, isCredentialManagementApp);
@@ -6263,10 +6262,8 @@
? CREDENTIAL_MANAGEMENT_APP : NOT_CREDENTIAL_MANAGEMENT_APP)
.write();
return true;
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "Installing certificate", e);
- } finally {
- keyChainConnection.close();
}
} catch (InterruptedException e) {
Slogf.w(LOG_TAG, "Interrupted while installing certificate", e);
@@ -6313,9 +6310,8 @@
final long id = Binder.clearCallingIdentity();
try {
- final KeyChainConnection keyChainConnection =
- KeyChain.bindAsUser(mContext, caller.getUserHandle());
- try {
+ try (KeyChainConnection keyChainConnection =
+ KeyChain.bindAsUser(mContext, caller.getUserHandle())) {
IKeyChainService keyChain = keyChainConnection.getService();
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.REMOVE_KEY_PAIR)
@@ -6325,10 +6321,8 @@
? CREDENTIAL_MANAGEMENT_APP : NOT_CREDENTIAL_MANAGEMENT_APP)
.write();
return keyChain.removeKeyPair(alias);
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "Removing keypair", e);
- } finally {
- keyChainConnection.close();
}
} catch (InterruptedException e) {
Slogf.w(LOG_TAG, "Interrupted while removing keypair", e);
@@ -6355,7 +6349,7 @@
try (KeyChainConnection keyChainConnection =
KeyChain.bindAsUser(mContext, caller.getUserHandle())) {
return keyChainConnection.getService().containsKeyPair(alias);
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "Querying keypair", e);
} catch (InterruptedException e) {
Slogf.w(LOG_TAG, "Interrupted while querying keypair", e);
@@ -6417,7 +6411,7 @@
}
}
return false;
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "Querying grant to wifi auth.", e);
return false;
}
@@ -6497,7 +6491,7 @@
}
result.put(uid, new ArraySet<String>(packages));
}
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "Querying keypair grants", e);
} catch (InterruptedException e) {
Slogf.w(LOG_TAG, "Interrupted while querying keypair grants", e);
@@ -6667,7 +6661,7 @@
.write();
return true;
}
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "KeyChain error while generating a keypair", e);
} catch (InterruptedException e) {
Slogf.w(LOG_TAG, "Interrupted while generating keypair", e);
@@ -6742,7 +6736,7 @@
} catch (InterruptedException e) {
Slogf.w(LOG_TAG, "Interrupted while setting keypair certificate", e);
Thread.currentThread().interrupt();
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "Failed setting keypair certificate", e);
} finally {
mInjector.binderRestoreCallingIdentity(id);
@@ -7227,7 +7221,7 @@
connection.getService().getCredentialManagementAppPolicy();
return policy != null && !policy.getAppAndUriMappings().isEmpty()
&& containsAlias(policy, alias);
- } catch (RemoteException | InterruptedException e) {
+ } catch (RemoteException | InterruptedException | AssertionError e) {
return false;
}
});
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceDemoModeTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceDemoModeTest.kt
new file mode 100644
index 0000000..dfdb0c7
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceDemoModeTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm
+
+import android.content.res.Configuration
+import android.os.Looper
+import android.os.SystemProperties
+import android.os.UserHandle
+import android.util.ArrayMap
+import com.android.server.LockGuard
+import com.android.server.extendedtestutils.wheneverStatic
+import com.android.server.testutils.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.MockitoAnnotations
+
+@RunWith(JUnit4::class)
+class UserManagerServiceDemoModeTest {
+ private lateinit var ums: UserManagerService
+
+ @Rule
+ @JvmField
+ val rule = MockSystemRule()
+
+ @Before
+ @Throws(Exception::class)
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ rule.system().stageNominalSystemState()
+
+ if (Looper.myLooper() == null) {
+ Looper.prepare()
+ }
+
+ wheneverStatic { LockGuard.installNewLock(LockGuard.INDEX_USER) }.thenReturn(Object())
+ whenever(rule.mocks().systemConfig.getAndClearPackageToUserTypeWhitelist()).thenReturn(ArrayMap<String, Set<String>>())
+ whenever(rule.mocks().systemConfig.getAndClearPackageToUserTypeBlacklist()).thenReturn(ArrayMap<String, Set<String>>())
+ whenever(rule.mocks().resources.getStringArray(com.android.internal.R.array.config_defaultFirstUserRestrictions)).thenReturn(arrayOf<String>())
+ whenever(rule.mocks().resources.configuration).thenReturn(Configuration())
+
+ ums = UserManagerService(rule.mocks().context)
+ }
+
+ @Test
+ fun isDemoUser_returnsTrue_whenSystemPropertyIsSet() {
+ wheneverStatic { SystemProperties.getBoolean("ro.boot.arc_demo_mode", false) }.thenReturn(true)
+
+ assertThat(ums.isDemoUser(0)).isTrue()
+ }
+
+ @Test
+ fun isDemoUser_returnsFalse_whenSystemPropertyIsSet() {
+ wheneverStatic { SystemProperties.getBoolean("ro.boot.arc_demo_mode", false) }.thenReturn(false)
+
+ assertThat(ums.isDemoUser(0)).isFalse()
+ }
+
+ @Test
+ fun isDemoUser_returnsFalse_whenSystemPropertyIsNotSet() {
+ assertThat(ums.isDemoUser(0)).isFalse()
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/jni/Android.bp b/services/tests/servicestests/jni/Android.bp
index 174beb8..c30e4eb 100644
--- a/services/tests/servicestests/jni/Android.bp
+++ b/services/tests/servicestests/jni/Android.bp
@@ -23,6 +23,7 @@
":lib_cachedAppOptimizer_native",
":lib_gameManagerService_native",
":lib_oomConnection_native",
+ ":lib_anrTimer_native",
"onload.cpp",
],
@@ -55,4 +56,4 @@
"android.hardware.graphics.mapper@4.0",
"android.hidl.token@1.0-utils",
],
-}
\ No newline at end of file
+}
diff --git a/services/tests/servicestests/jni/onload.cpp b/services/tests/servicestests/jni/onload.cpp
index f160b3d..25487c5 100644
--- a/services/tests/servicestests/jni/onload.cpp
+++ b/services/tests/servicestests/jni/onload.cpp
@@ -27,6 +27,7 @@
int register_android_server_am_CachedAppOptimizer(JNIEnv* env);
int register_android_server_app_GameManagerService(JNIEnv* env);
int register_android_server_am_OomConnection(JNIEnv* env);
+int register_android_server_utils_AnrTimer(JNIEnv *env);
};
using namespace android;
@@ -44,5 +45,6 @@
register_android_server_am_CachedAppOptimizer(env);
register_android_server_app_GameManagerService(env);
register_android_server_am_OomConnection(env);
+ register_android_server_utils_AnrTimer(env);
return JNI_VERSION_1_4;
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
index efcdbd4..1cd61e9 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
@@ -44,10 +44,6 @@
import android.graphics.PointF;
import android.os.Looper;
import android.os.SystemClock;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.testing.DexmakerShareClassLoaderRule;
import android.view.InputDevice;
import android.view.MotionEvent;
@@ -60,7 +56,6 @@
import com.android.server.accessibility.AccessibilityManagerService;
import com.android.server.accessibility.AccessibilityTraceManager;
import com.android.server.accessibility.EventStreamTransformation;
-import com.android.server.accessibility.Flags;
import com.android.server.accessibility.utils.GestureLogParser;
import com.android.server.testutils.OffsettableClock;
@@ -81,7 +76,6 @@
import java.util.ArrayList;
import java.util.List;
-
@RunWith(AndroidJUnit4.class)
public class TouchExplorerTest {
@@ -125,9 +119,6 @@
public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
new DexmakerShareClassLoaderRule();
- @Rule
- public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
-
/**
* {@link TouchExplorer#sendDownForAllNotInjectedPointers} injecting events with the same object
* is resulting {@link ArgumentCaptor} to capture events with last state. Before implementation
@@ -170,16 +161,11 @@
goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER);
// Wait for transiting to touch exploring state.
mHandler.fastForward(2 * USER_INTENT_TIMEOUT);
- assertState(STATE_TOUCH_EXPLORING);
- // Manually construct the next move event. Using moveEachPointers() will batch the move
- // event which produces zero movement for some reason.
- float[] x = new float[1];
- float[] y = new float[1];
- x[0] = mLastEvent.getX(0) + mTouchSlop;
- y[0] = mLastEvent.getY(0) + mTouchSlop;
- send(manyPointerEvent(ACTION_MOVE, x, y));
+ moveEachPointers(mLastEvent, p(10, 10));
+ send(mLastEvent);
goToStateClearFrom(STATE_TOUCH_EXPLORING_1FINGER);
assertCapturedEvents(ACTION_HOVER_ENTER, ACTION_HOVER_MOVE, ACTION_HOVER_EXIT);
+ assertState(STATE_TOUCH_EXPLORING);
}
/**
@@ -187,8 +173,7 @@
* change the coordinates.
*/
@Test
- @RequiresFlagsEnabled(Flags.FLAG_REDUCE_TOUCH_EXPLORATION_SENSITIVITY)
- public void testOneFingerMoveWithExtraMoveEvents_generatesOneMoveEvent() {
+ public void testOneFingerMoveWithExtraMoveEvents() {
goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER);
// Inject a set of move events that have the same coordinates as the down event.
moveEachPointers(mLastEvent, p(0, 0));
@@ -196,33 +181,7 @@
// Wait for transition to touch exploring state.
mHandler.fastForward(2 * USER_INTENT_TIMEOUT);
// Now move for real.
- moveAtLeastTouchSlop(mLastEvent);
- send(mLastEvent);
- // One more move event with no change.
- moveEachPointers(mLastEvent, p(0, 0));
- send(mLastEvent);
- goToStateClearFrom(STATE_TOUCH_EXPLORING_1FINGER);
- assertCapturedEvents(
- ACTION_HOVER_ENTER,
- ACTION_HOVER_MOVE,
- ACTION_HOVER_EXIT);
- }
-
- /**
- * Test the case where ACTION_DOWN is followed by a number of ACTION_MOVE events that do not
- * change the coordinates.
- */
- @Test
- @RequiresFlagsDisabled(Flags.FLAG_REDUCE_TOUCH_EXPLORATION_SENSITIVITY)
- public void testOneFingerMoveWithExtraMoveEvents_generatesThreeMoveEvent() {
- goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER);
- // Inject a set of move events that have the same coordinates as the down event.
- moveEachPointers(mLastEvent, p(0, 0));
- send(mLastEvent);
- // Wait for transition to touch exploring state.
- mHandler.fastForward(2 * USER_INTENT_TIMEOUT);
- // Now move for real.
- moveAtLeastTouchSlop(mLastEvent);
+ moveEachPointers(mLastEvent, p(10, 10));
send(mLastEvent);
// One more move event with no change.
moveEachPointers(mLastEvent, p(0, 0));
@@ -283,7 +242,7 @@
moveEachPointers(mLastEvent, p(0, 0), p(0, 0));
send(mLastEvent);
// Now move for real.
- moveEachPointers(mLastEvent, p(mTouchSlop, mTouchSlop), p(mTouchSlop, mTouchSlop));
+ moveEachPointers(mLastEvent, p(10, 10), p(10, 10));
send(mLastEvent);
goToStateClearFrom(STATE_DRAGGING_2FINGERS);
assertCapturedEvents(ACTION_DOWN, ACTION_MOVE, ACTION_MOVE, ACTION_MOVE, ACTION_UP);
@@ -292,7 +251,7 @@
@Test
public void testUpEvent_OneFingerMove_clearStateAndInjectHoverEvents() {
goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER);
- moveAtLeastTouchSlop(mLastEvent);
+ moveEachPointers(mLastEvent, p(10, 10));
send(mLastEvent);
// Wait 10 ms to make sure that hover enter and exit are not scheduled for the same moment.
mHandler.fastForward(10);
@@ -318,7 +277,7 @@
// Wait for the finger moving to the second view.
mHandler.fastForward(oneThirdUserIntentTimeout);
- moveAtLeastTouchSlop(mLastEvent);
+ moveEachPointers(mLastEvent, p(10, 10));
send(mLastEvent);
// Wait for the finger lifting from the second view.
@@ -443,6 +402,7 @@
// Manually construct the next move event. Using moveEachPointers() will batch the move
// event onto the pointer up event which will mean that the move event still has a pointer
// count of 3.
+ // Todo: refactor to avoid using batching as there is no special reason to do it that way.
float[] x = new float[2];
float[] y = new float[2];
x[0] = mLastEvent.getX(0) + 100;
@@ -774,9 +734,6 @@
}
}
- private void moveAtLeastTouchSlop(MotionEvent event) {
- moveEachPointers(event, p(2 * mTouchSlop, 0));
- }
/**
* A {@link android.os.Handler} that doesn't process messages until {@link #fastForward(int)} is
* invoked.
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
index 6dd9171..a2f8c8b 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
@@ -1239,9 +1239,6 @@
methodMap, 0 /* userId */);
assertEquals(0, settings.getCurrentUserId());
- settings.isShowImeWithHardKeyboardEnabled();
- verify(ownerUserContext.getContentResolver(), atLeastOnce()).getAttributionSource();
-
settings.getEnabledInputMethodSubtypeListLocked(nonSystemIme, true);
verify(ownerUserContext.getResources(), atLeastOnce()).getConfiguration();
@@ -1250,10 +1247,6 @@
settings.switchCurrentUser(10 /* userId */);
assertEquals(10, settings.getCurrentUserId());
- settings.isShowImeWithHardKeyboardEnabled();
- verify(TestContext.getSecondaryUserContext().getContentResolver(),
- atLeastOnce()).getAttributionSource();
-
settings.getEnabledInputMethodSubtypeListLocked(nonSystemIme, true);
verify(TestContext.getSecondaryUserContext().getResources(),
atLeastOnce()).getConfiguration();
diff --git a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java
index 835ccf0..6fffd75 100644
--- a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java
@@ -112,7 +112,7 @@
testServiceDefaultValue_On(ServiceType.NULL);
}
- @Suppress
+ @Suppress // TODO: b/317823111 - Remove once test fixed.
@SmallTest
public void testGetBatterySaverPolicy_PolicyVibration_DefaultValueCorrect() {
testDefaultValue(
@@ -219,7 +219,7 @@
ServiceType.QUICK_DOZE);
}
- @Suppress
+ @Suppress // TODO: b/317823111 - Remove once test fixed.
@SmallTest
public void testUpdateConstants_getCorrectData() {
mBatterySaverPolicy.updateConstantsLocked(BATTERY_SAVER_CONSTANTS, "");
@@ -327,6 +327,7 @@
}
}
+ @Suppress // TODO: b/317823111 - Remove once test fixed.
public void testSetPolicyLevel_Adaptive() {
mBatterySaverPolicy.setPolicyLevel(POLICY_LEVEL_ADAPTIVE);
diff --git a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
index 861d14a..6c085e0 100644
--- a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
+++ b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
@@ -23,17 +23,21 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.util.Log;
import android.platform.test.annotations.Presubmit;
import androidx.test.filters.SmallTest;
import com.android.internal.annotations.GuardedBy;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -45,6 +49,9 @@
@RunWith(Parameterized.class)
public class AnrTimerTest {
+ // A log tag.
+ private static final String TAG = "AnrTimerTest";
+
// The commonly used message timeout key.
private static final int MSG_TIMEOUT = 1;
@@ -63,9 +70,7 @@
}
}
- /**
- * The test handler is a self-contained object for a single test.
- */
+ /** The test helper is a self-contained object for a single test. */
private static class Helper {
final Object mLock = new Object();
@@ -114,7 +119,7 @@
/**
* Force AnrTimer to use the test parameter for the feature flag.
*/
- class TestInjector extends AnrTimer.Injector {
+ private class TestInjector extends AnrTimer.Injector {
@Override
boolean anrTimerServiceEnabled() {
return mEnabled;
@@ -124,9 +129,9 @@
/**
* An instrumented AnrTimer.
*/
- private static class TestAnrTimer extends AnrTimer<TestArg> {
+ private class TestAnrTimer extends AnrTimer<TestArg> {
private TestAnrTimer(Handler h, int key, String tag) {
- super(h, key, tag);
+ super(h, key, tag, false, new TestInjector());
}
TestAnrTimer(Helper helper) {
@@ -173,35 +178,103 @@
@Test
public void testSimpleTimeout() throws Exception {
Helper helper = new Helper(1);
- TestAnrTimer timer = new TestAnrTimer(helper);
- TestArg t = new TestArg(1, 1);
- timer.start(t, 10);
- // Delivery is immediate but occurs on a different thread.
- assertTrue(helper.await(5000));
- TestArg[] result = helper.messages(1);
- validate(t, result[0]);
+ try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+ // One-time check that the injector is working as expected.
+ assertEquals(mEnabled, timer.serviceEnabled());
+ TestArg t = new TestArg(1, 1);
+ timer.start(t, 10);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(1);
+ validate(t, result[0]);
+ }
}
/**
- * Verify that if three timers are scheduled, they are delivered in time order.
+ * Verify that a restarted timer is delivered exactly once. The initial timer value is very
+ * large, to ensure it does not expire before the timer can be restarted.
+ */
+ @Test
+ public void testTimerRestart() throws Exception {
+ Helper helper = new Helper(1);
+ try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+ TestArg t = new TestArg(1, 1);
+ timer.start(t, 10000);
+ // Briefly pause.
+ assertFalse(helper.await(10));
+ timer.start(t, 10);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(1);
+ validate(t, result[0]);
+ }
+ }
+
+ /**
+ * Verify that a restarted timer is delivered exactly once. The initial timer value is very
+ * large, to ensure it does not expire before the timer can be restarted.
+ */
+ @Test
+ public void testTimerZero() throws Exception {
+ Helper helper = new Helper(1);
+ try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+ TestArg t = new TestArg(1, 1);
+ timer.start(t, 0);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(1);
+ validate(t, result[0]);
+ }
+ }
+
+ /**
+ * Verify that if three timers are scheduled on a single AnrTimer, they are delivered in time
+ * order.
*/
@Test
public void testMultipleTimers() throws Exception {
// Expect three messages.
Helper helper = new Helper(3);
- TestAnrTimer timer = new TestAnrTimer(helper);
TestArg t1 = new TestArg(1, 1);
TestArg t2 = new TestArg(1, 2);
TestArg t3 = new TestArg(1, 3);
- timer.start(t1, 50);
- timer.start(t2, 60);
- timer.start(t3, 40);
- // Delivery is immediate but occurs on a different thread.
- assertTrue(helper.await(5000));
- TestArg[] result = helper.messages(3);
- validate(t3, result[0]);
- validate(t1, result[1]);
- validate(t2, result[2]);
+ try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+ timer.start(t1, 50);
+ timer.start(t2, 60);
+ timer.start(t3, 40);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(3);
+ validate(t3, result[0]);
+ validate(t1, result[1]);
+ validate(t2, result[2]);
+ }
+ }
+
+ /**
+ * Verify that if three timers are scheduled on three separate AnrTimers, they are delivered
+ * in time order.
+ */
+ @Test
+ public void testMultipleServices() throws Exception {
+ // Expect three messages.
+ Helper helper = new Helper(3);
+ TestArg t1 = new TestArg(1, 1);
+ TestArg t2 = new TestArg(1, 2);
+ TestArg t3 = new TestArg(1, 3);
+ try (TestAnrTimer x1 = new TestAnrTimer(helper);
+ TestAnrTimer x2 = new TestAnrTimer(helper);
+ TestAnrTimer x3 = new TestAnrTimer(helper)) {
+ x1.start(t1, 50);
+ x2.start(t2, 60);
+ x3.start(t3, 40);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(3);
+ validate(t3, result[0]);
+ validate(t1, result[1]);
+ validate(t2, result[2]);
+ }
}
/**
@@ -211,20 +284,109 @@
public void testCancelTimer() throws Exception {
// Expect two messages.
Helper helper = new Helper(2);
- TestAnrTimer timer = new TestAnrTimer(helper);
TestArg t1 = new TestArg(1, 1);
TestArg t2 = new TestArg(1, 2);
TestArg t3 = new TestArg(1, 3);
- timer.start(t1, 50);
- timer.start(t2, 60);
- timer.start(t3, 40);
- // Briefly pause.
- assertFalse(helper.await(10));
- timer.cancel(t1);
- // Delivery is immediate but occurs on a different thread.
- assertTrue(helper.await(5000));
- TestArg[] result = helper.messages(2);
- validate(t3, result[0]);
- validate(t2, result[1]);
+ try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+ timer.start(t1, 50);
+ timer.start(t2, 60);
+ timer.start(t3, 40);
+ // Briefly pause.
+ assertFalse(helper.await(10));
+ timer.cancel(t1);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(2);
+ validate(t3, result[0]);
+ validate(t2, result[1]);
+ }
+ }
+
+ /**
+ * Return the dump string.
+ */
+ private String getDumpOutput() {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ AnrTimer.dump(pw, true, new TestInjector());
+ pw.close();
+ return sw.getBuffer().toString();
+ }
+
+ /**
+ * Verify the dump output.
+ */
+ @Test
+ public void testDumpOutput() throws Exception {
+ String r1 = getDumpOutput();
+ assertEquals(false, r1.contains("timer:"));
+
+ Helper helper = new Helper(2);
+ TestArg t1 = new TestArg(1, 1);
+ TestArg t2 = new TestArg(1, 2);
+ TestArg t3 = new TestArg(1, 3);
+ try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+ timer.start(t1, 5000);
+ timer.start(t2, 5000);
+ timer.start(t3, 5000);
+
+ String r2 = getDumpOutput();
+ // There are timers in the list if and only if the feature is enabled.
+ final boolean expected = mEnabled;
+ assertEquals(expected, r2.contains("timer:"));
+ }
+
+ String r3 = getDumpOutput();
+ assertEquals(false, r3.contains("timer:"));
+ }
+
+ /**
+ * Verify that GC works as expected. This test will almost certainly be flaky, since it
+ * relies on the finalizers running, which is a best-effort on the part of the JVM.
+ * Therefore, the test is marked @Ignore. Remove that annotation to run the test locally.
+ */
+ @Ignore
+ @Test
+ public void testGarbageCollection() throws Exception {
+ if (!mEnabled) return;
+
+ String r1 = getDumpOutput();
+ assertEquals(false, r1.contains("timer:"));
+
+ Helper helper = new Helper(2);
+ TestArg t1 = new TestArg(1, 1);
+ TestArg t2 = new TestArg(1, 2);
+ TestArg t3 = new TestArg(1, 3);
+ // The timer is explicitly not closed. It is, however, scoped to the next block.
+ {
+ TestAnrTimer timer = new TestAnrTimer(helper);
+ timer.start(t1, 5000);
+ timer.start(t2, 5000);
+ timer.start(t3, 5000);
+
+ String r2 = getDumpOutput();
+ // There are timers in the list if and only if the feature is enabled.
+ final boolean expected = mEnabled;
+ assertEquals(expected, r2.contains("timer:"));
+ }
+
+ // Try to make finalizers run. The timer object above should be a candidate. Finalizers
+ // are run on their own thread, so pause this thread to give that thread some time.
+ String r3 = getDumpOutput();
+ for (int i = 0; i < 10 && r3.contains("timer:"); i++) {
+ Log.i(TAG, "requesting finalization " + i);
+ System.gc();
+ System.runFinalization();
+ Thread.sleep(4 * 1000);
+ r3 = getDumpOutput();
+ }
+
+ // The timer was not explicitly closed but it should have been implicitly closed by GC.
+ assertEquals(false, r3.contains("timer:"));
+ }
+
+ // TODO: [b/302724778] Remove manual JNI load
+ static {
+ System.loadLibrary("servicestestjni");
}
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 39779b0..f1edd9a 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -303,7 +303,6 @@
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
@@ -14061,7 +14060,6 @@
}
@Test
- @Ignore("b/316989461")
public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne()
throws RemoteException {
mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
@@ -14073,7 +14071,8 @@
mService.addNotification(nr1);
// Create old notification.
- final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr2);
// Cancel specific notifications via listener.
@@ -14091,16 +14090,17 @@
}
@Test
- @Ignore("b/316989461")
public void cancelNotificationsFromListener_rapidClear_old_cancelOne() throws RemoteException {
mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
.FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
// Create old notifications.
- final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr1);
- final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr2);
// Cancel specific notifications via listener.
@@ -14119,7 +14119,6 @@
}
@Test
- @Ignore("b/316989461")
public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne_flagDisabled()
throws RemoteException {
mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
@@ -14131,7 +14130,8 @@
mService.addNotification(nr1);
// Create old notification.
- final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr2);
// Cancel specific notifications via listener.
@@ -14150,7 +14150,6 @@
}
@Test
- @Ignore("b/316989461")
public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll()
throws RemoteException {
mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
@@ -14162,7 +14161,8 @@
mService.addNotification(nr1);
// Create old notification.
- final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr2);
// Cancel all notifications via listener.
@@ -14179,16 +14179,17 @@
}
@Test
- @Ignore("b/316989461")
public void cancelNotificationsFromListener_rapidClear_old_cancelAll() throws RemoteException {
mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
.FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
// Create old notifications.
- final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr1);
- final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr2);
// Cancel all notifications via listener.
@@ -14206,7 +14207,6 @@
}
@Test
- @Ignore("b/316989461")
public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll_flagDisabled()
throws RemoteException {
mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
@@ -14218,7 +14218,8 @@
mService.addNotification(nr1);
// Create old notification.
- final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr2);
// Cancel all notifications via listener.
diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
index 5363583..29467f2 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
@@ -16,6 +16,10 @@
package com.android.server.policy;
+import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static android.view.WindowManagerGlobal.ADD_OKAY;
+
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
@@ -26,11 +30,16 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.filters.SmallTest;
@@ -39,6 +48,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
/**
@@ -50,6 +60,9 @@
@SmallTest
public class PhoneWindowManagerTests {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
PhoneWindowManager mPhoneWindowManager;
@Before
@@ -85,6 +98,36 @@
verify(mPhoneWindowManager).createHomeDockIntent();
}
+ @Test
+ public void testCheckAddPermission_withoutAccessibilityOverlay_noAccessibilityAppOpLogged() {
+ mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
+ .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED);
+ int[] outAppOp = new int[1];
+ assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_WALLPAPER,
+ /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp));
+ assertThat(outAppOp[0]).isEqualTo(AppOpsManager.OP_NONE);
+ }
+
+ @Test
+ public void testCheckAddPermission_withAccessibilityOverlay() {
+ mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
+ .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED);
+ int[] outAppOp = new int[1];
+ assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_ACCESSIBILITY_OVERLAY,
+ /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp));
+ assertThat(outAppOp[0]).isEqualTo(AppOpsManager.OP_CREATE_ACCESSIBILITY_OVERLAY);
+ }
+
+ @Test
+ public void testCheckAddPermission_withAccessibilityOverlay_flagDisabled() {
+ mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
+ .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED);
+ int[] outAppOp = new int[1];
+ assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_ACCESSIBILITY_OVERLAY,
+ /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp));
+ assertThat(outAppOp[0]).isEqualTo(AppOpsManager.OP_NONE);
+ }
+
private void mockStartDockOrHome() throws Exception {
doNothing().when(ActivityManager.getService()).stopAppSwitches();
ActivityTaskManagerInternal mMockActivityTaskManagerInternal =
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 6497ee9..782d89c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -115,6 +115,9 @@
import android.os.RemoteException;
import android.os.SystemClock;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.view.Display;
@@ -146,6 +149,7 @@
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.wm.utils.WmDisplayCutout;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -172,6 +176,10 @@
@RunWith(WindowTestRunner.class)
public class DisplayContentTests extends WindowTestsBase {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
+
@SetupWindows(addAllCommonWindows = true)
@Test
public void testForAllWindows() {
@@ -508,6 +516,7 @@
* Tests tapping on a root task in different display results in window gaining focus.
*/
@Test
+ @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_REMOVE_POINTER_EVENT_TRACKING_IN_WM)
public void testInputEventBringsCorrectDisplayInFocus() {
DisplayContent dc0 = mWm.getDefaultDisplayContentLocked();
// Create a second display
diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
index 810cbe8..0f1e4d1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -137,6 +138,25 @@
}
@Test
+ public void testFinishSyncByStartingWindow() {
+ final ActivityRecord taskRoot = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ final Task task = taskRoot.getTask();
+ final ActivityRecord translucentTop = new ActivityBuilder(mAtm).setTask(task)
+ .setActivityTheme(android.R.style.Theme_Translucent).build();
+ createWindow(null, TYPE_BASE_APPLICATION, taskRoot, "win");
+ final WindowState startingWindow = createWindow(null, TYPE_APPLICATION_STARTING,
+ translucentTop, "starting");
+ startingWindow.mStartingData = new SnapshotStartingData(mWm, null, 0);
+ task.mSharedStartingData = startingWindow.mStartingData;
+ task.prepareSync();
+
+ final BLASTSyncEngine.SyncGroup group = mock(BLASTSyncEngine.SyncGroup.class);
+ assertFalse(task.isSyncFinished(group));
+ startingWindow.onSyncFinishedDrawing();
+ assertTrue(task.isSyncFinished(group));
+ }
+
+ @Test
public void testInvisibleSyncCallback() {
TestWindowContainer mockWC = new TestWindowContainer(mWm, true /* waiter */);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index d36ee2c..a88285a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -34,6 +34,7 @@
import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK;
import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE;
@@ -870,12 +871,19 @@
.setAnimationParams(animationParams)
.build();
mTransaction.addTaskFragmentOperation(mFragmentToken, operation);
+ final TaskFragmentOperation dimOperation = new TaskFragmentOperation.Builder(
+ OP_TYPE_SET_DIM_ON_TASK)
+ .setDimOnTask(true)
+ .build();
+ mTransaction.addTaskFragmentOperation(mFragmentToken, dimOperation);
mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE,
false /* shouldApplyIndependently */);
assertApplyTransactionAllowed(mTransaction);
assertEquals(animationParams, mTaskFragment.getAnimationParams());
assertEquals(Color.GREEN, mTaskFragment.getAnimationParams().getAnimationBackgroundColor());
+
+ assertTrue(mTaskFragment.isDimmingOnParentTask());
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 875e708..e9fe4bb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -684,6 +684,9 @@
// Return Task bounds if dimming on parent Task.
final Rect dimBounds = new Rect();
mTaskFragment.setEmbeddedDimArea(EMBEDDED_DIM_AREA_PARENT_TASK);
+ final Dimmer dimmer = mTaskFragment.getDimmer();
+ spyOn(dimmer);
+ doReturn(taskBounds).when(dimmer).getDimBounds();
mTaskFragment.getDimBounds(dimBounds);
assertEquals(taskBounds, dimBounds);
diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java
index df349f8..c958aba 100644
--- a/telephony/java/android/telephony/SmsManager.java
+++ b/telephony/java/android/telephony/SmsManager.java
@@ -563,7 +563,10 @@
* raw pdu of the status report is in the extended data ("pdu").
*
* @throws IllegalArgumentException if destinationAddress or text are empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendTextMessage(
String destinationAddress, String scAddress, String text,
PendingIntent sentIntent, PendingIntent deliveryIntent) {
@@ -581,8 +584,11 @@
* Used for logging and diagnostics purposes. The id may be 0.
*
* @throws IllegalArgumentException if destinationAddress or text are empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendTextMessage(
@NonNull String destinationAddress, @Nullable String scAddress, @NonNull String text,
@Nullable PendingIntent sentIntent, @Nullable PendingIntent deliveryIntent,
@@ -788,12 +794,16 @@
* </p>
*
* @see #sendTextMessage(String, String, String, PendingIntent, PendingIntent)
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
@SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@RequiresPermission(allOf = {
android.Manifest.permission.MODIFY_PHONE_STATE,
android.Manifest.permission.SEND_SMS
})
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendTextMessageWithoutPersisting(
String destinationAddress, String scAddress, String text,
PendingIntent sentIntent, PendingIntent deliveryIntent) {
@@ -908,7 +918,10 @@
* {@link #RESULT_REMOTE_EXCEPTION} for error.
*
* @throws IllegalArgumentException if the format is invalid.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void injectSmsPdu(
byte[] pdu, @SmsMessage.Format String format, PendingIntent receivedIntent) {
if (!format.equals(SmsMessage.FORMAT_3GPP) && !format.equals(SmsMessage.FORMAT_3GPP2)) {
@@ -940,6 +953,7 @@
* @return an <code>ArrayList</code> of strings that, in order, comprise the original message.
* @throws IllegalArgumentException if text is null.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public ArrayList<String> divideMessage(String text) {
if (null == text) {
throw new IllegalArgumentException("text is null");
@@ -1046,7 +1060,10 @@
* extended data ("pdu").
*
* @throws IllegalArgumentException if destinationAddress or data are empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendMultipartTextMessage(
String destinationAddress, String scAddress, ArrayList<String> parts,
ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents) {
@@ -1062,8 +1079,10 @@
* Used for logging and diagnostics purposes. The id may be 0.
*
* @throws IllegalArgumentException if destinationAddress or data are empty
- *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendMultipartTextMessage(
@NonNull String destinationAddress, @Nullable String scAddress,
@NonNull List<String> parts, @Nullable List<PendingIntent> sentIntents,
@@ -1089,7 +1108,11 @@
*
* @param packageName serves as the default package name if the package name that is
* associated with the user id is null.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendMultipartTextMessage(
@NonNull String destinationAddress, @Nullable String scAddress,
@NonNull List<String> parts, @Nullable List<PendingIntent> sentIntents,
@@ -1191,10 +1214,14 @@
* </p>
*
* @see #sendMultipartTextMessage(String, String, ArrayList, ArrayList, ArrayList)
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
* @hide
**/
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendMultipartTextMessageWithoutPersisting(
String destinationAddress, String scAddress, List<String> parts,
List<PendingIntent> sentIntents, List<PendingIntent> deliveryIntents) {
@@ -1498,7 +1525,10 @@
* raw pdu of the status report is in the extended data ("pdu").
*
* @throws IllegalArgumentException if destinationAddress or data are empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendDataMessage(
String destinationAddress, String scAddress, short destinationPort,
byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
@@ -1609,6 +1639,7 @@
* .{@link #createForSubscriptionId createForSubscriptionId(subId)} instead
*/
@Deprecated
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public static SmsManager getSmsManagerForSubscriptionId(int subId) {
return getSmsManagerForContextAndSubscriptionId(null, subId);
}
@@ -1626,6 +1657,7 @@
* @see SubscriptionManager#getActiveSubscriptionInfoList()
* @see SubscriptionManager#getDefaultSmsSubscriptionId()
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public @NonNull SmsManager createForSubscriptionId(int subId) {
return getSmsManagerForContextAndSubscriptionId(mContext, subId);
}
@@ -1651,7 +1683,11 @@
* @return associated subscription ID or {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID} if
* the default subscription id cannot be determined or the device has multiple active
* subscriptions and and no default is set ("ask every time") by the user.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public int getSubscriptionId() {
try {
return (mSubId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)
@@ -2018,10 +2054,14 @@
*
* @throws IllegalArgumentException if endMessageId < startMessageId
* @deprecated Use {@link TelephonyManager#setCellBroadcastIdRanges} instead.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
* {@hide}
*/
@Deprecated
@SystemApi
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public boolean enableCellBroadcastRange(int startMessageId, int endMessageId,
@android.telephony.SmsCbMessage.MessageFormat int ranType) {
boolean success = false;
@@ -2079,11 +2119,15 @@
* @see #enableCellBroadcastRange(int, int, int)
*
* @throws IllegalArgumentException if endMessageId < startMessageId
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
+ *
* @deprecated Use {@link TelephonyManager#setCellBroadcastIdRanges} instead.
* {@hide}
*/
@Deprecated
@SystemApi
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public boolean disableCellBroadcastRange(int startMessageId, int endMessageId,
@android.telephony.SmsCbMessage.MessageFormat int ranType) {
boolean success = false;
@@ -2223,7 +2267,11 @@
* @return the user-defined default SMS subscription id, or the active subscription id if
* there's only one active subscription available, otherwise
* {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID}.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public static int getDefaultSmsSubscriptionId() {
try {
return getISmsService().getPreferredSmsSubscription();
@@ -2271,10 +2319,14 @@
* </p>
*
* @return the total number of SMS records which can be stored on the SIM card.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
@RequiresPermission(anyOf = {android.Manifest.permission.READ_PHONE_STATE,
android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE})
@IntRange(from = 0)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public int getSmsCapacityOnIcc() {
int ret = 0;
try {
@@ -2819,7 +2871,10 @@
* <code>MMS_ERROR_DATA_DISABLED</code><br>
* <code>MMS_ERROR_MMS_DISABLED_BY_CARRIER</code><br>
* @throws IllegalArgumentException if contentUri is empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendMultimediaMessage(Context context, Uri contentUri, String locationUrl,
Bundle configOverrides, PendingIntent sentIntent) {
sendMultimediaMessage(context, contentUri, locationUrl, configOverrides, sentIntent,
@@ -2863,7 +2918,10 @@
* @param messageId an id that uniquely identifies the message requested to be sent.
* Used for logging and diagnostics purposes. The id may be 0.
* @throws IllegalArgumentException if contentUri is empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendMultimediaMessage(@NonNull Context context, @NonNull Uri contentUri,
@Nullable String locationUrl,
@SuppressWarnings("NullableCollection") @Nullable Bundle configOverrides,
@@ -2922,7 +2980,10 @@
* <code>MMS_ERROR_DATA_DISABLED</code><br>
* <code>MMS_ERROR_MMS_DISABLED_BY_CARRIER</code><br>
* @throws IllegalArgumentException if locationUrl or contentUri is empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void downloadMultimediaMessage(Context context, String locationUrl, Uri contentUri,
Bundle configOverrides, PendingIntent downloadedIntent) {
downloadMultimediaMessage(context, locationUrl, contentUri, configOverrides,
@@ -2968,7 +3029,10 @@
* @param messageId an id that uniquely identifies the message requested to be downloaded.
* Used for logging and diagnostics purposes. The id may be 0.
* @throws IllegalArgumentException if locationUrl or contentUri is empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void downloadMultimediaMessage(@NonNull Context context, @NonNull String locationUrl,
@NonNull Uri contentUri,
@SuppressWarnings("NullableCollection") @Nullable Bundle configOverrides,
@@ -3079,7 +3143,11 @@
*
* @return the bundle key/values pairs that contains MMS configuration values
* or an empty Bundle if they cannot be found.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
@NonNull public Bundle getCarrierConfigValues() {
try {
ISms iSms = getISmsService();
@@ -3115,7 +3183,11 @@
*
* @return Token to include in an SMS message. The token will be 11 characters long.
* @see android.provider.Telephony.Sms.Intents#getMessagesFromIntent
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public String createAppSpecificSmsToken(PendingIntent intent) {
try {
ISms iccSms = getISmsServiceOrThrow();
@@ -3233,7 +3305,11 @@
* message.
* @param intent this intent is sent when the matching SMS message is received.
* @return Token to include in an SMS message.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
@Nullable
public String createAppSpecificSmsTokenWithPackageInfo(
@Nullable String prefixes, @NonNull PendingIntent intent) {
@@ -3393,9 +3469,13 @@
* </p>
*
* @return the SMSC address string, null if failed.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
@SuppressAutoDoc // for carrier privileges and default SMS application.
@RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
@Nullable
public String getSmscAddress() {
String smsc = null;
@@ -3430,9 +3510,13 @@
*
* @param smsc the SMSC address string.
* @return true for success, false otherwise. Failure can be due modem returning an error.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
@SuppressAutoDoc // for carrier privileges and default SMS application.
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public boolean setSmscAddress(@NonNull String smsc) {
try {
ISms iSms = getISmsService();
@@ -3455,10 +3539,14 @@
* {@link SmsManager#PREMIUM_SMS_CONSENT_ASK_USER},
* {@link SmsManager#PREMIUM_SMS_CONSENT_NEVER_ALLOW}, or
* {@link SmsManager#PREMIUM_SMS_CONSENT_ALWAYS_ALLOW}
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public @PremiumSmsConsent int getPremiumSmsConsent(@NonNull String packageName) {
int permission = 0;
try {
@@ -3479,10 +3567,14 @@
* @param permission one of {@link SmsManager#PREMIUM_SMS_CONSENT_ASK_USER},
* {@link SmsManager#PREMIUM_SMS_CONSENT_NEVER_ALLOW}, or
* {@link SmsManager#PREMIUM_SMS_CONSENT_ALWAYS_ALLOW}
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void setPremiumSmsConsent(
@NonNull String packageName, @PremiumSmsConsent int permission) {
try {
@@ -3498,11 +3590,15 @@
/**
* Reset all cell broadcast ranges. Previously enabled ranges will become invalid after this.
* @deprecated Use {@link TelephonyManager#setCellBroadcastIdRanges} with empty list instead
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
* @hide
*/
@Deprecated
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_CELL_BROADCASTS)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void resetAllCellBroadcastRanges() {
try {
ISms iSms = getISmsService();
@@ -3530,6 +3626,8 @@
* available.
* @throws SecurityException if the caller does not have the required permission/privileges.
* @throws IllegalStateException in case of telephony service is not available.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@NonNull
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 6c8663a..ff7b392 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -3785,9 +3785,9 @@
Map<ParcelUuid, SubscriptionInfo> groupMap = new HashMap<>();
for (SubscriptionInfo info : availableList) {
- // Opportunistic subscriptions are considered invisible
+ // Grouped opportunistic subscriptions are considered invisible
// to users so they should never be returned.
- if (!isSubscriptionVisible(info)) continue;
+ if (info.getGroupUuid() != null && info.isOpportunistic()) continue;
ParcelUuid groupUuid = info.getGroupUuid();
if (groupUuid == null) {
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
index 60b5ce7..80c1e5be 100644
--- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
@@ -810,6 +810,7 @@
private FpsRange convertCategory(int category) {
switch (category) {
+ case Surface.FRAME_RATE_CATEGORY_HIGH_HINT:
case Surface.FRAME_RATE_CATEGORY_HIGH:
return FRAME_RATE_CATEGORY_HIGH;
case Surface.FRAME_RATE_CATEGORY_NORMAL:
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java
index 4b56c10..caaee63 100644
--- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java
@@ -93,6 +93,12 @@
}
@Test
+ public void testSurfaceControlFrameRateCategoryHighHint() throws InterruptedException {
+ GraphicsActivity activity = mActivityRule.getActivity();
+ activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_HIGH_HINT);
+ }
+
+ @Test
public void testSurfaceControlFrameRateCategoryNormal() throws InterruptedException {
GraphicsActivity activity = mActivityRule.getActivity();
activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_NORMAL);