diff options
168 files changed, 4557 insertions, 2010 deletions
diff --git a/Android.bp b/Android.bp index 13b170353dd6..f6a9328d2501 100644 --- a/Android.bp +++ b/Android.bp @@ -220,6 +220,7 @@ java_library { "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 c65e50640ee9..de6f0235cd83 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -12,4 +12,11 @@ flag { 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 7ab234ab3e40..95af71cfa074 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4369,7 +4369,7 @@ package android.app { 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 @@ package android.app { 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 45515ddb219a..c1c5c0e6e019 100644 --- a/core/java/android/accounts/AbstractAccountAuthenticator.java +++ b/core/java/android/accounts/AbstractAccountAuthenticator.java @@ -142,10 +142,7 @@ public abstract class AbstractAccountAuthenticator { 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 5674a108baaa..5d4d5e23d6db 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1174,12 +1174,23 @@ public class Activity extends ContextThemeWrapper 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 343348b89625..f9ab55e00dc6 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -70,6 +70,8 @@ public final class AutomaticZenRule implements Parcelable { 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 @@ public final class AutomaticZenRule implements Parcelable { /** * 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 4a6349b1b02f..5c42b0ed975a 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -2598,8 +2598,8 @@ public class DevicePolicyManager { * 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 ec2e5fe23ab9..084cba37de09 100644 --- a/core/java/android/appwidget/flags.aconfig +++ b/core/java/android/appwidget/flags.aconfig @@ -20,3 +20,10 @@ flag { 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/os/HwNoService.java b/core/java/android/os/HwNoService.java index 117c3ad7ee48..084031496629 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 { +final class HwNoService extends IServiceManager.Stub implements IHwBinder, IHwInterface { + private static final String TAG = "HwNoService"; + /** @hide */ @Override - public void transact(int code, HwParcel request, HwParcel reply, int flags) {} + public String toString() { + return "[HwNoService]"; + } - /** @hide */ @Override - public IHwInterface queryLocalInterface(String descriptor) { - return new HwNoService(); + 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; } - /** @hide */ @Override - public boolean linkToDeath(DeathRecipient recipient, long cookie) { + 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 4e5588cce1c9..fe6c4a4321e9 100644 --- a/core/java/android/security/responsible_apis_flags.aconfig +++ b/core/java/android/security/responsible_apis_flags.aconfig @@ -20,3 +20,10 @@ flag { 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/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index a38092a21178..49d2ceb8fecf 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -2067,10 +2067,10 @@ public final class AccessibilityManager { } /** - * 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 @@ public final class AccessibilityManager { } /** - * 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/window/ActivityWindowInfo.aidl b/core/java/android/window/ActivityWindowInfo.aidl new file mode 100644 index 000000000000..d0526bc68fd4 --- /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 000000000000..946bb823398c --- /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 0ec9ffe6390b..acc6a749e9b7 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -120,6 +120,11 @@ public final class TaskFragmentOperation implements Parcelable { */ 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 @@ public final class TaskFragmentOperation implements Parcelable { 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 @@ public final class TaskFragmentOperation implements Parcelable { 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 @@ public final class TaskFragmentOperation implements Parcelable { mSecondaryFragmentToken = secondaryFragmentToken; mAnimationParams = animationParams; mIsolatedNav = isolatedNav; + mDimOnTask = dimOnTask; } private TaskFragmentOperation(Parcel in) { @@ -190,6 +199,7 @@ public final class TaskFragmentOperation implements Parcelable { mSecondaryFragmentToken = in.readStrongBinder(); mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR); mIsolatedNav = in.readBoolean(); + mDimOnTask = in.readBoolean(); } @Override @@ -202,6 +212,7 @@ public final class TaskFragmentOperation implements Parcelable { dest.writeStrongBinder(mSecondaryFragmentToken); dest.writeTypedObject(mAnimationParams, flags); dest.writeBoolean(mIsolatedNav); + dest.writeBoolean(mDimOnTask); } @NonNull @@ -282,6 +293,13 @@ public final class TaskFragmentOperation implements Parcelable { 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 @@ public final class TaskFragmentOperation implements Parcelable { sb.append(", animationParams=").append(mAnimationParams); } sb.append(", isolatedNav=").append(mIsolatedNav); + sb.append(", dimOnTask=").append(mDimOnTask); sb.append('}'); return sb.toString(); @@ -313,7 +332,7 @@ public final class TaskFragmentOperation implements Parcelable { @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 @@ public final class TaskFragmentOperation implements Parcelable { && 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 @@ public final class TaskFragmentOperation implements Parcelable { private boolean mIsolatedNav; + private boolean mDimOnTask; + /** * @param opType the {@link OperationType} of this {@link TaskFragmentOperation}. */ @@ -435,13 +457,22 @@ public final class TaskFragmentOperation implements Parcelable { } /** + * 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 59d7b0e55e85..f743ab74d1f5 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -38,4 +38,12 @@ flag { 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 000000000000..53164f3e1ec9 --- /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 44f436eaaa19..d73e62373732 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 @@ flag { 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 781895eeeaba..477bd096b11a 100644 --- a/core/jni/android_os_HwBinder.cpp +++ b/core/jni/android_os_HwBinder.cpp @@ -258,14 +258,59 @@ static void JHwBinder_native_setup(JNIEnv *env, jobject thiz) { 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 382a82cd090e..2a0feee25e86 100644 --- a/core/proto/android/server/windowmanagerservice.proto +++ b/core/proto/android/server/windowmanagerservice.proto @@ -404,7 +404,7 @@ message WindowTokenProto { 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 d337954ff2a0..120e4bbc743a 100644 --- a/core/sysprop/FoldLockBehaviorProperties.sysprop +++ b/core/sysprop/FoldLockBehaviorProperties.sysprop @@ -22,3 +22,11 @@ prop { 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 421bc25d60e9..bf6094469215 100644 --- a/data/etc/preinstalled-packages-platform.xml +++ b/data/etc/preinstalled-packages-platform.xml @@ -128,4 +128,9 @@ to pre-existing users, but cannot uninstall pre-existing system packages from pr <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 742d5a2627eb..917a30061aca 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 ca3d8d18db83..592f9a57884c 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 @@ package androidx.window.extensions.embedding; 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 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { 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 543570c63ad7..6f356fa35d41 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_FULLSCREEN; 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.WindowLayoutComponentImpl; 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 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { 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 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { 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 810bded8a7f0..b52971a15a3c 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 @@ class TaskFragmentContainer { 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 @@ class TaskFragmentContainer { 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 6981d9d7ebb8..941b4e1c3e41 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 @@ public class SplitPresenterTest { } @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 71bf487249fb..0ef047f44909 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 @@ public abstract class WMShellModule { mainChoreographer, taskOrganizer, displayController, - syncQueue); + syncQueue, + transitions); } // 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 cf1692018518..cebc4006656a 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 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { 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 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { 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 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { 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 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { 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 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { 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 6e7d11d9082b..1debb02e86af 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 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL @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 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL 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 ab29df1f780c..4fd362591151 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } } } - } 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 2f51278e3c80..0c8e93b48d02 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin RelayoutParams relayoutParams, Context context, ActivityManager.RunningTaskInfo taskInfo, - boolean applyStartTransactionOnDraw) { + boolean applyStartTransactionOnDraw, + boolean shouldSetTaskPositionAndCrop) { relayoutParams.reset(); relayoutParams.mRunningTaskInfo = taskInfo; relayoutParams.mLayoutResId = @@ -314,6 +323,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin : 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 677c7f1fb5a8..5afbd54088d1 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.PointF; 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 @@ public class DragPositioningCallbackUtility { 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 @@ public class DragPositioningCallbackUtility { } } - /** - * 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 5d006fb4d9e2..6bfc7cdcb33e 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 @@ class FluidResizeTaskPositioner implements DragPositioningCallback { // 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 @@ class FluidResizeTaskPositioner implements DragPositioningCallback { // 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 @@ class FluidResizeTaskPositioner implements DragPositioningCallback { 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 @@ class FluidResizeTaskPositioner implements DragPositioningCallback { 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 @@ class FluidResizeTaskPositioner implements DragPositioningCallback { || (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 000000000000..40421b599889 --- /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 4363558ca00b..c1b18f959641 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 @@ import java.util.function.Supplier; * 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 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, 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 class VeiledResizeTaskPositioner implements DragPositioningCallback, } 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 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, 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 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, 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 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, @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 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, @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 ee0e31ec3aef..b5373c67c602 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 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> private WindowlessWindowManager mCaptionWindowManager; private SurfaceControlViewHost mViewHost; private Configuration mWindowDecorConfig; + TaskDragResizer mTaskDragResizer; private boolean mIsCaptionVisible; private final Binder mOwner = new Binder(); @@ -311,25 +312,21 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> 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 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } } + void setTaskDragResizer(TaskDragResizer taskDragResizer) { + mTaskDragResizer = taskDragResizer; + } + private void setCaptionVisibility(View rootView, boolean visible) { if (rootView == null) { return; @@ -559,6 +560,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> Configuration mWindowDecorConfig; boolean mApplyStartTransactionOnDraw; + boolean mSetTaskPositionAndCrop; void reset() { mLayoutResId = Resources.ID_NULL; @@ -572,6 +574,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> 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 77667ca579f2..193f16da3e39 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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 2ce49cf62614..de6903d9a06a 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 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.ShellTaskOrganizer 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.never 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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { @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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { 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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { `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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { } @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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { 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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.top.toFloat() ) + 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(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_hasEffectiveMove_issuesTransactionOnMoveAndEnd() { + fun testDragResize_hasEffectiveMove_issuesTransitionOnMoveAndEnd() { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, STARTING_BOUNDS.left.toFloat(), @@ -192,13 +212,13 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { ) 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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { 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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { 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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { 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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { 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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { ) // 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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { 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 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { VALID_DRAG_AREA.bottom && change.configuration.windowConfiguration.bounds.left == VALID_DRAG_AREA.left - } - }) + }}, eq(taskPositioner)) } @Test @@ -741,6 +768,59 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { 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 a759b53f4238..08412101c30c 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_270 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.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.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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { @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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { 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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { ) // 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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { 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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { 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 fe508e23af33..32a91461e40f 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 junit.framework.Assert.assertTrue; 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 @@ public class WindowDecorationTests extends ShellTestCase { 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 @@ public class WindowDecorationTests extends ShellTestCase { 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/pipeline/skia/DumpOpsCanvas.h b/libs/hwui/pipeline/skia/DumpOpsCanvas.h index 6a052dbb7cea..260547cda1c2 100644 --- a/libs/hwui/pipeline/skia/DumpOpsCanvas.h +++ b/libs/hwui/pipeline/skia/DumpOpsCanvas.h @@ -90,11 +90,6 @@ protected: 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 dc36a2e01815..df5f04f9904e 100644 --- a/libs/hwui/tests/common/CallCountingCanvas.h +++ b/libs/hwui/tests/common/CallCountingCanvas.h @@ -109,12 +109,6 @@ public: 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 18c50472a7df..4ae76e2f1fd2 100644 --- a/libs/hwui/tests/unit/CanvasOpTests.cpp +++ b/libs/hwui/tests/unit/CanvasOpTests.cpp @@ -492,7 +492,7 @@ TEST(CanvasOp, simpleDrawImage) { 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 96a0c6114682..8b95e0cd267d 100644 --- a/libs/hwui/tests/unit/FatalTestCanvas.h +++ b/libs/hwui/tests/unit/FatalTestCanvas.h @@ -69,10 +69,6 @@ public: 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 073a8357e574..ca540874833c 100644 --- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp @@ -941,8 +941,9 @@ RENDERTHREAD_TEST(RenderNodeDrawable, simple) { 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 3ded540c3152..785e2869d15e 100644 --- a/libs/hwui/tests/unit/SkiaPipelineTests.cpp +++ b/libs/hwui/tests/unit/SkiaPipelineTests.cpp @@ -303,8 +303,9 @@ RENDERTHREAD_TEST(SkiaPipeline, clipped) { 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 @@ RENDERTHREAD_TEST(SkiaPipeline, clipped_rotated) { 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/location/java/android/location/flags/gnss.aconfig b/location/java/android/location/flags/gnss.aconfig index a8464d3f86ec..794a555e22cb 100644 --- a/location/java/android/location/flags/gnss.aconfig +++ b/location/java/android/location/flags/gnss.aconfig @@ -34,3 +34,10 @@ flag { 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 e0df7945ab0e..193728a1c780 100644 --- a/native/graphics/jni/libjnigraphics.map.txt +++ b/native/graphics/jni/libjnigraphics.map.txt @@ -18,21 +18,21 @@ LIBJNIGRAPHICS { 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 679f696ff59f..b29cb2ab308c 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 @@ import com.android.packageinstaller.v2.ui.InstallActionListener; 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 @@ public class AnonymousSourceFragment extends DialogFragment { @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 @@ public class AnonymousSourceFragment extends DialogFragment { .setNegativeButton(R.string.cancel, ((dialog, which) -> mInstallActionListener.onNegativeResponse( InstallStage.STAGE_USER_ACTION_REQUIRED))).create(); + return mDialog; } @Override @@ -60,4 +64,24 @@ public class AnonymousSourceFragment extends DialogFragment { 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 49901de96bc4..2314d6b3b47e 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 @@ import com.android.packageinstaller.v2.ui.InstallActionListener; 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 @@ public class ExternalSourcesBlockedFragment extends DialogFragment { @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 @@ public class ExternalSourcesBlockedFragment extends DialogFragment { (dialog, which) -> mInstallActionListener.onNegativeResponse( mDialogData.getStageCode())) .create(); + return mDialog; } @Override @@ -69,4 +74,24 @@ public class ExternalSourcesBlockedFragment extends DialogFragment { 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 25363d0b5f7b..5ca02eae5167 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 @@ public class InstallConfirmationFragment extends DialogFragment { private final InstallUserActionRequired mDialogData; @NonNull private InstallActionListener mInstallActionListener; + @NonNull + private AlertDialog mDialog; public InstallConfirmationFragment(@NonNull InstallUserActionRequired dialogData) { mDialogData = dialogData; @@ -58,7 +60,7 @@ public class InstallConfirmationFragment extends DialogFragment { 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 @@ public class InstallConfirmationFragment extends DialogFragment { } viewToEnable.setVisibility(View.VISIBLE); - return dialog; + return mDialog; } @Override @@ -92,4 +94,24 @@ public class InstallConfirmationFragment extends DialogFragment { 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 14bcac2cc46c..aa0903cab7aa 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -145,6 +145,16 @@ flag { } 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 @@ flag { 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/keyguard/ui/composable/ViewBasedLockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt index 976161b3beb7..8119d2a119ca 100644 --- 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 @@ -31,6 +31,7 @@ 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.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel import com.android.systemui.notifications.ui.composable.NotificationStack import com.android.systemui.res.R @@ -47,8 +48,9 @@ import javax.inject.Inject class ViewBasedLockscreenContent @Inject constructor( - private val viewModel: LockscreenSceneViewModel, + private val lockscreenSceneViewModel: LockscreenSceneViewModel, @KeyguardRootView private val viewProvider: () -> @JvmSuppressWildcards View, + private val keyguardRootViewModel: KeyguardRootViewModel, ) { @Composable fun SceneScope.Content( @@ -59,7 +61,7 @@ constructor( } LockscreenLongPress( - viewModel = viewModel.longPress, + viewModel = lockscreenSceneViewModel.longPress, modifier = modifier, ) { onSettingsMenuPlaced -> AndroidView( @@ -74,7 +76,7 @@ constructor( ) val notificationStackPosition by - viewModel.keyguardRoot.notificationBounds.collectAsState() + keyguardRootViewModel.notificationBounds.collectAsState() Layout( modifier = @@ -92,7 +94,7 @@ constructor( }, content = { NotificationStack( - viewModel = viewModel.notifications, + viewModel = lockscreenSceneViewModel.notifications, isScrimVisible = false, ) } 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 000000000000..c4184905f28d --- /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 d9d98cbd2da6..7385a251200e 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.Modifier 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 @@ -55,6 +56,7 @@ constructor( private val ambientIndicationSection: AmbientIndicationSection, private val bottomAreaSection: BottomAreaSection, private val settingsMenuSection: SettingsMenuSection, + private val clockInteractor: KeyguardClockInteractor, ) : LockscreenSceneBlueprint { override val id: String = "default" @@ -62,6 +64,7 @@ constructor( @Composable override fun SceneScope.Content(modifier: Modifier) { val isUdfpsVisible = viewModel.isUdfpsVisible + val burnIn = rememberBurnIn(clockInteractor) LockscreenLongPress( viewModel = viewModel.longPress, @@ -74,8 +77,19 @@ constructor( 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)) 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 4704f5c3d1eb..acd47797baca 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.Modifier 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 @@ -55,6 +56,7 @@ constructor( private val ambientIndicationSection: AmbientIndicationSection, private val bottomAreaSection: BottomAreaSection, private val settingsMenuSection: SettingsMenuSection, + private val clockInteractor: KeyguardClockInteractor, ) : LockscreenSceneBlueprint { override val id: String = "shortcuts-besides-udfps" @@ -62,6 +64,7 @@ constructor( @Composable override fun SceneScope.Content(modifier: Modifier) { val isUdfpsVisible = viewModel.isUdfpsVisible + val burnIn = rememberBurnIn(clockInteractor) LockscreenLongPress( viewModel = viewModel.longPress, @@ -74,8 +77,19 @@ constructor( 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)) 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 000000000000..f9dd04b66b1f --- /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 0e7ac5ec046a..1e5481ebfa6c 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 @@ -35,14 +35,16 @@ class AmbientIndicationSection @Inject constructor() { 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), - ) + content { + Box( + modifier = Modifier.fillMaxWidth().background(Color.Green), + ) { + Text( + text = "TODO(b/316211368): Ambient indication", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } } } } 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 db20f65ee78d..8bd0d45920f4 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.animation.view.LaunchableImageView 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( key = IndicationAreaElementKey, modifier = modifier.shortcutPadding(), ) { - IndicationArea( - indicationAreaViewModel = indicationAreaViewModel, - keyguardRootViewModel = keyguardRootViewModel, - indicationController = indicationController, - ) + content { + IndicationArea( + indicationAreaViewModel = indicationAreaViewModel, + alphaViewModel = alphaViewModel, + indicationController = indicationController, + ) + } } } @@ -179,7 +183,7 @@ constructor( @Composable private fun IndicationArea( indicationAreaViewModel: KeyguardIndicationAreaViewModel, - keyguardRootViewModel: KeyguardRootViewModel, + alphaViewModel: AodAlphaViewModel, indicationController: KeyguardIndicationController, modifier: Modifier = Modifier, ) { @@ -192,7 +196,7 @@ constructor( 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 eaf8063b6f15..0f3fc47d4e91 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 @@ -26,6 +26,7 @@ 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 com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import javax.inject.Inject @@ -35,8 +36,12 @@ constructor( private val viewModel: KeyguardClockViewModel, ) { @Composable - fun SceneScope.SmallClock(modifier: Modifier = Modifier) { + fun SceneScope.SmallClock( + onTopChanged: (top: Float?) -> Unit, + modifier: Modifier = Modifier, + ) { if (viewModel.useLargeClock) { + onTopChanged(null) return } @@ -44,14 +49,19 @@ constructor( 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 { + Box( + modifier = + Modifier.fillMaxWidth() + .background(Color.Magenta) + .onTopPlacementChanged(onTopChanged) + ) { + Text( + text = "TODO(b/316211368): Small clock", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } } } } @@ -66,14 +76,16 @@ constructor( 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 { + Box( + modifier = Modifier.fillMaxWidth().background(Color.Blue), + ) { + Text( + text = "TODO(b/316211368): Large clock", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } } } } 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 c547e2b93158..900616f6af89 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.compose.animation.scene.SceneScope 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/SmartSpaceSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt index 3c49cbcc1f7a..9b718444b75c 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 androidx.compose.ui.viewinterop.AndroidView 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( 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 6811eb4cea5c..ddc12ff22d50 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.LayoutInflater 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 @@ constructor( key = StatusBarElementKey, modifier = modifier, ) { - AndroidView( - factory = { - notificationPanelView.get().findViewById<View>(R.id.keyguard_header)?.let { - (it.parent as ViewGroup).removeView(it) - } + content { + 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 - val provider = - object : ShadeViewStateProvider { - override val lockscreenShadeDragProgress: Float = 0f - override val panelViewExpandedHeight: Float = 0f - override fun shouldHeadsUpBeVisible(): Boolean { - return false + 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().height { - Utils.getStatusBarHeaderHeightKeyguard(context) + @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 12f1b301c836..0eec024d3c81 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 androidx.compose.ui.unit.dp 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 @@ private fun SceneScope.NotificationPlaceholder( 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 @@ private fun SceneScope.NotificationPlaceholder( } ) { 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 f3cde534ebaa..65a53f57f2e7 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 @@ fun SceneScope.QuickSettings( 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 4bbb78b69392..99f81ee5c7d3 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 androidx.compose.ui.viewinterop.AndroidView 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 @@ import com.android.systemui.statusbar.policy.Clock object ShadeHeader { object Elements { - val FormatPlaceholder = ElementKey("ShadeHeaderFormatPlaceholder") val ExpandedContent = ElementKey("ShadeHeaderExpandedContent") val CollapsedContent = ElementKey("ShadeHeaderCollapsedContent") } @@ -92,14 +91,7 @@ fun SceneScope.CollapsedShadeHeader( 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 @@ fun SceneScope.ExpandedShadeHeader( 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 2944bd9f9a8e..b26194f2397b 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 androidx.compose.ui.unit.lerp 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 + + onDispose { + // Remove the value associated to the current scene, and eventually remove the maps if + // they are empty. + sceneToValueMap.remove(scene) - if (value != sharedValue.value) { - sharedValue.value = value + 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 a85d9bff283e..280fbfb7d3d3 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 @@ import kotlinx.coroutines.launch @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 @@ internal class Element(val key: ElementKey) { 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 @@ internal class Element(val key: ElementKey) { 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 @@ internal fun Modifier.element( 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 @@ internal fun Modifier.element( 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 @@ internal class ElementNode( 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 @@ private fun shouldDrawElement( 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 @@ private fun shouldDrawElement( transition, scene.key, element.key, - sharedTransformation, ) } @@ -319,17 +290,14 @@ internal fun shouldDrawOrComposeSharedElement( 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 @@ private fun isElementOpaque( 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 @@ private fun elementAlpha( layoutImpl: SceneTransitionLayoutImpl, element: Element, scene: Scene, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, ): Float { return computeValue( layoutImpl, @@ -426,9 +394,8 @@ private fun elementAlpha( 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 @@ private fun IntermediateMeasureScope.measure( 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 @@ private fun IntermediateMeasureScope.measure( 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 @@ private fun IntermediateMeasureScope.measure( ) 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 @@ private fun getDrawScale( layoutImpl: SceneTransitionLayoutImpl, element: Element, scene: Scene, - sceneValues: Element.TargetValues + sceneState: Element.SceneState ): Scale { return computeValue( layoutImpl, @@ -505,8 +472,8 @@ private fun getDrawScale( 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 @@ private fun IntermediateMeasureScope.place( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, - sceneValues: Element.TargetValues, + sceneState: Element.SceneState, placeable: Placeable, placementScope: Placeable.PlacementScope, ) { @@ -526,14 +493,14 @@ private fun IntermediateMeasureScope.place( // 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 @@ private fun IntermediateMeasureScope.place( 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 @@ private inline fun <T> computeValue( 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 @@ private inline fun <T> computeValue( 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 @@ private inline fun <T> computeValue( // 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 @@ private inline fun <T> computeValue( // 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 @@ private inline fun <T> computeValue( 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 84d3b8647d6c..90f46bd4dcaa 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 @@ class ElementKey( 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 49df2f6b6062..af3c0999c97b 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 @@ internal fun MovableElement( 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() - - // Draw the content. - drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) } - } - } - ) { - element.movableContent { movableElementScope.content() } - } + .also { layoutImpl.movableContents[element] = it } + + // 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 @@ internal fun MovableElement( private fun shouldComposeMovableElement( layoutImpl: SceneTransitionLayoutImpl, scene: SceneKey, - element: Element, + element: ElementKey, ): Boolean { val transition = layoutImpl.state.currentTransition @@ -130,72 +180,55 @@ private fun shouldComposeMovableElement( 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 560e92becba5..454c0ecf8ac5 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 @@ private class PunchHoleNode( 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 @@ private class PunchHoleNode( 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 30e50a972230..3537b7989ed5 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.gestures.Orientation 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 @@ internal class Scene( 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 @@ internal class Scene( } } -private class SceneScopeImpl( +internal class SceneScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val scene: Scene, ) : SceneScope { @@ -87,6 +81,42 @@ private class SceneScopeImpl( 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 @@ private class SceneScopeImpl( 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 5eb339e4a5e4..84fade8937ff 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.Composable 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 @@ interface SceneTransitionLayoutScope { */ @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 @@ interface SceneScope { * 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,82 +203,114 @@ interface SceneScope { ): 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. + * Punch a hole in this [element] using the bounds of [bounds] in [scene] and the given [shape]. * - * @see element + * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area. + * This can be used to make content drawn below an opaque element visible. For example, if we + * have [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below + * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big + * clock time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be + * the result. */ - @Composable - fun MovableElement( - key: ElementKey, - modifier: Modifier, - content: @Composable MovableElementScope.() -> Unit, - ) + fun Modifier.punchHole(element: ElementKey, bounds: ElementKey, shape: Shape): Modifier + + /** + * Don't resize during transitions. This can for instance be used to make sure that scrollable + * lists keep a constant size during transitions even if its elements are growing/shrinking. + */ + fun Modifier.noResizeDuringTransitions(): Modifier +} +@Stable +@ElementDsl +interface SceneScope : BaseSceneScope { /** - * Animate some value of a shared element. + * 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 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 + * @see animateSceneIntAsState + * @see animateSceneFloatAsState + * @see animateSceneDpAsState + * @see animateSceneColorAsState */ @Composable - fun <T> animateSharedValueAsState( + fun <T> animateSceneValueAsState( value: T, key: ValueKey, - element: ElementKey?, lerp: (start: T, stop: T, fraction: Float) -> T, canOverflow: Boolean, - ): State<T> + ): AnimatedState<T> +} +@Stable +@ElementDsl +interface ElementScope<ContentScope> { /** - * Punch a hole in this [element] using the bounds of [bounds] in [scene] and the given [shape]. + * Animate some value associated to this element. * - * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area. - * This can be used to make content drawn below an opaque element visible. For example, if we - * have [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below - * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big - * clock time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be - * the result. + * @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 */ - fun Modifier.punchHole(element: ElementKey, bounds: ElementKey, shape: Shape): Modifier + @Composable + fun <T> animateElementValueAsState( + value: T, + key: ValueKey, + lerp: (start: T, stop: T, fraction: Float) -> T, + canOverflow: Boolean, + ): AnimatedState<T> /** - * Don't resize during transitions. This can for instance be used to make sure that scrollable - * lists keep a constant size during transitions even if its elements are growing/shrinking. + * The content of this element. + * + * Important: This must be called exactly once, after all calls to [animateElementValueAsState]. */ - fun Modifier.noResizeDuringTransitions(): Modifier + @Composable fun content(content: @Composable ContentScope.() -> Unit) } -// TODO(b/291053742): Add animateSharedValueAsState(targetValue) without any ValueKey and ElementKey -// arguments to allow sharing values inside a movable element. +/** + * 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 MovableElementScope { - @Composable - fun <T> animateSharedValueAsState( - value: T, - debugName: String, - lerp: (start: T, stop: T, fraction: Float) -> T, - canOverflow: Boolean, - ): State<T> +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 45e1a0fa8f77..0227aba94b53 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 androidx.compose.ui.util.fastForEach 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 @@ internal class SceneTransitionLayoutImpl( /** * 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 d1ba582d6c23..0607aa148157 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 @@ sealed interface TransitionState { /** 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 @@ internal class SceneTransitionLayoutStateImpl( 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 dfa2a9a18e91..dc8505c43889 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 @@ interface TransitionBuilder : PropertyTransformationBuilder { * * @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,44 +130,132 @@ interface TransitionBuilder : PropertyTransformationBuilder { 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 + + /** + * 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, + ): SceneKey { + 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 + } + } } -object DefaultSharedElementScenePicker : SharedElementScenePicker { +/** An [ElementScenePicker] that draws/composes elements in the scene with the highest z-order. */ +object HighestZIndexScenePicker : ElementScenePicker { override fun sceneDuringTransition( element: ElementKey, - fromScene: SceneKey, - toScene: SceneKey, - progress: () -> Float, + transition: TransitionState.Transition, 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) - ) { - fromScene + return if (fromSceneZIndex > toSceneZIndex) { + transition.fromScene } else { - toScene + 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 70468669297c..b96f9bebb08b 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 @@ internal class TransitionBuilderImpl : TransitionBuilder { 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 40c814e0f25c..124ec290f42a 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 @@ internal class AnchoredSize( 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 a1d63193bc73..7aa702b0bbd2 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 @@ internal class AnchoredTranslate( 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 d1cf8ee6ad4a..6704a3bbeff2 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 @@ internal class DrawScale( 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 70534dde4f6f..191a8fbcd009 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 @@ internal class EdgeTranslate( 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 17032dc288e0..41f626e24e79 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 @@ internal class Fade( 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 233ae597090b..f5207dc4d345 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 @@ internal class ScaleSize( 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 0cd11b9914c9..04254fbb588b 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.Element 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 @@ sealed interface Transformation { 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 @@ internal sealed interface PropertyTransformation<T> : Transformation { 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 864b937a3fe0..04d5914bff69 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 @@ internal class Translate( 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 5473186c14ec..a116501a298c 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 @@ 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.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.compose.ui.unit.lerp 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 @@ class AnimatedSharedAsStateTest { 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 @@ class AnimatedSharedAsStateTest { ) { 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) + + LaunchedEffect(Unit) { + snapshotFlow { Values(int, float, dp, color) }.collect(onCurrentValueChanged) + } + } + } - // 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) } + @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 @@ class AnimatedSharedAsStateTest { } 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 000000000000..3b022e8adc72 --- /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 da5a0a04ed63..54c5de710f77 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 @@ class ElementTest { 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 @@ class ElementTest { 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 @@ class ElementTest { 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 @@ class ElementTest { 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 @@ class ElementTest { 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 @@ class ElementTest { // 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 @@ class ElementTest { 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 @@ class ElementTest { // 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 @@ class ElementTest { 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 @@ class ElementTest { 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 000000000000..fb46a34e3cab --- /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 3cd65cde274e..35cb691e6e37 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.getValue 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 @@ class MovableElementTest { @Composable private fun SceneScope.MovableCounter(key: ElementKey, modifier: Modifier) { - MovableElement(key, modifier) { Counter() } + MovableElement(key, modifier) { content { Counter() } } } @Test @@ -142,39 +147,37 @@ class MovableElementTest { @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 @@ class MovableElementTest { } } } + + @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 c5b8d9ae0d10..75dee47a91cd 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 @@ class SceneTransitionLayoutStateTest { 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 ebbd5006be55..649e4991434e 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 @@ class SceneTransitionLayoutTest { @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) - - Box( - Modifier.offset { - val pxOffset = offset.roundToPx() - IntOffset(pxOffset, pxOffset) - } - .size(30.dp) - .background(Color.Blue) - .testTag(TestElements.Bar.debugName) - ) + val offset by animateElementDpAsState(childOffset, TestValues.Value1) + + 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 000000000000..238b21e1ea37 --- /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/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt new file mode 100644 index 000000000000..83782e214780 --- /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 000000000000..0543bc257440 --- /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/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt new file mode 100644 index 000000000000..7c3dc972cfd0 --- /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 d07836d3abce..74d309c1d359 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.SceneKey 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 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { KeyguardLongPressViewModel( interactor = mock(), ), - keyguardRoot = utils.keyguardRootViewModel(), notifications = utils.notificationsPlaceholderViewModel(), ) } 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 224903ff36b8..efd4f9bdf449 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 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { KeyguardLongPressViewModel( interactor = mock(), ), - keyguardRoot = utils.keyguardRootViewModel(), notifications = utils.notificationsPlaceholderViewModel(), ) diff --git a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt index 603471b1de41..7a560e846318 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 @@ package com.android.keyguard.mediator 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 @@ class ScreenOnCoordinator @Inject constructor( 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 8a1a2da6cf3f..a4f90ebfb83c 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.Inject; 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 @@ public class AuthController implements 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 @@ public class AuthController implements @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/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index a25c78871115..92300efdc930 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.people.PeopleHubModule; 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 @@ import javax.inject.Named; ClockRegistryModule.class, CommunalModule.class, CommonDataLayerModule.class, + ConfigurationControllerModule.class, ConnectivityModule.class, ControlsModule.class, CoroutinesModule.class, 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 6cb68bade9a9..89bfd96d2408 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 @@ data class SliderHapticFeedbackConfig( 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 9e6245ae7f21..6f28ab7f414c 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 @@ class SliderHapticFeedbackProvider( 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 af5d48d9ae07..50836fe9ee51 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -39,6 +39,7 @@ import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder 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 @@ -83,6 +84,7 @@ constructor( private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor, private val vibratorHelper: VibratorHelper, private val falsingManager: FalsingManager, + private val aodAlphaViewModel: AodAlphaViewModel, ) : CoreStartable { private var rootViewHandle: DisposableHandle? = null @@ -126,7 +128,7 @@ constructor( KeyguardIndicationAreaBinder.bind( notificationShadeWindowView.requireViewById(R.id.keyguard_indication_area), keyguardIndicationAreaViewModel, - keyguardRootViewModel, + aodAlphaViewModel, indicationController, ) } @@ -148,7 +150,6 @@ constructor( 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 000000000000..70c2e6d56ca3 --- /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 ba7b9870103a..91f8420393e1 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 @@ constructor( 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 2d43897c2565..fbf693625f63 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 @@ constructor( 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 56f552961432..d95c38e2697c 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 class StartKeyguardTransitionModule { 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 f5bcab96a5a4..92612b824974 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 @@ enum class KeyguardState { * 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 @@ enum class KeyguardState { 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/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt index 4c33d905b785..7c1368af652c 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 android.widget.TextView 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 @@ object KeyguardIndicationAreaBinder { fun bind( view: ViewGroup, viewModel: KeyguardIndicationAreaViewModel, - keyguardRootViewModel: KeyguardRootViewModel, + aodAlphaViewModel: AodAlphaViewModel, indicationController: KeyguardIndicationController, ): DisposableHandle { indicationController.setIndicationArea(view) @@ -69,7 +69,7 @@ object KeyguardIndicationAreaBinder { 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 fad0370a85d7..2aebd99e3664 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.Text 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.DisposableHandle 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 @@ object KeyguardRootViewBinder { view: ViewGroup, viewModel: KeyguardRootViewModel, configuration: ConfigurationState, - featureFlags: FeatureFlagsClassic, occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel, chipbarCoordinator: ChipbarCoordinator, screenOffAnimationController: ScreenOffAnimationController, @@ -108,6 +110,8 @@ object KeyguardRootViewBinder { } } + val burnInParams = MutableStateFlow(BurnInParameters()) + val disposableHandle = view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { @@ -164,35 +168,41 @@ object KeyguardRootViewBinder { // 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 - } + burnInParams + .flatMapLatest { params -> viewModel.translationY(params) } + .collect { y -> + childViews[burnInLayerId]?.translationY = y + childViews[largeClockId]?.translationY = y + } } launch { - viewModel.translationX.collect { x -> - childViews[burnInLayerId]?.translationX = x - childViews[largeClockId]?.translationX = x - } + burnInParams + .flatMapLatest { params -> viewModel.translationX(params) } + .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 + 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 } - } else { - // For weather clock, large clock should have only scale - // transition with other parts in burnInLayer - childViews[burnInLayerId]?.scaleX = scale - childViews[burnInLayerId]?.scaleY = scale } - } } if (NotificationIconContainerRefactor.isEnabled) { @@ -274,10 +284,12 @@ object KeyguardRootViewBinder { } 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 @@ object KeyguardRootViewBinder { 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 @@ object KeyguardRootViewBinder { ) } - 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 @@ object KeyguardRootViewBinder { } 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 03e45fdbe75f..eb3afb7c9eec 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 @@ constructor( 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 66c137f7d75e..ea05c1d878b8 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.Flags.keyguardBottomAreaRefactor 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 @@ class DefaultIndicationAreaSection 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 @@ constructor( 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 000000000000..d4ea728bbffb --- /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 000000000000..780e323a96bc --- /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/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 26dace00ad76..5059e6be9080 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.AnimatedValue 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 @@ constructor( 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 539db7fb1ae3..2b28a71b4a3d 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 @@ constructor( 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/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index ab69acbc6e9d..3be60b74af21 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 @@ constructor( // 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 @@ constructor( 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 286037ef1961..fb6bc38c9a0b 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 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump 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 000000000000..fc3456ad6a23 --- /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/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java index b048da492eb1..942d186e7005 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 @@ public interface StatusBarPhoneModule { @Binds @SysUISingleton CentralSurfaces bindsCentralSurfaces(CentralSurfacesImpl impl); - - /** - * Starts {@link ConfigurationControllerStartable} - */ - @Binds - @IntoMap - @ClassKey(ConfigurationControllerStartable.class) - CoreStartable bindConfigControllerStartable(ConfigurationControllerStartable impl); } 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 9fe32f1e378b..b45c8948e763 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 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { @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 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { onUnfoldOverlayReady() onFoldAodReady() - waitHandlerIdle(testHandler) + waitHandlerIdle() // Should be called when both unfold overlay and keyguard drawn ready verify(runnable).run() @@ -90,7 +98,7 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { onUnfoldOverlayReady() onFoldAodReady() - waitHandlerIdle(testHandler) + waitHandlerIdle() // Should be called when both unfold overlay and keyguard drawn ready verify(runnable).run() @@ -104,7 +112,8 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { 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 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { @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 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { 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/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt index ab6bc2ca2dda..66fdf538e284 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 @@ package com.android.systemui.haptics.slider 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 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { 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 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { 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 8dd33d5e60bb..1205dceb49e9 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 android.view.ViewGroup 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 @@ import org.mockito.MockitoAnnotations @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 @@ class DefaultIndicationAreaSectionTest : SysuiTestCase() { DefaultIndicationAreaSection( context, keyguardIndicationAreaViewModel, - keyguardRootViewModel, + aodAlphaViewModel, indicationController, ) } 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 ee1be10607cf..000000000000 --- 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/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 b0d941dc6c24..a9d89a37c542 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.Kosmos.Fixture 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 000000000000..a3955f7634eb --- /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 000000000000..6b89e0f8901a --- /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 000000000000..35cfa89e56ed --- /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 14e2cff6a7a5..00ece1482236 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 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/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index 13ee74738437..933f50c36b7b 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 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi 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 5bbde2b1c419..93ecb7968ee2 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 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/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt index 843cc3b78031..54d805409c51 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 @@ class UnfoldRemoteFilter( 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 000000000000..5e001fba6aa1 --- /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 a41d0e57cd21..000000000000 --- 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/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java index 69647633eaff..52a8f9ed10d4 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.NonNull; 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; @@ -398,7 +399,11 @@ class AssociationRequestsProcessor { 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/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java index bd646fa6bfbc..4e471f5b0bc9 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 static com.android.server.companion.Utils.prepareForIpc; 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 @@ public class SystemDataTransferProcessor { 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 8ad60e6a0782..72e62c37106d 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 @@ final class ActivityManagerConstants extends ContentObserver { /** * 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 848a2b004f25..57c52c2cf408 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 @@ final class ActivityManagerShellCommand extends ShellCommand { 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 @@ final class ActivityManagerShellCommand extends ShellCommand { 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 @@ final class ActivityManagerShellCommand extends ShellCommand { 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 575db01931e6..e90910a13b3b 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/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index 56a94ec06ad4..49f607095b90 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 @@ public class ClipboardService extends SystemService { 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/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index c4d94ee93235..4f998ee2cfa2 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -276,7 +276,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 +316,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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(); @@ -4812,7 +4812,20 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 efa1e0d66f35..6ed4848c20b4 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 @@ package com.android.server.inputmethod; 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 @@ final class InputMethodMenuController { 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 @@ final class InputMethodMenuController { 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 List<ImeSubtypeListItem> imList = mSwitchingController - .getSortedInputMethodAndSubtypeListForImeMenuLocked( - showAuxSubtypes, isScreenLocked); - if (imList.isEmpty()) { - return; - } + final int userId = mService.getCurrentImeUserIdLocked(); - hideInputMethodMenuLocked(); + if (imList.isEmpty()) { + return; + } - 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()); - } + 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()); } + } - 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; - } + // 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) -> { - 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); + 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; } - hideInputMethodMenuLocked(); + mService.setInputMethodLocked(im.getId(), subtypeId); } - }; - 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(); - - } + 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 547fd2f17980..a0b55edddec7 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 @@ final class InputMethodUtils { 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 5ef89ad4269a..a5939e924adb 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.Arrays; 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 @@ public class GnssConfiguration { } 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 @@ public class GnssConfiguration { 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 @@ public class GnssConfiguration { } } + 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 ae889d8255c6..21e7befc1b89 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 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider } 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 1aa8601bdcf9..9e9802354a4d 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 2305d6c9fba9..75b453184db8 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 @@ public class UserManagerService extends IUserManager.Stub { 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/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 77b4a74ab109..1577cef9de00 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 @@ final class AccessibilityController { } } - 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 @@ final class AccessibilityController { .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 @@ final class AccessibilityController { } 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 @@ final class AccessibilityController { return mMagnificationSpec; } - void drawWindowIfNeeded() { + void drawWindowIfNeeded(SurfaceControl.Transaction t) { recomputeBounds(); - mWindow.postDrawIfNeeded(); + mWindow.drawIfNeeded(t); } void destroyWindow() { @@ -1158,7 +1158,7 @@ final class AccessibilityController { 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 @@ final class AccessibilityController { 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 @@ final class AccessibilityController { InputMonitor.setTrustedOverlayInputInfo(mSurfaceControl, t, mDisplayContent.getDisplayId(), "Magnification Overlay"); t.apply(); - mTransaction = t; mSurface = mBlastBufferQueue.createSurface(); mAnimationController = new AnimationController(context, @@ -1223,11 +1219,10 @@ final class AccessibilityController { 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 @@ final class AccessibilityController { Slog.i(LOG_TAG, "ViewportWindow shown: " + mShown); } } - return true; } @SuppressWarnings("unused") @@ -1291,22 +1285,7 @@ final class AccessibilityController { 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 class AccessibilityController { } } - final boolean showSurface; // Draw without holding WindowManagerGlobalLock. if (alpha > 0) { Canvas canvas = null; @@ -1351,17 +1329,9 @@ final class AccessibilityController { 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 a43e7d533240..b8a92bbb059b 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A 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 class ActivityRecord extends WindowToken implements WindowManagerService.A final DisplayContent displayContent = getDisplayContent(); displayContent.mOpeningApps.remove(this); displayContent.mClosingApps.remove(this); - waitingToShow = false; setVisibleRequested(visible); mLastDeferHidingClient = deferHidingClient; @@ -5411,25 +5416,16 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @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 d90d017a5570..13f71521c240 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 @@ class ActivityStarter { } } - 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 @@ class ActivityStarter { } 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/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 05087f8a6edf..939babc4df41 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 @@ public class AppTransitionController { 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 d556f095ae50..3b0634311501 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 @@ class Task extends TaskFragment { 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 d425bdf5613f..93cce2aa3fd0 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 @@ class TaskFragment extends WindowContainer<WindowContainer> { 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 @@ class TaskFragment extends WindowContainer<WindowContainer> { @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 @@ class TaskFragment extends WindowContainer<WindowContainer> { /** 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 @@ class TaskFragment extends WindowContainer<WindowContainer> { 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 b12855e2bb49..56bef3335b8b 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 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { 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 c4e1d6e51e70..750fd509e50f 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 @@ public class WindowAnimator { 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 9e4a31c3773a..59d0210251d1 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_REQUEST_FOCUS_ON_TASK 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.ActivityTaskManagerService.enforceTaskPermis 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 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub 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 315c00f7fb8c..0b43be700b0d 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 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP * 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 7d21dbf85a66..5048cef3da1b 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.WindowManagerDebugConfig.TAG_WM; 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 @@ class WindowToken extends WindowContainer<WindowState> { // 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 @@ class WindowToken extends WindowContainer<WindowState> { 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 @@ class WindowToken extends WindowContainer<WindowState> { 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/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index a490013303e9..f288103bd954 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { ? 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { ? 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { .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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { 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 000000000000..dfdb0c7241c4 --- /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/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java index 89a49615dbe1..59c94dc1f250 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java @@ -65,6 +65,7 @@ import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvide import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintResetLockoutClient; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; @@ -247,6 +248,7 @@ public class HidlToAidlSensorAdapterTest { } @Test + @Ignore("b/317403648") public void lockoutPermanentResetViaClient() { setLockoutPermanent(); 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 6dd91718f70d..a2f8c8bbe13e 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 @@ public class InputMethodUtilsTest { 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 @@ public class InputMethodUtilsTest { 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 835ccf0b19f6..6fffd7533df8 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 @@ public class BatterySaverPolicyTest extends AndroidTestCase { testServiceDefaultValue_On(ServiceType.NULL); } - @Suppress + @Suppress // TODO: b/317823111 - Remove once test fixed. @SmallTest public void testGetBatterySaverPolicy_PolicyVibration_DefaultValueCorrect() { testDefaultValue( @@ -219,7 +219,7 @@ public class BatterySaverPolicyTest extends AndroidTestCase { 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 @@ public class BatterySaverPolicyTest extends AndroidTestCase { } } + @Suppress // TODO: b/317823111 - Remove once test fixed. public void testSetPolicyLevel_Adaptive() { mBatterySaverPolicy.setPolicyLevel(POLICY_LEVEL_ADAPTIVE); 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 53635835f164..29467f259ac3 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.spyOn; 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 com.android.server.wm.ActivityTaskManagerInternal; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; /** @@ -50,6 +60,9 @@ import org.junit.Test; @SmallTest public class PhoneWindowManagerTests { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + PhoneWindowManager mPhoneWindowManager; @Before @@ -85,6 +98,36 @@ public class PhoneWindowManagerTests { 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/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java index 810cbe8f8080..0f1e4d1e928f 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 @@ public class SyncEngineTests extends WindowTestsBase { } @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 d36ee2c62fd2..a88285ac4c8f 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_REORDER_TO_TOP_OF_TAS 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 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { .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 875e708ce1da..e9fe4bb91329 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 @@ public class TaskFragmentTest extends WindowTestsBase { // 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); |