diff options
243 files changed, 5801 insertions, 1280 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index dd919cacc534..a0f38d9a7bd9 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -680,6 +680,11 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +cc_aconfig_library { + name: "com.android.media.flags.editing-aconfig-cc", + aconfig_declarations: "com.android.media.flags.editing-aconfig", +} + // MediaProjection aconfig_declarations { name: "com.android.media.flags.projection-aconfig", diff --git a/Android.bp b/Android.bp index 5b9f2cbf2d0d..8c0158549d2d 100644 --- a/Android.bp +++ b/Android.bp @@ -99,6 +99,7 @@ filegroup { ":android.hardware.biometrics.common-V4-java-source", ":android.hardware.biometrics.fingerprint-V5-java-source", ":android.hardware.biometrics.fingerprint.virtualhal-java-source", + ":android.hardware.biometrics.face.virtualhal-java-source", ":android.hardware.biometrics.face-V4-java-source", ":android.hardware.gnss-V2-java-source", ":android.hardware.graphics.common-V3-java-source", diff --git a/core/api/current.txt b/core/api/current.txt index d0e20031c74d..f817241d80da 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -6495,6 +6495,7 @@ package android.app { field public static final int FLAG_NO_CLEAR = 32; // 0x20 field public static final int FLAG_ONGOING_EVENT = 2; // 0x2 field public static final int FLAG_ONLY_ALERT_ONCE = 8; // 0x8 + field @FlaggedApi("android.app.api_rich_ongoing") public static final int FLAG_PROMOTED_ONGOING = 262144; // 0x40000 field @Deprecated public static final int FLAG_SHOW_LIGHTS = 1; // 0x1 field public static final int FOREGROUND_SERVICE_DEFAULT = 0; // 0x0 field public static final int FOREGROUND_SERVICE_DEFERRED = 2; // 0x2 @@ -36934,14 +36935,16 @@ package android.provider { @FlaggedApi("android.provider.new_default_account_api_enabled") public static final class ContactsContract.RawContacts.DefaultAccount.DefaultAccountAndState { ctor public ContactsContract.RawContacts.DefaultAccount.DefaultAccountAndState(int, @Nullable android.accounts.Account); - method @Nullable public android.accounts.Account getCloudAccount(); + method @Nullable public android.accounts.Account getAccount(); method public int getState(); method @NonNull public static android.provider.ContactsContract.RawContacts.DefaultAccount.DefaultAccountAndState ofCloud(@NonNull android.accounts.Account); method @NonNull public static android.provider.ContactsContract.RawContacts.DefaultAccount.DefaultAccountAndState ofLocal(); method @NonNull public static android.provider.ContactsContract.RawContacts.DefaultAccount.DefaultAccountAndState ofNotSet(); + method @NonNull public static android.provider.ContactsContract.RawContacts.DefaultAccount.DefaultAccountAndState ofSim(@NonNull android.accounts.Account); field public static final int DEFAULT_ACCOUNT_STATE_CLOUD = 3; // 0x3 field public static final int DEFAULT_ACCOUNT_STATE_LOCAL = 2; // 0x2 field public static final int DEFAULT_ACCOUNT_STATE_NOT_SET = 1; // 0x1 + field public static final int DEFAULT_ACCOUNT_STATE_SIM = 4; // 0x4 } public static final class ContactsContract.RawContacts.DisplayPhoto { diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index e2bee64cbb7b..b9fe356690e0 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -258,4 +258,6 @@ interface INotificationManager @EnforcePermission(allOf={"INTERACT_ACROSS_USERS", "ACCESS_NOTIFICATIONS"}) void unregisterCallNotificationEventListener(String packageName, in UserHandle userHandle, in ICallNotificationEventCallback listener); + void setCanBePromoted(String pkg, int uid, boolean promote); + boolean canBePromoted(String pkg, int uid); } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 81d2c890ee31..4d73c354707d 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -772,6 +772,17 @@ public class Notification implements Parcelable @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) public static final int FLAG_SILENT = 1 << 17; //0x00020000 + /** + * Bit to be bitwise-ored into the {@link #flags} field that should be + * set by the system if this notification is a promoted ongoing notification, either via a + * user setting or allowlist. + * + * Applications cannot set this flag directly, but the posting app and + * {@link android.service.notification.NotificationListenerService} can read it. + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final int FLAG_PROMOTED_ONGOING = 0x00040000; + private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList( BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class, DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class, @@ -3110,6 +3121,53 @@ public class Notification implements Parcelable } /** + * @hide + */ + @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) + public boolean containsCustomViews() { + return contentView != null + || bigContentView != null + || headsUpContentView != null + || (publicVersion != null + && (publicVersion.contentView != null + || publicVersion.bigContentView != null + || publicVersion.headsUpContentView != null)); + } + + /** + * @hide + */ + @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) + public boolean hasTitle() { + return extras != null + && (!TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE)) + || !TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE_BIG))); + } + + /** + * @hide + */ + @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) + public boolean hasPromotableStyle() { + //TODO(b/367739672): Add progress style + return extras == null || !extras.containsKey(Notification.EXTRA_TEMPLATE) + || isStyle(Notification.BigPictureStyle.class) + || isStyle(Notification.BigTextStyle.class) + || isStyle(Notification.CallStyle.class); + } + + /** + * @hide + */ + @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) + public boolean hasPromotableCharacteristics() { + return isColorized() + && hasTitle() + && !containsCustomViews() + && hasPromotableStyle(); + } + + /** * Whether this notification was posted by a headless system app. * * If we don't have enough information to figure this out, this will return false. Therefore, @@ -7636,7 +7694,6 @@ public class Notification implements Parcelable if (mLargeIcon != null || largeIcon != null) { Resources resources = context.getResources(); - Class<? extends Style> style = getNotificationStyle(); int maxSize = resources.getDimensionPixelSize(isLowRam ? R.dimen.notification_right_icon_size_low_ram : R.dimen.notification_right_icon_size); diff --git a/core/java/android/hardware/face/FaceSensorConfigurations.java b/core/java/android/hardware/face/FaceSensorConfigurations.java index 12471681f913..51c5f4c398a1 100644 --- a/core/java/android/hardware/face/FaceSensorConfigurations.java +++ b/core/java/android/hardware/face/FaceSensorConfigurations.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.content.Context; import android.hardware.biometrics.face.IFace; import android.hardware.biometrics.face.SensorProps; +import android.hardware.biometrics.face.virtualhal.IVirtualHal; import android.os.Binder; import android.os.Parcel; import android.os.Parcelable; @@ -160,6 +161,41 @@ public class FaceSensorConfigurations implements Parcelable { dest.writeByte((byte) (mResetLockoutRequiresChallenge ? 1 : 0)); dest.writeMap(mSensorPropsMap); } + /** + * Remap fqName of VHAL because the `virtual` instance is registered + * with IVirtulalHal now (IFace previously) + * @param fqName fqName to be translated + * @return real fqName + */ + public static String remapFqName(String fqName) { + if (!fqName.contains(IFace.DESCRIPTOR + "/virtual")) { + return fqName; //no remap needed for real hardware HAL + } else { + //new Vhal instance name + return fqName.replace("IFace", "virtualhal.IVirtualHal"); + } + } + /** + * @param fqName aidl interface instance name + * @return aidl interface + */ + public static IFace getIFace(String fqName) { + if (fqName.contains("virtual")) { + String fqNameMapped = remapFqName(fqName); + Slog.i(TAG, "getIFace fqName is mapped: " + fqName + "->" + fqNameMapped); + try { + IVirtualHal vhal = IVirtualHal.Stub.asInterface( + Binder.allowBlocking(ServiceManager.waitForService(fqNameMapped))); + return vhal.getFaceHal(); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception in vhal.getFaceHal() call" + fqNameMapped); + } + } + + return IFace.Stub.asInterface( + Binder.allowBlocking(ServiceManager.waitForDeclaredService(fqName))); + } + /** * Returns face sensor props for the HAL {@param instance}. @@ -173,14 +209,13 @@ public class FaceSensorConfigurations implements Parcelable { return props; } - final String fqName = IFace.DESCRIPTOR + "/" + instance; - IFace face = IFace.Stub.asInterface(Binder.allowBlocking( - ServiceManager.waitForDeclaredService(fqName))); try { - if (face != null) { - props = face.getSensorProps(); + final String fqName = IFace.DESCRIPTOR + "/" + instance; + final IFace fp = getIFace(fqName); + if (fp != null) { + props = fp.getSensorProps(); } else { - Slog.e(TAG, "Unable to get declared service: " + fqName); + Log.d(TAG, "IFace null for instance " + instance); } } catch (RemoteException e) { Log.d(TAG, "Unable to get sensor properties!"); diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index a62281049678..27b1dfbd9b18 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -3046,6 +3046,11 @@ public final class ContactsContract { * <li> {@link #DEFAULT_ACCOUNT_STATE_CLOUD}: The default account is set to a * cloud-synced account. New raw contacts requested for insertion without a specified * {@link Account} will be saved in the default cloud account. </li> + * <li> {@link #DEFAULT_ACCOUNT_STATE_SIM}: The default account is set to a + * account that is associated with one of + * {@link SimContacts#getSimAccounts(ContentResolver)}. New raw contacts requested + * for insertion without a specified {@link Account} will be + * saved in this SIM account. </li> * </ul> */ @FlaggedApi(Flags.FLAG_NEW_DEFAULT_ACCOUNT_API_ENABLED) @@ -3063,44 +3068,51 @@ public final class ContactsContract { public static final int DEFAULT_ACCOUNT_STATE_CLOUD = 3; /** + * A state indicating that the default account is set as an account that is + * associated with one of {@link SimContacts#getSimAccounts(ContentResolver)}. + */ + public static final int DEFAULT_ACCOUNT_STATE_SIM = 4; + + /** * The state of the default account. One of * {@link #DEFAULT_ACCOUNT_STATE_NOT_SET}, - * {@link #DEFAULT_ACCOUNT_STATE_LOCAL} or - * {@link #DEFAULT_ACCOUNT_STATE_CLOUD}. + * {@link #DEFAULT_ACCOUNT_STATE_LOCAL}, + * {@link #DEFAULT_ACCOUNT_STATE_CLOUD} + * {@link #DEFAULT_ACCOUNT_STATE_SIM}. */ @DefaultAccountState private final int mState; /** - * The account of the default account, when {@link mState} is { + * The account of the default account, when {@link #mState} is { * - * @link #STATE_SET_TO_CLOUD}, or null otherwise. + * @link #DEFAULT_ACCOUNT_STATE_CLOUD} or {@link #DEFAULT_ACCOUNT_STATE_SIM}, or + * null otherwise. */ - private final Account mCloudAccount; + private final Account mAccount; /** * Constructs a new `DefaultAccountAndState` instance with the specified state and * cloud * account. * - * @param state The state of the default account. - * @param cloudAccount The cloud account associated with the default account, - * or null if the state is not - * {@link #DEFAULT_ACCOUNT_STATE_CLOUD}. + * @param state The state of the default account. + * @param account The account associated with the default account if the state is + * {@link #DEFAULT_ACCOUNT_STATE_CLOUD} or + * {@link #DEFAULT_ACCOUNT_STATE_SIM}, or null otherwise. */ public DefaultAccountAndState(@DefaultAccountState int state, - @Nullable Account cloudAccount) { + @Nullable Account account) { if (!isValidDefaultAccountState(state)) { throw new IllegalArgumentException("Invalid default account state."); } - if ((state == DEFAULT_ACCOUNT_STATE_CLOUD) != (cloudAccount != null)) { + if (isCloudOrSimAccount(state) != (account != null)) { throw new IllegalArgumentException( - "Default account can be set to cloud if and only if the cloud " + "Default account can be set to cloud or SIM if and only if the " + "account is provided."); } this.mState = state; - this.mCloudAccount = - (mState == DEFAULT_ACCOUNT_STATE_CLOUD) ? cloudAccount : null; + this.mAccount = isCloudOrSimAccount(state) ? account : null; } /** @@ -3118,6 +3130,21 @@ public final class ContactsContract { return new DefaultAccountAndState(DEFAULT_ACCOUNT_STATE_CLOUD, cloudAccount); } + + /** + * Creates a `DefaultAccountAndState` instance representing a default account + * that is set to the sim and associated with the specified sim account. + * + * @param simAccount The non-null sim account associated with the default + * contacts account. + * @return A new `DefaultAccountAndState` instance with state + * {@link #DEFAULT_ACCOUNT_STATE_SIM}. + */ + public static @NonNull DefaultAccountAndState ofSim( + @NonNull Account simAccount) { + return new DefaultAccountAndState(DEFAULT_ACCOUNT_STATE_SIM, simAccount); + } + /** * Creates a `DefaultAccountAndState` instance representing a default account * that is set to the local device storage. @@ -3140,6 +3167,18 @@ public final class ContactsContract { return new DefaultAccountAndState(DEFAULT_ACCOUNT_STATE_NOT_SET, null); } + private static boolean isCloudOrSimAccount(@DefaultAccountState int state) { + return state == DEFAULT_ACCOUNT_STATE_CLOUD + || state == DEFAULT_ACCOUNT_STATE_SIM; + } + + private static boolean isValidDefaultAccountState(int state) { + return state == DEFAULT_ACCOUNT_STATE_NOT_SET + || state == DEFAULT_ACCOUNT_STATE_LOCAL + || state == DEFAULT_ACCOUNT_STATE_CLOUD + || state == DEFAULT_ACCOUNT_STATE_SIM; + } + /** * @return the state of the default account. */ @@ -3149,16 +3188,17 @@ public final class ContactsContract { } /** - * @return the cloud account associated with the default account, or null if the - * state is not {@link #DEFAULT_ACCOUNT_STATE_CLOUD}. + * @return the cloud account associated with the default account if the + * state is {@link #DEFAULT_ACCOUNT_STATE_CLOUD} or + * {@link #DEFAULT_ACCOUNT_STATE_SIM}. */ - public @Nullable Account getCloudAccount() { - return mCloudAccount; + public @Nullable Account getAccount() { + return mAccount; } @Override public int hashCode() { - return Objects.hash(mState, mCloudAccount); + return Objects.hash(mState, mAccount); } @Override @@ -3170,14 +3210,8 @@ public final class ContactsContract { return false; } - return mState == that.mState && Objects.equals(mCloudAccount, - that.mCloudAccount); - } - - private static boolean isValidDefaultAccountState(int state) { - return state == DEFAULT_ACCOUNT_STATE_NOT_SET - || state == DEFAULT_ACCOUNT_STATE_LOCAL - || state == DEFAULT_ACCOUNT_STATE_CLOUD; + return mState == that.mState && Objects.equals(mAccount, + that.mAccount); } /** @@ -3189,7 +3223,8 @@ public final class ContactsContract { @IntDef( prefix = {"DEFAULT_ACCOUNT_STATE_"}, value = {DEFAULT_ACCOUNT_STATE_NOT_SET, - DEFAULT_ACCOUNT_STATE_LOCAL, DEFAULT_ACCOUNT_STATE_CLOUD}) + DEFAULT_ACCOUNT_STATE_LOCAL, DEFAULT_ACCOUNT_STATE_CLOUD, + DEFAULT_ACCOUNT_STATE_SIM}) public @interface DefaultAccountState { } } diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index d45b24ed69be..303197dfd82d 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -202,10 +202,8 @@ public class ZenModeConfig implements Parcelable { private static final int DEFAULT_CALLS_SOURCE = SOURCE_STAR; public static final String MANUAL_RULE_ID = "MANUAL_RULE"; - public static final String EVENTS_DEFAULT_RULE_ID = "EVENTS_DEFAULT_RULE"; + public static final String EVENTS_OBSOLETE_RULE_ID = "EVENTS_DEFAULT_RULE"; public static final String EVERY_NIGHT_DEFAULT_RULE_ID = "EVERY_NIGHT_DEFAULT_RULE"; - public static final List<String> DEFAULT_RULE_IDS = Arrays.asList(EVERY_NIGHT_DEFAULT_RULE_ID, - EVENTS_DEFAULT_RULE_ID); public static final int[] ALL_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY }; @@ -424,21 +422,10 @@ public class ZenModeConfig implements Parcelable { return policy; } + @FlaggedApi(Flags.FLAG_MODES_UI) public static ZenModeConfig getDefaultConfig() { ZenModeConfig config = new ZenModeConfig(); - EventInfo eventInfo = new EventInfo(); - eventInfo.reply = REPLY_YES_OR_MAYBE; - ZenRule events = new ZenRule(); - events.id = EVENTS_DEFAULT_RULE_ID; - events.conditionId = toEventConditionId(eventInfo); - events.component = ComponentName.unflattenFromString( - "android/com.android.server.notification.EventConditionProvider"); - events.enabled = false; - events.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - events.pkg = "android"; - config.automaticRules.put(EVENTS_DEFAULT_RULE_ID, events); - ScheduleInfo scheduleInfo = new ScheduleInfo(); scheduleInfo.days = new int[] {1, 2, 3, 4, 5, 6, 7}; scheduleInfo.startHour = 22; @@ -457,6 +444,13 @@ public class ZenModeConfig implements Parcelable { return config; } + // TODO: b/368247671 - Can be made a constant again when modes_ui is inlined + public static List<String> getDefaultRuleIds() { + return Flags.modesUi() + ? List.of(EVERY_NIGHT_DEFAULT_RULE_ID) + : List.of(EVERY_NIGHT_DEFAULT_RULE_ID, EVENTS_OBSOLETE_RULE_ID); + } + void ensureManualZenRule() { if (manualRule == null) { final ZenRule newRule = new ZenRule(); diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 384add5cf929..2ab16e91d987 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -2397,7 +2397,11 @@ public abstract class WallpaperService extends Service { // it hasn't changed and there is no need to update. ret = mBlastBufferQueue.createSurface(); } else { - mBlastBufferQueue.update(mBbqSurfaceControl, width, height, format); + if (mBbqSurfaceControl != null && mBbqSurfaceControl.isValid()) { + mBlastBufferQueue.update(mBbqSurfaceControl, width, height, format); + } else { + Log.w(TAG, "Skipping BlastBufferQueue update - invalid surface control"); + } } return ret; diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java index e90b1c0fc167..229e8ee75844 100644 --- a/core/java/android/view/ImeInsetsSourceConsumer.java +++ b/core/java/android/view/ImeInsetsSourceConsumer.java @@ -26,7 +26,6 @@ import android.annotation.Nullable; import android.os.IBinder; import android.os.Trace; import android.util.proto.ProtoOutputStream; -import android.view.SurfaceControl.Transaction; import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import android.view.inputmethod.InputMethodManager; @@ -34,8 +33,6 @@ import android.view.inputmethod.InputMethodManager; import com.android.internal.inputmethod.ImeTracing; import com.android.internal.inputmethod.SoftInputShowHideReason; -import java.util.function.Supplier; - /** * Controls the visibility and animations of IME window insets source. * @hide @@ -54,10 +51,8 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { */ private boolean mIsRequestedVisibleAwaitingLeash; - public ImeInsetsSourceConsumer( - int id, InsetsState state, Supplier<Transaction> transactionSupplier, - InsetsController controller) { - super(id, WindowInsets.Type.ime(), state, transactionSupplier, controller); + public ImeInsetsSourceConsumer(int id, InsetsState state, InsetsController controller) { + super(id, WindowInsets.Type.ime(), state, controller); } @Override diff --git a/core/java/android/view/InsetsAnimationControlCallbacks.java b/core/java/android/view/InsetsAnimationControlCallbacks.java index 04bb6091672b..a0d8a173c3e5 100644 --- a/core/java/android/view/InsetsAnimationControlCallbacks.java +++ b/core/java/android/view/InsetsAnimationControlCallbacks.java @@ -54,13 +54,6 @@ public interface InsetsAnimationControlCallbacks { void notifyFinished(InsetsAnimationControlRunner runner, boolean shown); /** - * Apply the new params to the surface. - * @param params The {@link android.view.SyncRtSurfaceTransactionApplier.SurfaceParams} to - * apply. - */ - void applySurfaceParams(SyncRtSurfaceTransactionApplier.SurfaceParams... params); - - /** * Post a message to release the Surface, guaranteed to happen after all * previous calls to applySurfaceParams. */ diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java index 91e9230cdc6a..97facc1ba472 100644 --- a/core/java/android/view/InsetsAnimationControlImpl.java +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -99,6 +99,7 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro private final @InsetsType int mTypes; private @InsetsType int mControllingTypes; private final InsetsAnimationControlCallbacks mController; + private final SurfaceParamsApplier mSurfaceParamsApplier; private final WindowInsetsAnimation mAnimation; private final long mDurationMs; private final Interpolator mInterpolator; @@ -123,6 +124,7 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro public InsetsAnimationControlImpl(SparseArray<InsetsSourceControl> controls, @Nullable Rect frame, InsetsState state, WindowInsetsAnimationControlListener listener, @InsetsType int types, InsetsAnimationControlCallbacks controller, + SurfaceParamsApplier surfaceParamsApplier, InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType, @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation, CompatibilityInfo.Translator translator, @Nullable ImeTracker.Token statsToken) { @@ -131,6 +133,7 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro mTypes = types; mControllingTypes = types; mController = controller; + mSurfaceParamsApplier = surfaceParamsApplier; mInitialInsetsState = new InsetsState(state, true /* copySources */); if (frame != null) { final SparseIntArray idSideMap = new SparseIntArray(); @@ -258,6 +261,11 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro } @Override + public SurfaceParamsApplier getSurfaceParamsApplier() { + return mSurfaceParamsApplier; + } + + @Override @Nullable public ImeTracker.Token getStatsToken() { return mStatsToken; @@ -305,7 +313,7 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro updateLeashesForSide(SIDE_RIGHT, offset.right, params, outState, mPendingAlpha); updateLeashesForSide(SIDE_BOTTOM, offset.bottom, params, outState, mPendingAlpha); - mController.applySurfaceParams(params.toArray(new SurfaceParams[params.size()])); + mSurfaceParamsApplier.applySurfaceParams(params.toArray(new SurfaceParams[params.size()])); mCurrentInsets = mPendingInsets; mAnimation.setFraction(mPendingFraction); mCurrentAlpha = mPendingAlpha; diff --git a/core/java/android/view/InsetsAnimationControlRunner.java b/core/java/android/view/InsetsAnimationControlRunner.java index 8cb8b47dd0ec..4f102da4692a 100644 --- a/core/java/android/view/InsetsAnimationControlRunner.java +++ b/core/java/android/view/InsetsAnimationControlRunner.java @@ -77,6 +77,11 @@ public interface InsetsAnimationControlRunner { @AnimationType int getAnimationType(); /** + * @return The {@link SurfaceParamsApplier} this runner is using. + */ + SurfaceParamsApplier getSurfaceParamsApplier(); + + /** * @return The token tracking the current IME request or {@code null} otherwise. */ @Nullable @@ -99,4 +104,27 @@ public interface InsetsAnimationControlRunner { * @param fieldId FieldId of the implementation class */ void dumpDebug(ProtoOutputStream proto, long fieldId); + + /** + * Interface applying given surface operations. + */ + interface SurfaceParamsApplier { + + SurfaceParamsApplier DEFAULT = params -> { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + for (int i = params.length - 1; i >= 0; i--) { + SyncRtSurfaceTransactionApplier.applyParams(t, params[i], new float[9]); + } + t.apply(); + t.close(); + }; + + /** + * Apply the new params to the surface. + * + * @param params The {@link SyncRtSurfaceTransactionApplier.SurfaceParams} to apply. + */ + void applySurfaceParams(SyncRtSurfaceTransactionApplier.SurfaceParams... params); + + } } diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java index fc185bc73735..8c2c4951a9f7 100644 --- a/core/java/android/view/InsetsAnimationThreadControlRunner.java +++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java @@ -17,7 +17,6 @@ package android.view; import static android.view.InsetsController.DEBUG; -import static android.view.SyncRtSurfaceTransactionApplier.applyParams; import android.annotation.Nullable; import android.annotation.UiThread; @@ -30,7 +29,6 @@ import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import android.view.InsetsController.AnimationType; import android.view.InsetsController.LayoutInsetsDuringAnimation; -import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsAnimation.Bounds; import android.view.inputmethod.ImeTracker; @@ -50,8 +48,6 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro private final InsetsAnimationControlCallbacks mCallbacks = new InsetsAnimationControlCallbacks() { - private final float[] mTmpFloat9 = new float[9]; - @Override @UiThread public <T extends InsetsAnimationControlRunner & InternalInsetsAnimationController> @@ -81,19 +77,6 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro } @Override - public void applySurfaceParams(SurfaceParams... params) { - if (DEBUG) Log.d(TAG, "applySurfaceParams"); - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - for (int i = params.length - 1; i >= 0; i--) { - SyncRtSurfaceTransactionApplier.SurfaceParams surfaceParams = params[i]; - applyParams(t, surfaceParams, mTmpFloat9); - } - t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); - t.apply(); - t.close(); - } - - @Override public void releaseSurfaceControlFromRt(SurfaceControl sc) { if (DEBUG) Log.d(TAG, "releaseSurfaceControlFromRt"); // Since we don't push the SurfaceParams to the RT we can release directly @@ -106,6 +89,22 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro } }; + private SurfaceParamsApplier mSurfaceParamsApplier = new SurfaceParamsApplier() { + + private final float[] mTmpFloat9 = new float[9]; + + @Override + public void applySurfaceParams(SyncRtSurfaceTransactionApplier.SurfaceParams... params) { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + for (int i = params.length - 1; i >= 0; i--) { + SyncRtSurfaceTransactionApplier.applyParams(t, params[i], mTmpFloat9); + } + t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); + t.apply(); + t.close(); + } + }; + @UiThread public InsetsAnimationThreadControlRunner(SparseArray<InsetsSourceControl> controls, @Nullable Rect frame, InsetsState state, WindowInsetsAnimationControlListener listener, @@ -117,8 +116,8 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro mMainThreadHandler = mainThreadHandler; mOuterCallbacks = controller; mControl = new InsetsAnimationControlImpl(controls, frame, state, listener, types, - mCallbacks, insetsAnimationSpec, animationType, layoutInsetsDuringAnimation, - translator, statsToken); + mCallbacks, mSurfaceParamsApplier, insetsAnimationSpec, animationType, + layoutInsetsDuringAnimation, translator, statsToken); InsetsAnimationThread.getHandler().post(() -> { if (mControl.isCancelled()) { return; @@ -187,6 +186,11 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro } @Override + public SurfaceParamsApplier getSurfaceParamsApplier() { + return mSurfaceParamsApplier; + } + + @Override public void updateLayoutInsetsDuringAnimation( @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation) { InsetsAnimationThread.getHandler().post( diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 8fdf91a2d87c..e38281ffd020 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -55,7 +55,6 @@ import android.util.Pair; import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import android.view.InsetsSourceConsumer.ShowResult; -import android.view.SurfaceControl.Transaction; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsAnimation.Bounds; @@ -85,7 +84,8 @@ import java.util.Objects; * Implements {@link WindowInsetsController} on the client. * @hide */ -public class InsetsController implements WindowInsetsController, InsetsAnimationControlCallbacks { +public class InsetsController implements WindowInsetsController, InsetsAnimationControlCallbacks, + InsetsAnimationControlRunner.SurfaceParamsApplier { private int mTypesBeingCancelled; @@ -307,7 +307,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } /** Not running an animation. */ - @VisibleForTesting public static final int ANIMATION_TYPE_NONE = -1; /** Running animation will show insets */ @@ -317,11 +316,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation public static final int ANIMATION_TYPE_HIDE = 1; /** Running animation is controlled by user via {@link #controlWindowInsetsAnimation} */ - @VisibleForTesting(visibility = PACKAGE) public static final int ANIMATION_TYPE_USER = 2; /** Running animation will resize insets */ - @VisibleForTesting public static final int ANIMATION_TYPE_RESIZE = 3; @Retention(RetentionPolicy.SOURCE) @@ -757,11 +754,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation public InsetsController(Host host) { this(host, (controller, id, type) -> { if (!Flags.refactorInsetsController() && type == ime()) { - return new ImeInsetsSourceConsumer(id, controller.mState, - Transaction::new, controller); + return new ImeInsetsSourceConsumer(id, controller.mState, controller); } else { - return new InsetsSourceConsumer(id, type, controller.mState, - Transaction::new, controller); + return new InsetsSourceConsumer(id, type, controller.mState, controller); } }, host.getHandler()); } @@ -1525,9 +1520,15 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation insetsAnimationSpec, animationType, layoutInsetsDuringAnimation, mHost.getTranslator(), mHost.getHandler(), statsToken) : new InsetsAnimationControlImpl(controls, - frame, mState, listener, typesReady, this, insetsAnimationSpec, + frame, mState, listener, typesReady, this, this, insetsAnimationSpec, animationType, layoutInsetsDuringAnimation, mHost.getTranslator(), statsToken); + for (int i = controls.size() - 1; i >= 0; i--) { + final InsetsSourceConsumer consumer = mSourceConsumers.get(controls.keyAt(i)); + if (consumer != null) { + consumer.setSurfaceParamsApplier(runner.getSurfaceParamsApplier()); + } + } if ((typesReady & WindowInsets.Type.ime()) != 0) { ImeTracing.getInstance().triggerClientDump("InsetsAnimationControlImpl", mHost.getInputMethodManager(), null /* icProto */); diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java index f90b8411e333..5262751cc6ed 100644 --- a/core/java/android/view/InsetsResizeAnimationRunner.java +++ b/core/java/android/view/InsetsResizeAnimationRunner.java @@ -94,6 +94,11 @@ public class InsetsResizeAnimationRunner implements InsetsAnimationControlRunner } @Override + public SurfaceParamsApplier getSurfaceParamsApplier() { + return SurfaceParamsApplier.DEFAULT; + } + + @Override @Nullable public ImeTracker.Token getStatsToken() { // Return null as resizing the IME view is not explicitly tracked. diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index 391d757365e6..2e2ff1d49dfe 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -17,6 +17,7 @@ package android.view; import static android.view.InsetsController.ANIMATION_TYPE_NONE; +import static android.view.InsetsController.ANIMATION_TYPE_RESIZE; import static android.view.InsetsController.AnimationType; import static android.view.InsetsController.DEBUG; import static android.view.InsetsSourceConsumerProto.ANIMATION_STATE; @@ -32,12 +33,13 @@ import static com.android.window.flags.Flags.insetsControlSeq; import android.annotation.IntDef; import android.annotation.Nullable; +import android.graphics.Matrix; +import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; import android.util.proto.ProtoOutputStream; -import android.view.SurfaceControl.Transaction; import android.view.WindowInsets.Type.InsetsType; import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; @@ -48,7 +50,6 @@ import com.android.internal.inputmethod.ImeTracing; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; -import java.util.function.Supplier; /** * Controls the visibility and animations of a single window insets source. @@ -92,10 +93,12 @@ public class InsetsSourceConsumer { private final int mType; private static final String TAG = "InsetsSourceConsumer"; - private final Supplier<Transaction> mTransactionSupplier; @Nullable private InsetsSourceControl mSourceControl; private boolean mHasWindowFocus; + private InsetsAnimationControlRunner.SurfaceParamsApplier mSurfaceParamsApplier = + InsetsAnimationControlRunner.SurfaceParamsApplier.DEFAULT; + private final Matrix mTmpMatrix = new Matrix(); /** * Whether the view has focus returned by {@link #onWindowFocusGained(boolean)}. @@ -108,16 +111,13 @@ public class InsetsSourceConsumer { * @param id The ID of the consumed insets. * @param type The {@link InsetsType} of the consumed insets. * @param state The current {@link InsetsState} of the consumed insets. - * @param transactionSupplier The source of new {@link Transaction} instances. The supplier - * must provide *new* instances, which will be explicitly closed by this class. * @param controller The {@link InsetsController} to use for insets interaction. */ public InsetsSourceConsumer(int id, @InsetsType int type, InsetsState state, - Supplier<Transaction> transactionSupplier, InsetsController controller) { + InsetsController controller) { mId = id; mType = type; mState = state; - mTransactionSupplier = transactionSupplier; mController = controller; } @@ -162,6 +162,9 @@ public class InsetsSourceConsumer { if (localVisible != serverVisible) { mController.notifyVisibilityChanged(); } + + // Reset the applier to the default one which has the most lightweight implementation. + setSurfaceParamsApplier(InsetsAnimationControlRunner.SurfaceParamsApplier.DEFAULT); } else { final boolean requestedVisible = isRequestedVisibleAwaitingControl(); final SurfaceControl oldLeash = lastControl != null ? lastControl.getLeash() : null; @@ -184,10 +187,11 @@ public class InsetsSourceConsumer { mController.notifyVisibilityChanged(); } - // If we have a new leash, make sure visibility is up-to-date, even though we - // didn't want to run an animation above. - if (mController.getAnimationType(mType) == ANIMATION_TYPE_NONE) { - applyRequestedVisibilityToControl(); + // If there is no animation controlling the leash, make sure the visibility and the + // position is up-to-date. Note: ANIMATION_TYPE_RESIZE doesn't control the leash. + final int animType = mController.getAnimationType(mType); + if (animType == ANIMATION_TYPE_NONE || animType == ANIMATION_TYPE_RESIZE) { + applyRequestedVisibilityAndPositionToControl(); } // Remove the surface that owned by last control when it lost. @@ -229,6 +233,15 @@ public class InsetsSourceConsumer { } /** + * Sets the SurfaceParamsApplier that the latest animation runner is using. The leash owned by + * this class is always applied by the applier, so that the transaction order can always be + * aligned with the calling sequence. + */ + void setSurfaceParamsApplier(InsetsAnimationControlRunner.SurfaceParamsApplier applier) { + mSurfaceParamsApplier = applier; + } + + /** * Called right after the animation is started or finished. */ @VisibleForTesting(visibility = PACKAGE) @@ -431,24 +444,30 @@ public class InsetsSourceConsumer { if (DEBUG) Log.d(TAG, "updateSource: " + newSource); } - private void applyRequestedVisibilityToControl() { - if (mSourceControl == null || mSourceControl.getLeash() == null) { + private void applyRequestedVisibilityAndPositionToControl() { + if (mSourceControl == null) { return; } - - final boolean requestedVisible = (mController.getRequestedVisibleTypes() & mType) != 0; - try (Transaction t = mTransactionSupplier.get()) { - if (DEBUG) Log.d(TAG, "applyRequestedVisibilityToControl: " + requestedVisible); - if (requestedVisible) { - t.show(mSourceControl.getLeash()); - } else { - t.hide(mSourceControl.getLeash()); - } - // Ensure the alpha value is aligned with the actual requested visibility. - t.setAlpha(mSourceControl.getLeash(), requestedVisible ? 1 : 0); - t.apply(); + final SurfaceControl leash = mSourceControl.getLeash(); + if (leash == null) { + return; } - onPerceptible(requestedVisible); + + final boolean visible = (mController.getRequestedVisibleTypes() & mType) != 0; + final Point surfacePosition = mSourceControl.getSurfacePosition(); + + if (DEBUG) Log.d(TAG, "applyRequestedVisibilityAndPositionToControl: visible=" + visible + + " position=" + surfacePosition); + + mTmpMatrix.setTranslate(surfacePosition.x, surfacePosition.y); + mSurfaceParamsApplier.applySurfaceParams( + new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(leash) + .withVisibility(visible) + .withAlpha(visible ? 1 : 0) + .withMatrix(mTmpMatrix) + .build()); + + onPerceptible(visible); } void dumpDebug(ProtoOutputStream proto, long fieldId) { diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index e1402f8224eb..b6aad1145880 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -266,3 +266,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "remove_starting_window_wait_for_multi_transitions" + namespace: "windowing_frontend" + description: "Avoid remove starting window too early when playing multiple transitions" + bug: "362347290" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/os/anr/OWNERS b/core/java/com/android/internal/os/anr/OWNERS index 9816752db891..1ad642f78cde 100644 --- a/core/java/com/android/internal/os/anr/OWNERS +++ b/core/java/com/android/internal/os/anr/OWNERS @@ -1,3 +1,2 @@ benmiles@google.com -gaillard@google.com mohamadmahmoud@google.com
\ No newline at end of file diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index fbc058cc0330..b0e38e256430 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -122,18 +122,20 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto private final Lock mBackgroundServiceLock = new ReentrantLock(); private ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor(); - public PerfettoProtoLogImpl(@NonNull IProtoLogGroup[] groups) { + public PerfettoProtoLogImpl(@NonNull IProtoLogGroup[] groups) + throws ServiceManager.ServiceNotFoundException { this(null, null, null, () -> {}, groups); } - public PerfettoProtoLogImpl(@NonNull Runnable cacheUpdater, @NonNull IProtoLogGroup[] groups) { + public PerfettoProtoLogImpl(@NonNull Runnable cacheUpdater, @NonNull IProtoLogGroup[] groups) + throws ServiceManager.ServiceNotFoundException { this(null, null, null, cacheUpdater, groups); } public PerfettoProtoLogImpl( @NonNull String viewerConfigFilePath, @NonNull Runnable cacheUpdater, - @NonNull IProtoLogGroup[] groups) { + @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException { this(viewerConfigFilePath, null, new ProtoLogViewerConfigReader(() -> { @@ -177,12 +179,14 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, @Nullable ProtoLogViewerConfigReader viewerConfigReader, @NonNull Runnable cacheUpdater, - @NonNull IProtoLogGroup[] groups) { + @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException { this(viewerConfigFilePath, viewerConfigInputStreamProvider, viewerConfigReader, cacheUpdater, groups, ProtoLogDataSource::new, - IProtoLogConfigurationService.Stub - .asInterface(ServiceManager.getService(PROTOLOG_CONFIGURATION_SERVICE)) + android.tracing.Flags.clientSideProtoLogging() ? + IProtoLogConfigurationService.Stub.asInterface( + ServiceManager.getServiceOrThrow(PROTOLOG_CONFIGURATION_SERVICE) + ) : null ); } @@ -222,7 +226,7 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto if (android.tracing.Flags.clientSideProtoLogging()) { mProtoLogConfigurationService = configurationService; Objects.requireNonNull(mProtoLogConfigurationService, - "ServiceManager returned a null ProtoLog Configuration Service"); + "A null ProtoLog Configuration Service was provided!"); try { var args = new ProtoLogConfigurationServiceImpl.RegisterClientArgs(); diff --git a/core/java/com/android/internal/protolog/ProtoLog.java b/core/java/com/android/internal/protolog/ProtoLog.java index bf77db7b6a33..adf03fe5f775 100644 --- a/core/java/com/android/internal/protolog/ProtoLog.java +++ b/core/java/com/android/internal/protolog/ProtoLog.java @@ -16,6 +16,8 @@ package com.android.internal.protolog; +import android.os.ServiceManager; + import com.android.internal.protolog.common.IProtoLog; import com.android.internal.protolog.common.IProtoLogGroup; import com.android.internal.protolog.common.LogLevel; @@ -76,7 +78,11 @@ public class ProtoLog { groups = allGroups.toArray(new IProtoLogGroup[0]); } - sProtoLogInstance = new PerfettoProtoLogImpl(groups); + try { + sProtoLogInstance = new PerfettoProtoLogImpl(groups); + } catch (ServiceManager.ServiceNotFoundException e) { + throw new RuntimeException(e); + } } } else { sProtoLogInstance = new LogcatOnlyProtoLogImpl(); diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java index 7bdcf2d14b19..5d67534b1b44 100644 --- a/core/java/com/android/internal/protolog/ProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java @@ -23,6 +23,7 @@ import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LO import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.VIEWER_CONFIG_PATH; import android.annotation.Nullable; +import android.os.ServiceManager; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; @@ -106,18 +107,23 @@ public class ProtoLogImpl { final var groups = sLogGroups.values().toArray(new IProtoLogGroup[0]); if (android.tracing.Flags.perfettoProtologTracing()) { - File f = new File(sViewerConfigPath); - if (!ProtoLog.REQUIRE_PROTOLOGTOOL && !f.exists()) { - // TODO(b/353530422): Remove - temporary fix to unblock b/352290057 - // In some tests the viewer config file might not exist in which we don't - // want to provide config path to the user - Log.w(LOG_TAG, "Failed to find viewerConfigFile when setting up " - + ProtoLogImpl.class.getSimpleName() + ". " - + "Setting up without a viewer config instead..."); - sServiceInstance = new PerfettoProtoLogImpl(sCacheUpdater, groups); - } else { - sServiceInstance = - new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater, groups); + try { + File f = new File(sViewerConfigPath); + if (!ProtoLog.REQUIRE_PROTOLOGTOOL && !f.exists()) { + // TODO(b/353530422): Remove - temporary fix to unblock b/352290057 + // In some tests the viewer config file might not exist in which we don't + // want to provide config path to the user + Log.w(LOG_TAG, "Failed to find viewerConfigFile when setting up " + + ProtoLogImpl.class.getSimpleName() + ". " + + "Setting up without a viewer config instead..."); + + sServiceInstance = new PerfettoProtoLogImpl(sCacheUpdater, groups); + } else { + sServiceInstance = + new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater, groups); + } + } catch (ServiceManager.ServiceNotFoundException e) { + throw new RuntimeException(e); } } else { var protologImpl = new LegacyProtoLogImpl( diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java index e65b4b65945f..c0a7383c9f06 100644 --- a/core/java/com/android/internal/widget/PointerLocationView.java +++ b/core/java/com/android/internal/widget/PointerLocationView.java @@ -16,14 +16,19 @@ package com.android.internal.widget; +import static java.lang.Float.NaN; + import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Insets; import android.graphics.Paint; import android.graphics.Paint.FontMetricsInt; import android.graphics.Path; +import android.graphics.PorterDuff; import android.graphics.RectF; import android.graphics.Region; import android.hardware.input.InputManager; @@ -65,11 +70,14 @@ public class PointerLocationView extends View implements InputDeviceListener, private static final PointerState EMPTY_POINTER_STATE = new PointerState(); public static class PointerState { - // Trace of previous points. - private float[] mTraceX = new float[32]; - private float[] mTraceY = new float[32]; - private boolean[] mTraceCurrent = new boolean[32]; - private int mTraceCount; + private float mCurrentX = NaN; + private float mCurrentY = NaN; + private float mPreviousX = NaN; + private float mPreviousY = NaN; + private float mFirstX = NaN; + private float mFirstY = NaN; + private boolean mPreviousPointIsHistorical; + private boolean mCurrentPointIsHistorical; // True if the pointer is down. @UnsupportedAppUsage @@ -96,31 +104,20 @@ public class PointerLocationView extends View implements InputDeviceListener, public PointerState() { } - public void clearTrace() { - mTraceCount = 0; - } - - public void addTrace(float x, float y, boolean current) { - int traceCapacity = mTraceX.length; - if (mTraceCount == traceCapacity) { - traceCapacity *= 2; - float[] newTraceX = new float[traceCapacity]; - System.arraycopy(mTraceX, 0, newTraceX, 0, mTraceCount); - mTraceX = newTraceX; - - float[] newTraceY = new float[traceCapacity]; - System.arraycopy(mTraceY, 0, newTraceY, 0, mTraceCount); - mTraceY = newTraceY; - - boolean[] newTraceCurrent = new boolean[traceCapacity]; - System.arraycopy(mTraceCurrent, 0, newTraceCurrent, 0, mTraceCount); - mTraceCurrent= newTraceCurrent; + public void addTrace(float x, float y, boolean isHistorical) { + if (Float.isNaN(mFirstX)) { + mFirstX = x; + } + if (Float.isNaN(mFirstY)) { + mFirstY = y; } - mTraceX[mTraceCount] = x; - mTraceY[mTraceCount] = y; - mTraceCurrent[mTraceCount] = current; - mTraceCount += 1; + mPreviousX = mCurrentX; + mPreviousY = mCurrentY; + mCurrentX = x; + mCurrentY = y; + mPreviousPointIsHistorical = mCurrentPointIsHistorical; + mCurrentPointIsHistorical = isHistorical; } } @@ -149,6 +146,12 @@ public class PointerLocationView extends View implements InputDeviceListener, private final SparseArray<PointerState> mPointers = new SparseArray<PointerState>(); private final PointerCoords mTempCoords = new PointerCoords(); + // Draw the trace of all pointers in the current gesture in a separate layer + // that is not cleared on every frame so that we don't have to re-draw the + // entire trace on each frame. + private final Bitmap mTraceBitmap; + private final Canvas mTraceCanvas; + private final Region mSystemGestureExclusion = new Region(); private final Region mSystemGestureExclusionRejected = new Region(); private final Path mSystemGestureExclusionPath = new Path(); @@ -197,6 +200,10 @@ public class PointerLocationView extends View implements InputDeviceListener, mPathPaint.setARGB(255, 0, 96, 255); mPathPaint.setStyle(Paint.Style.STROKE); + mTraceBitmap = Bitmap.createBitmap(getResources().getDisplayMetrics().widthPixels, + getResources().getDisplayMetrics().heightPixels, Bitmap.Config.ARGB_8888); + mTraceCanvas = new Canvas(mTraceBitmap); + configureDensityDependentFactors(); mSystemGestureExclusionPaint = new Paint(); @@ -256,7 +263,7 @@ public class PointerLocationView extends View implements InputDeviceListener, protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mTextPaint.getFontMetricsInt(mTextMetrics); - mHeaderBottom = mHeaderPaddingTop-mTextMetrics.ascent+mTextMetrics.descent+2; + mHeaderBottom = mHeaderPaddingTop - mTextMetrics.ascent + mTextMetrics.descent + 2; if (false) { Log.i("foo", "Metrics: ascent=" + mTextMetrics.ascent + " descent=" + mTextMetrics.descent @@ -269,6 +276,7 @@ public class PointerLocationView extends View implements InputDeviceListener, // Draw an oval. When angle is 0 radians, orients the major axis vertically, // angles less than or greater than 0 radians rotate the major axis left or right. private RectF mReusableOvalRect = new RectF(); + private void drawOval(Canvas canvas, float x, float y, float major, float minor, float angle, Paint paint) { canvas.save(Canvas.MATRIX_SAVE_FLAG); @@ -285,6 +293,8 @@ public class PointerLocationView extends View implements InputDeviceListener, protected void onDraw(Canvas canvas) { final int NP = mPointers.size(); + canvas.drawBitmap(mTraceBitmap, 0, 0, null); + if (!mSystemGestureExclusion.isEmpty()) { mSystemGestureExclusionPath.reset(); mSystemGestureExclusion.getBoundaryPath(mSystemGestureExclusionPath); @@ -303,32 +313,9 @@ public class PointerLocationView extends View implements InputDeviceListener, // Pointer trace. for (int p = 0; p < NP; p++) { final PointerState ps = mPointers.valueAt(p); + float lastX = ps.mCurrentX, lastY = ps.mCurrentY; - // Draw path. - final int N = ps.mTraceCount; - float lastX = 0, lastY = 0; - boolean haveLast = false; - boolean drawn = false; - mPaint.setARGB(255, 128, 255, 255); - for (int i=0; i < N; i++) { - float x = ps.mTraceX[i]; - float y = ps.mTraceY[i]; - if (Float.isNaN(x) || Float.isNaN(y)) { - haveLast = false; - continue; - } - if (haveLast) { - canvas.drawLine(lastX, lastY, x, y, mPathPaint); - final Paint paint = ps.mTraceCurrent[i - 1] ? mCurrentPointPaint : mPaint; - canvas.drawPoint(lastX, lastY, paint); - drawn = true; - } - lastX = x; - lastY = y; - haveLast = true; - } - - if (drawn) { + if (!Float.isNaN(lastX) && !Float.isNaN(lastY)) { // Draw velocity vector. mPaint.setARGB(255, 255, 64, 128); float xVel = ps.mXVelocity * (1000 / 60); @@ -353,7 +340,7 @@ public class PointerLocationView extends View implements InputDeviceListener, Math.max(getHeight(), getWidth()), mTargetPaint); // Draw current point. - int pressureLevel = (int)(ps.mCoords.pressure * 255); + int pressureLevel = (int) (ps.mCoords.pressure * 255); mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel); canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint); @@ -424,8 +411,7 @@ public class PointerLocationView extends View implements InputDeviceListener, .append(" / ").append(mMaxNumPointers) .toString(), 1, base, mTextPaint); - final int count = ps.mTraceCount; - if ((mCurDown && ps.mCurDown) || count == 0) { + if ((mCurDown && ps.mCurDown) || Float.isNaN(ps.mCurrentX)) { canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom, mTextBackgroundPaint); canvas.drawText(mText.clear() @@ -437,8 +423,8 @@ public class PointerLocationView extends View implements InputDeviceListener, .append("Y: ").append(ps.mCoords.y, 1) .toString(), 1 + itemW * 2, base, mTextPaint); } else { - float dx = ps.mTraceX[count - 1] - ps.mTraceX[0]; - float dy = ps.mTraceY[count - 1] - ps.mTraceY[0]; + float dx = ps.mCurrentX - ps.mFirstX; + float dy = ps.mCurrentY - ps.mFirstY; canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom, Math.abs(dx) < mVC.getScaledTouchSlop() ? mTextBackgroundPaint : mTextLevelPaint); @@ -565,9 +551,9 @@ public class PointerLocationView extends View implements InputDeviceListener, .append(" TouchMinor=").append(coords.touchMinor, 3) .append(" ToolMajor=").append(coords.toolMajor, 3) .append(" ToolMinor=").append(coords.toolMinor, 3) - .append(" Orientation=").append((float)(coords.orientation * 180 / Math.PI), 1) + .append(" Orientation=").append((float) (coords.orientation * 180 / Math.PI), 1) .append("deg") - .append(" Tilt=").append((float)( + .append(" Tilt=").append((float) ( coords.getAxisValue(MotionEvent.AXIS_TILT) * 180 / Math.PI), 1) .append("deg") .append(" Distance=").append(coords.getAxisValue(MotionEvent.AXIS_DISTANCE), 1) @@ -598,6 +584,7 @@ public class PointerLocationView extends View implements InputDeviceListener, mCurNumPointers = 0; mMaxNumPointers = 0; mVelocity.clear(); + mTraceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); if (mAltVelocity != null) { mAltVelocity.clear(); } @@ -646,7 +633,8 @@ public class PointerLocationView extends View implements InputDeviceListener, logCoords("Pointer", action, i, coords, id, event); } if (ps != null) { - ps.addTrace(coords.x, coords.y, false); + ps.addTrace(coords.x, coords.y, true); + updateDrawTrace(ps); } } } @@ -659,7 +647,8 @@ public class PointerLocationView extends View implements InputDeviceListener, logCoords("Pointer", action, i, coords, id, event); } if (ps != null) { - ps.addTrace(coords.x, coords.y, true); + ps.addTrace(coords.x, coords.y, false); + updateDrawTrace(ps); ps.mXVelocity = mVelocity.getXVelocity(id); ps.mYVelocity = mVelocity.getYVelocity(id); if (mAltVelocity != null) { @@ -702,13 +691,26 @@ public class PointerLocationView extends View implements InputDeviceListener, if (mActivePointerId == id) { mActivePointerId = event.getPointerId(index == 0 ? 1 : 0); } - ps.addTrace(Float.NaN, Float.NaN, false); + ps.addTrace(Float.NaN, Float.NaN, true); } } invalidate(); } + private void updateDrawTrace(PointerState ps) { + mPaint.setARGB(255, 128, 255, 255); + float x = ps.mCurrentX; + float y = ps.mCurrentY; + float lastX = ps.mPreviousX; + float lastY = ps.mPreviousY; + if (!Float.isNaN(x) && !Float.isNaN(y) && !Float.isNaN(lastX) && !Float.isNaN(lastY)) { + mTraceCanvas.drawLine(lastX, lastY, x, y, mPathPaint); + Paint paint = ps.mPreviousPointIsHistorical ? mPaint : mCurrentPointPaint; + mTraceCanvas.drawPoint(lastX, lastY, paint); + } + } + @Override public boolean onTouchEvent(MotionEvent event) { onPointerEvent(event); @@ -767,7 +769,7 @@ public class PointerLocationView extends View implements InputDeviceListener, return true; default: return KeyEvent.isGamepadButton(keyCode) - || KeyEvent.isModifierKey(keyCode); + || KeyEvent.isModifierKey(keyCode); } } @@ -887,7 +889,7 @@ public class PointerLocationView extends View implements InputDeviceListener, public FasterStringBuilder append(int value, int zeroPadWidth) { final boolean negative = value < 0; if (negative) { - value = - value; + value = -value; if (value < 0) { append("-2147483648"); return this; @@ -973,26 +975,27 @@ public class PointerLocationView extends View implements InputDeviceListener, private ISystemGestureExclusionListener mSystemGestureExclusionListener = new ISystemGestureExclusionListener.Stub() { - @Override - public void onSystemGestureExclusionChanged(int displayId, Region systemGestureExclusion, - Region systemGestureExclusionUnrestricted) { - Region exclusion = Region.obtain(systemGestureExclusion); - Region rejected = Region.obtain(); - if (systemGestureExclusionUnrestricted != null) { - rejected.set(systemGestureExclusionUnrestricted); - rejected.op(exclusion, Region.Op.DIFFERENCE); - } - Handler handler = getHandler(); - if (handler != null) { - handler.post(() -> { - mSystemGestureExclusion.set(exclusion); - mSystemGestureExclusionRejected.set(rejected); - exclusion.recycle(); - invalidate(); - }); - } - } - }; + @Override + public void onSystemGestureExclusionChanged(int displayId, + Region systemGestureExclusion, + Region systemGestureExclusionUnrestricted) { + Region exclusion = Region.obtain(systemGestureExclusion); + Region rejected = Region.obtain(); + if (systemGestureExclusionUnrestricted != null) { + rejected.set(systemGestureExclusionUnrestricted); + rejected.op(exclusion, Region.Op.DIFFERENCE); + } + Handler handler = getHandler(); + if (handler != null) { + handler.post(() -> { + mSystemGestureExclusion.set(exclusion); + mSystemGestureExclusionRejected.set(rejected); + exclusion.recycle(); + invalidate(); + }); + } + } + }; @Override protected void onConfigurationChanged(Configuration newConfig) { diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index 0837b458c3ba..0f73df92ca93 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -37,6 +37,7 @@ import static android.app.Notification.EXTRA_PICTURE; import static android.app.Notification.EXTRA_PICTURE_ICON; import static android.app.Notification.EXTRA_SUMMARY_TEXT; import static android.app.Notification.EXTRA_TITLE; +import static android.app.Notification.FLAG_CAN_COLORIZE; import static android.app.Notification.GROUP_ALERT_CHILDREN; import static android.app.Notification.GROUP_ALERT_SUMMARY; import static android.app.Notification.GROUP_KEY_SILENT; @@ -96,6 +97,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TextAppearanceSpan; import android.util.Pair; +import android.util.Slog; import android.widget.RemoteViews; import androidx.test.InstrumentationRegistry; @@ -126,6 +128,8 @@ public class NotificationTest { private Context mContext; + private RemoteViews mRemoteViews; + @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -133,23 +137,25 @@ public class NotificationTest { @Before public void setUp() { mContext = InstrumentationRegistry.getContext(); + mRemoteViews = new RemoteViews( + mContext.getPackageName(), R.layout.notification_template_header); } @Test public void testColorizedByPermission() { Notification n = new Notification.Builder(mContext, "test") - .setFlag(Notification.FLAG_CAN_COLORIZE, true) + .setFlag(FLAG_CAN_COLORIZE, true) .setColorized(true).setColor(Color.WHITE) .build(); assertTrue(n.isColorized()); n = new Notification.Builder(mContext, "test") - .setFlag(Notification.FLAG_CAN_COLORIZE, true) + .setFlag(FLAG_CAN_COLORIZE, true) .build(); assertFalse(n.isColorized()); n = new Notification.Builder(mContext, "test") - .setFlag(Notification.FLAG_CAN_COLORIZE, false) + .setFlag(FLAG_CAN_COLORIZE, false) .setColorized(true).setColor(Color.WHITE) .build(); assertFalse(n.isColorized()); @@ -215,6 +221,275 @@ public class NotificationTest { } @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasTitle_noStyle() { + Notification n = new Notification.Builder(mContext, "test") + .setContentTitle("TITLE") + .build(); + assertThat(n.hasTitle()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasTitle_bigText() { + Notification n = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .build(); + assertThat(n.hasTitle()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasTitle_noTitle() { + Notification n = new Notification.Builder(mContext, "test") + .setContentText("text not title") + .build(); + assertThat(n.hasTitle()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testContainsCustomViews_none() { + Notification np = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .build(); + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .setPublicVersion(np) + .build(); + assertThat(n.containsCustomViews()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testContainsCustomViews_content() { + Notification np = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .build(); + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .setCustomContentView(mRemoteViews) + .setPublicVersion(np) + .build(); + assertThat(n.containsCustomViews()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testContainsCustomViews_big() { + Notification np = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .build(); + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .setCustomBigContentView(mRemoteViews) + .setPublicVersion(np) + .build(); + assertThat(n.containsCustomViews()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testContainsCustomViews_headsUp() { + Notification np = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .build(); + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .setCustomHeadsUpContentView(mRemoteViews) + .setPublicVersion(np) + .build(); + assertThat(n.containsCustomViews()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testContainsCustomViews_content_public() { + Notification np = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("public") + .setCustomContentView(mRemoteViews) + .build(); + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .setPublicVersion(np) + .build(); + assertThat(n.containsCustomViews()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testContainsCustomViews_big_public() { + Notification np = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .setCustomBigContentView(mRemoteViews) + .build(); + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .setPublicVersion(np) + .build(); + assertThat(n.containsCustomViews()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testContainsCustomViews_headsUp_public() { + Notification np = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .setCustomHeadsUpContentView(mRemoteViews) + .build(); + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .setPublicVersion(np) + .build(); + assertThat(n.containsCustomViews()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableStyle_noStyle() { + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentText("test") + .build(); + assertThat(n.hasPromotableStyle()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableStyle_bigPicture() { + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigPictureStyle()) + .build(); + assertThat(n.hasPromotableStyle()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableStyle_bigText() { + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle()) + .build(); + assertThat(n.hasPromotableStyle()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableStyle_no_messagingStyle() { + Notification.MessagingStyle style = new Notification.MessagingStyle("self name") + .setGroupConversation(true) + .setConversationTitle("test conversation title"); + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(style) + .build(); + assertThat(n.hasPromotableStyle()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableStyle_no_mediaStyle() { + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.MediaStyle()) + .build(); + assertThat(n.hasPromotableStyle()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableStyle_no_inboxStyle() { + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.InboxStyle()) + .build(); + assertThat(n.hasPromotableStyle()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableStyle_callText() { + PendingIntent answerIntent = createPendingIntent("answer"); + PendingIntent declineIntent = createPendingIntent("decline"); + Notification.CallStyle style = Notification.CallStyle.forIncomingCall( + new Person.Builder().setName("A Caller").build(), + declineIntent, + answerIntent + ); + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(style) + .build(); + assertThat(n.hasPromotableStyle()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableCharacteristics() { + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .setColorized(true) + .setFlag(FLAG_CAN_COLORIZE, true) + .build(); + assertThat(n.hasPromotableCharacteristics()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableCharacteristics_wrongStyle() { + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.InboxStyle()) + .setContentTitle("TITLE") + .setColor(Color.WHITE) + .setColorized(true) + .setFlag(FLAG_CAN_COLORIZE, true) + .build(); + assertThat(n.hasPromotableCharacteristics()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableCharacteristics_notColorized() { + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .build(); + assertThat(n.hasPromotableCharacteristics()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_UI_RICH_ONGOING) + public void testHasPromotableCharacteristics_noTitle() { + Notification n = new Notification.Builder(mContext, "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle()) + .setColor(Color.WHITE) + .setColorized(true) + .setFlag(FLAG_CAN_COLORIZE, true) + .build(); + assertThat(n.hasPromotableCharacteristics()).isFalse(); + } + + @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void testGetShortCriticalText_noneSet() { Notification n = new Notification.Builder(mContext, "test") diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java index 786f1e84728d..ba6f62c6ed19 100644 --- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java +++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java @@ -38,7 +38,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.platform.test.annotations.Presubmit; import android.util.SparseArray; -import android.view.SurfaceControl.Transaction; import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; @@ -79,7 +78,6 @@ public class InsetsAnimationControlImplTest { private SurfaceControl mNavLeash; private InsetsState mInsetsState; - @Mock Transaction mMockTransaction; @Mock InsetsController mMockController; @Mock WindowInsetsAnimationControlListener mMockListener; @@ -98,16 +96,14 @@ public class InsetsAnimationControlImplTest { mInsetsState.getOrCreateSource(ID_NAVIGATION_BAR, navigationBars()) .setFrame(new Rect(400, 0, 500, 500)); InsetsSourceConsumer topConsumer = new InsetsSourceConsumer(ID_STATUS_BAR, - WindowInsets.Type.statusBars(), mInsetsState, - () -> mMockTransaction, mMockController); + WindowInsets.Type.statusBars(), mInsetsState, mMockController); topConsumer.setControl( new InsetsSourceControl(ID_STATUS_BAR, WindowInsets.Type.statusBars(), mStatusLeash, true, new Point(0, 0), Insets.of(0, 100, 0, 0)), new int[1], new int[1]); InsetsSourceConsumer navConsumer = new InsetsSourceConsumer(ID_NAVIGATION_BAR, - WindowInsets.Type.navigationBars(), mInsetsState, - () -> mMockTransaction, mMockController); + WindowInsets.Type.navigationBars(), mInsetsState, mMockController); navConsumer.setControl( new InsetsSourceControl(ID_NAVIGATION_BAR, WindowInsets.Type.navigationBars(), mNavLeash, true, new Point(400, 0), Insets.of(0, 0, 100, 0)), @@ -131,8 +127,9 @@ public class InsetsAnimationControlImplTest { mController = new InsetsAnimationControlImpl(controls, new Rect(0, 0, 500, 500), mInsetsState, mMockListener, systemBars(), - mMockController, spec /* insetsAnimationSpecCreator */, 0 /* animationType */, - 0 /* layoutInsetsDuringAnimation */, null /* translator */, null /* statsToken */); + mMockController, mMockController, spec /* insetsAnimationSpecCreator */, + 0 /* animationType */, 0 /* layoutInsetsDuringAnimation */, null /* translator */, + null /* statsToken */); mController.setReadyDispatched(true); } diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index bec8b1f76394..4516e9ce72fc 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -63,7 +63,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.os.CancellationSignal; import android.platform.test.annotations.Presubmit; -import android.view.SurfaceControl.Transaction; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController.OnControllableInsetsChangedListener; import android.view.WindowManager.BadTokenException; @@ -138,8 +137,7 @@ public class InsetsControllerTest { mTestHost = spy(new TestHost(mViewRoot)); mController = new InsetsController(mTestHost, (controller, id, type) -> { if (!Flags.refactorInsetsController() && type == ime()) { - return new InsetsSourceConsumer(id, type, controller.getState(), - Transaction::new, controller) { + return new InsetsSourceConsumer(id, type, controller.getState(), controller) { private boolean mImeRequestedShow; @@ -155,8 +153,7 @@ public class InsetsControllerTest { } }; } else { - return new InsetsSourceConsumer(id, type, controller.getState(), - Transaction::new, controller); + return new InsetsSourceConsumer(id, type, controller.getState(), controller); } }, mTestHandler); final Rect rect = new Rect(5, 5, 5, 5); diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java index 655cb4519d3c..d6d45e839f2f 100644 --- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java @@ -28,10 +28,7 @@ import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; import android.app.Instrumentation; import android.content.Context; @@ -39,7 +36,6 @@ import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; -import android.view.SurfaceControl.Transaction; import android.view.WindowManager.BadTokenException; import android.view.WindowManager.LayoutParams; import android.view.inputmethod.ImeTracker; @@ -51,7 +47,6 @@ import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -75,9 +70,9 @@ public class InsetsSourceConsumerTest { private SurfaceSession mSession = new SurfaceSession(); private SurfaceControl mLeash; - @Mock Transaction mMockTransaction; private InsetsSource mSpyInsetsSource; private boolean mRemoveSurfaceCalled = false; + private boolean mSurfaceParamsApplied = false; private InsetsController mController; private InsetsState mState; private ViewRootImpl mViewRoot; @@ -102,9 +97,14 @@ public class InsetsSourceConsumerTest { mSpyInsetsSource = Mockito.spy(new InsetsSource(ID_STATUS_BAR, statusBars())); mState.addSource(mSpyInsetsSource); - mController = new InsetsController(new ViewRootInsetsControllerHost(mViewRoot)); - mConsumer = new InsetsSourceConsumer(ID_STATUS_BAR, statusBars(), mState, - () -> mMockTransaction, mController) { + mController = new InsetsController(new ViewRootInsetsControllerHost(mViewRoot)) { + @Override + public void applySurfaceParams( + final SyncRtSurfaceTransactionApplier.SurfaceParams... params) { + mSurfaceParamsApplied = true; + } + }; + mConsumer = new InsetsSourceConsumer(ID_STATUS_BAR, statusBars(), mState, mController) { @Override public void removeSurface() { super.removeSurface(); @@ -148,8 +148,7 @@ public class InsetsSourceConsumerTest { InsetsState state = new InsetsState(); InsetsController controller = new InsetsController(new ViewRootInsetsControllerHost( mViewRoot)); - InsetsSourceConsumer consumer = new InsetsSourceConsumer( - ID_IME, ime(), state, null, controller); + InsetsSourceConsumer consumer = new InsetsSourceConsumer(ID_IME, ime(), state, controller); InsetsSource source = new InsetsSource(ID_IME, ime()); source.setFrame(0, 1, 2, 3); @@ -182,9 +181,9 @@ public class InsetsSourceConsumerTest { public void testRestore() { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { mConsumer.setControl(null, new int[1], new int[1]); - reset(mMockTransaction); + mSurfaceParamsApplied = false; mController.setRequestedVisibleTypes(0 /* visibleTypes */, statusBars()); - verifyZeroInteractions(mMockTransaction); + assertFalse(mSurfaceParamsApplied); int[] hideTypes = new int[1]; mConsumer.setControl( new InsetsSourceControl(ID_STATUS_BAR, statusBars(), mLeash, @@ -200,8 +199,9 @@ public class InsetsSourceConsumerTest { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { mController.setRequestedVisibleTypes(0 /* visibleTypes */, statusBars()); mConsumer.setControl(null, new int[1], new int[1]); - reset(mMockTransaction); - verifyZeroInteractions(mMockTransaction); + mLeash = new SurfaceControl.Builder(mSession) + .setName("testSurface") + .build(); mRemoveSurfaceCalled = false; int[] hideTypes = new int[1]; mConsumer.setControl( @@ -221,8 +221,7 @@ public class InsetsSourceConsumerTest { ViewRootInsetsControllerHost host = new ViewRootInsetsControllerHost(mViewRoot); InsetsController insetsController = new InsetsController(host, (ic, id, type) -> { if (type == ime()) { - return new InsetsSourceConsumer(ID_IME, ime(), state, - () -> mMockTransaction, ic) { + return new InsetsSourceConsumer(ID_IME, ime(), state, ic) { @Override public int requestShow(boolean fromController, ImeTracker.Token statsToken) { @@ -230,14 +229,14 @@ public class InsetsSourceConsumerTest { } }; } - return new InsetsSourceConsumer(id, type, ic.getState(), Transaction::new, ic); + return new InsetsSourceConsumer(id, type, ic.getState(), ic); }, host.getHandler()); InsetsSourceConsumer imeConsumer = insetsController.getSourceConsumer(ID_IME, ime()); // Initial IME insets source control with its leash. imeConsumer.setControl(new InsetsSourceControl(ID_IME, ime(), mLeash, false /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1]); - reset(mMockTransaction); + mSurfaceParamsApplied = false; // Verify when the app requests controlling show IME animation, the IME leash // visibility won't be updated when the consumer received the same leash in setControl. @@ -246,7 +245,7 @@ public class InsetsSourceConsumerTest { assertEquals(ANIMATION_TYPE_USER, insetsController.getAnimationType(ime())); imeConsumer.setControl(new InsetsSourceControl(ID_IME, ime(), mLeash, true /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1]); - verify(mMockTransaction, never()).show(mLeash); + assertFalse(mSurfaceParamsApplied); }); } } diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index e07471cd64bc..6d31578ac020 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -128,22 +128,6 @@ public final class Bitmap implements Parcelable { private static final WeakHashMap<Bitmap, Void> sAllBitmaps = new WeakHashMap<>(); /** - * @hide - */ - private static NativeAllocationRegistry getRegistry(boolean malloc, long size) { - final long free = nativeGetNativeFinalizer(); - if (com.android.libcore.Flags.nativeMetrics()) { - Class cls = Bitmap.class; - return malloc ? NativeAllocationRegistry.createMalloced(cls, free, size) - : NativeAllocationRegistry.createNonmalloced(cls, free, size); - } else { - ClassLoader loader = Bitmap.class.getClassLoader(); - return malloc ? NativeAllocationRegistry.createMalloced(loader, free, size) - : NativeAllocationRegistry.createNonmalloced(loader, free, size); - } - } - - /** * Private constructor that must receive an already allocated native bitmap * int (pointer). */ @@ -167,6 +151,7 @@ public final class Bitmap implements Parcelable { mWidth = width; mHeight = height; mRequestPremultiplied = requestPremultiplied; + mNinePatchChunk = ninePatchChunk; mNinePatchInsets = ninePatchInsets; if (density >= 0) { @@ -174,9 +159,17 @@ public final class Bitmap implements Parcelable { } mNativePtr = nativeBitmap; - final int allocationByteCount = getAllocationByteCount(); - getRegistry(fromMalloc, allocationByteCount).registerNativeAllocation(this, mNativePtr); + final int allocationByteCount = getAllocationByteCount(); + NativeAllocationRegistry registry; + if (fromMalloc) { + registry = NativeAllocationRegistry.createMalloced( + Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount); + } else { + registry = NativeAllocationRegistry.createNonmalloced( + Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount); + } + registry.registerNativeAllocation(this, nativeBitmap); synchronized (Bitmap.class) { sAllBitmaps.put(this, null); } diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 94809f2d258f..f8574294a3a2 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -147,8 +147,10 @@ java_library { java_library { name: "WindowManager-Shell-lite-proto", - srcs: ["src/com/android/wm/shell/desktopmode/education/data/proto/**/*.proto"], - + srcs: [ + "src/com/android/wm/shell/desktopmode/education/data/proto/**/*.proto", + "src/com/android/wm/shell/desktopmode/persistence/*.proto", + ], proto: { type: "lite", }, diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml index c0ff1922edc8..1d1cdfa85040 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml @@ -28,6 +28,8 @@ android:layout_height="@dimen/desktop_mode_fullscreen_decor_caption_height" android:paddingVertical="16dp" android:paddingHorizontal="10dp" + android:screenReaderFocusable="true" + android:importantForAccessibility="yes" android:contentDescription="@string/handle_text" android:src="@drawable/decor_handle_dark" tools:tint="@color/desktop_mode_caption_handle_bar_dark" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml index 7dcb3c237c51..3dbf7542ac6e 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml @@ -31,14 +31,16 @@ android:orientation="horizontal" android:clickable="true" android:focusable="true" + android:contentDescription="@string/desktop_mode_app_header_chip_text" android:layout_marginStart="12dp"> <ImageView android:id="@+id/application_icon" android:layout_width="@dimen/desktop_mode_caption_icon_radius" android:layout_height="@dimen/desktop_mode_caption_icon_radius" android:layout_gravity="center_vertical" - android:contentDescription="@string/app_icon_text" android:layout_marginStart="6dp" + android:clickable="false" + android:focusable="false" android:scaleType="centerCrop"/> <TextView @@ -53,18 +55,22 @@ android:layout_gravity="center_vertical" android:layout_weight="1" android:layout_marginStart="8dp" + android:clickable="false" + android:focusable="false" tools:text="Gmail"/> <ImageButton android:id="@+id/expand_menu_button" android:layout_width="16dp" android:layout_height="16dp" - android:contentDescription="@string/expand_menu_text" android:src="@drawable/ic_baseline_expand_more_24" android:background="@null" android:scaleType="fitCenter" android:clickable="false" android:focusable="false" + android:screenReaderFocusable="false" + android:importantForAccessibility="no" + android:contentDescription="@null" android:layout_marginHorizontal="8dp" android:layout_gravity="center_vertical"/> @@ -90,6 +96,7 @@ <com.android.wm.shell.windowdecor.MaximizeButtonView android:id="@+id/maximize_button_view" + android:importantForAccessibility="no" android:layout_width="44dp" android:layout_height="40dp" android:layout_gravity="end" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index 64f71c713d1c..6913e54c2b10 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -43,13 +43,15 @@ android:layout_height="@dimen/desktop_mode_caption_icon_radius" android:layout_marginStart="12dp" android:layout_marginEnd="12dp" - android:contentDescription="@string/app_icon_text"/> + android:contentDescription="@string/app_icon_text" + android:importantForAccessibility="no"/> <TextView android:id="@+id/application_name" android:layout_width="0dp" android:layout_height="wrap_content" tools:text="Gmail" + android:importantForAccessibility="no" android:textColor="?androidprv:attr/materialColorOnSurface" android:textSize="14sp" android:textFontWeight="500" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index 5fe3f2af63a0..35ef2393bb9b 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -41,6 +41,8 @@ android:id="@+id/maximize_menu_maximize_button" style="?android:attr/buttonBarButtonStyle" android:stateListAnimator="@null" + android:importantForAccessibility="yes" + android:contentDescription="@string/desktop_mode_maximize_menu_maximize_button_text" android:layout_marginRight="8dp" android:layout_marginBottom="4dp" android:alpha="0"/> @@ -53,6 +55,7 @@ android:layout_marginBottom="76dp" android:gravity="center" android:fontFamily="google-sans-text" + android:importantForAccessibility="no" android:text="@string/desktop_mode_maximize_menu_maximize_text" android:textColor="?androidprv:attr/materialColorOnSurface" android:alpha="0"/> @@ -78,6 +81,8 @@ android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" android:layout_marginRight="4dp" android:background="@drawable/desktop_mode_maximize_menu_button_background" + android:importantForAccessibility="yes" + android:contentDescription="@string/desktop_mode_maximize_menu_snap_left_button_text" android:stateListAnimator="@null"/> <Button @@ -86,6 +91,8 @@ android:layout_width="41dp" android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" android:background="@drawable/desktop_mode_maximize_menu_button_background" + android:importantForAccessibility="yes" + android:contentDescription="@string/desktop_mode_maximize_menu_snap_right_button_text" android:stateListAnimator="@null"/> </LinearLayout> <TextView @@ -96,6 +103,7 @@ android:layout_marginBottom="76dp" android:layout_gravity="center" android:gravity="center" + android:importantForAccessibility="no" android:fontFamily="google-sans-text" android:text="@string/desktop_mode_maximize_menu_snap_text" android:textColor="?androidprv:attr/materialColorOnSurface" diff --git a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml index cf1b8947467e..b734d2d81455 100644 --- a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml +++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml @@ -19,7 +19,8 @@ <FrameLayout android:layout_width="44dp" - android:layout_height="40dp"> + android:layout_height="40dp" + android:importantForAccessibility="noHideDescendants"> <ProgressBar android:id="@+id/progress_bar" style="?android:attr/progressBarStyleHorizontal" diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index a6da421dbbb9..bda56860d3ba 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -300,12 +300,19 @@ <string name="close_text">Close</string> <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] --> <string name="collapse_menu_text">Close Menu</string> - <!-- Accessibility text for the handle menu open menu button [CHAR LIMIT=NONE] --> - <string name="expand_menu_text">Open Menu</string> + <!-- Accessibility text for the App Header's App Chip [CHAR LIMIT=NONE] --> + <string name="desktop_mode_app_header_chip_text">Open Menu</string> <!-- Maximize menu maximize button string. --> <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string> <!-- Maximize menu snap buttons string. --> <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string> <!-- Snap resizing non-resizable string. --> <string name="desktop_mode_non_resizable_snap_text">This app can\'t be resized</string> + <!-- Accessibility text for the Maximize Menu's maximize button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_maximize_menu_maximize_button_text">Maximize</string> + <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_maximize_menu_snap_left_button_text">Snap left</string> + <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_maximize_menu_snap_right_button_text">Snap right</string> + </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 7e6f43458ba6..4607a8ec1210 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -584,7 +584,8 @@ public class ShellTaskOrganizer extends TaskOrganizer { final boolean windowModeChanged = data.getTaskInfo().getWindowingMode() != taskInfo.getWindowingMode(); final boolean visibilityChanged = data.getTaskInfo().isVisible != taskInfo.isVisible; - if (windowModeChanged || visibilityChanged) { + if (windowModeChanged || (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM + && visibilityChanged)) { mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskRunningInfoChanged(taskInfo)); } 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 80a9b675ebd8..308bd0bccc95 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 @@ -79,6 +79,7 @@ import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; +import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.draganddrop.GlobalDragListener; import com.android.wm.shell.freeform.FreeformComponents; @@ -712,8 +713,14 @@ public abstract class WMShellModule { @WMSingleton @Provides @DynamicOverride - static DesktopModeTaskRepository provideDesktopModeTaskRepository() { - return new DesktopModeTaskRepository(); + static DesktopModeTaskRepository provideDesktopModeTaskRepository( + Context context, + ShellInit shellInit, + DesktopPersistentRepository desktopPersistentRepository, + @ShellMainThread CoroutineScope mainScope + ) { + return new DesktopModeTaskRepository(context, shellInit, desktopPersistentRepository, + mainScope); } @WMSingleton @@ -798,6 +805,14 @@ public abstract class WMShellModule { shellTaskOrganizer, appHandleEducationDatastoreRepository, applicationScope); } + @WMSingleton + @Provides + static DesktopPersistentRepository provideDesktopPersistentRepository( + Context context, + @ShellBackgroundThread CoroutineScope bgScope) { + return new DesktopPersistentRepository(context, bgScope); + } + // // Drag and drop // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 9d041692200d..759ed035895e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.desktopmode +import android.content.Context import android.graphics.Rect import android.graphics.Region import android.util.ArrayMap @@ -27,13 +28,27 @@ import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags +import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.desktopmode.persistence.DesktopTask +import com.android.wm.shell.desktopmode.persistence.DesktopTaskState import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit import java.io.PrintWriter import java.util.concurrent.Executor import java.util.function.Consumer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** Tracks task data for Desktop Mode. */ -class DesktopModeTaskRepository { +class DesktopModeTaskRepository ( + private val context: Context, + shellInit: ShellInit, + private val persistentRepository: DesktopPersistentRepository, + @ShellMainThread private val mainCoroutineScope: CoroutineScope, +){ /** * Task data tracked per desktop. @@ -54,7 +69,15 @@ class DesktopModeTaskRepository { // TODO(b/332682201): Remove when the repository state is updated via TransitionObserver val closingTasks: ArraySet<Int> = ArraySet(), val freeformTasksInZOrder: ArrayList<Int> = ArrayList(), - ) + ) { + fun deepCopy(): DesktopTaskData = DesktopTaskData( + activeTasks = ArraySet(activeTasks), + visibleTasks = ArraySet(visibleTasks), + minimizedTasks = ArraySet(minimizedTasks), + closingTasks = ArraySet(closingTasks), + freeformTasksInZOrder = ArrayList(freeformTasksInZOrder) + ) + } /* Current wallpaper activity token to remove wallpaper activity when last task is removed. */ var wallpaperActivityToken: WindowContainerToken? = null @@ -77,6 +100,40 @@ class DesktopModeTaskRepository { this[displayId] ?: DesktopTaskData().also { this[displayId] = it } } + init { + if (DesktopModeStatus.canEnterDesktopMode(context)) { + shellInit.addInitCallback(::initRepoFromPersistentStorage, this) + } + } + + private fun initRepoFromPersistentStorage() { + if (!Flags.enableDesktopWindowingPersistence()) return + // TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized + mainCoroutineScope.launch { + val desktop = persistentRepository.readDesktop() + val maxTasks = + DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } + ?: desktop.zOrderedTasksCount + + desktop.zOrderedTasksList + // Reverse it so we initialize the repo from bottom to top. + .reversed() + .map { taskId -> + desktop.tasksByTaskIdMap.getOrDefault( + taskId, + DesktopTask.getDefaultInstance() + ) + } + .filter { task -> task.desktopTaskState == DesktopTaskState.VISIBLE } + .take(maxTasks) + .forEach { task -> + addOrMoveFreeformTaskToTop(desktop.displayId, task.taskId) + addActiveTask(desktop.displayId, task.taskId) + updateTaskVisibility(desktop.displayId, task.taskId, visible = false) + } + } + } + /** Adds [activeTasksListener] to be notified of updates to active tasks. */ fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) { activeTasksListeners.add(activeTasksListener) @@ -266,12 +323,18 @@ class DesktopModeTaskRepository { desktopTaskDataByDisplayId.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId) // Unminimize the task if it is minimized. unminimizeTask(displayId, taskId) + if (Flags.enableDesktopWindowingPersistence()) { + updatePersistentRepository(displayId) + } } /** Minimizes the task for [taskId] and [displayId] */ fun minimizeTask(displayId: Int, taskId: Int) { logD("Minimize Task: display=%d, task=%d", displayId, taskId) desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId) + if (Flags.enableDesktopWindowingPersistence()) { + updatePersistentRepository(displayId) + } } /** Unminimizes the task for [taskId] and [displayId] */ @@ -315,7 +378,10 @@ class DesktopModeTaskRepository { // Remove task from unminimized task if it is minimized. unminimizeTask(displayId, taskId) removeActiveTask(taskId) - updateTaskVisibility(displayId, taskId, visible = false); + updateTaskVisibility(displayId, taskId, visible = false) + if (Flags.enableDesktopWindowingPersistence()) { + updatePersistentRepository(displayId) + } } /** @@ -352,6 +418,27 @@ class DesktopModeTaskRepository { fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) = boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds)) + private fun updatePersistentRepository(displayId: Int) { + // Create a deep copy of the data + desktopTaskDataByDisplayId[displayId]?.deepCopy()?.let { desktopTaskDataByDisplayIdCopy -> + mainCoroutineScope.launch { + try { + persistentRepository.addOrUpdateDesktop( + visibleTasks = desktopTaskDataByDisplayIdCopy.visibleTasks, + minimizedTasks = desktopTaskDataByDisplayIdCopy.minimizedTasks, + freeformTasksInZOrder = desktopTaskDataByDisplayIdCopy.freeformTasksInZOrder + ) + } catch (exception: Exception) { + logE( + "An exception occurred while updating the persistent repository \n%s", + exception.stackTrace + ) + } + } + } + } + + internal fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopModeTaskRepository") @@ -390,6 +477,10 @@ class DesktopModeTaskRepository { ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } + private fun logE(msg: String, vararg arguments: Any?) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + companion object { private const val TAG = "DesktopModeTaskRepository" } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index f3ae3ed45f41..968f40c3df5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -58,6 +58,7 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE import com.android.internal.jank.InteractionJankMonitor import com.android.internal.policy.ScreenDecorationsUtils import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController @@ -80,8 +81,8 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.recents.RecentTasksController import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener -import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.ShellSharedConstants +import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.annotations.ExternalThread import com.android.wm.shell.shared.annotations.ShellMainThread import android.window.flags.DesktopModeFlags @@ -728,7 +729,7 @@ class DesktopTasksController( // exclude current task since maximize/restore transition has not taken place yet. .filterNot { taskId -> taskId == excludeTaskId } .any { taskId -> - val taskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId)!! + val taskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return false val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) val stableBounds = Rect().apply { displayLayout?.getStableBounds(this) } logD("taskInfo = %s", taskInfo) @@ -896,6 +897,7 @@ class DesktopTasksController( val nonMinimizedTasksOrderedFrontToBack = taskRepository.getActiveNonMinimizedOrderedTasks(displayId) // If we're adding a new Task we might need to minimize an old one + // TODO(b/365725441): Handle non running task minimization val taskToMinimize: RunningTaskInfo? = if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) { desktopTasksLimiter @@ -907,12 +909,26 @@ class DesktopTasksController( } else { null } + nonMinimizedTasksOrderedFrontToBack // If there is a Task to minimize, let it stay behind the Home Task .filter { taskId -> taskId != taskToMinimize?.taskId } - .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } .reversed() // Start from the back so the front task is brought forward last - .forEach { task -> wct.reorder(task.token, /* onTop= */ true) } + .forEach { taskId -> + val runningTaskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) + if (runningTaskInfo != null) { + // Task is already running, reorder it to the front + wct.reorder(runningTaskInfo.token, /* onTop= */ true) + } else if (Flags.enableDesktopWindowingPersistence()) { + // Task is not running, start it + wct.startTask( + taskId, + ActivityOptions.makeBasic().apply { + launchWindowingMode = WINDOWING_MODE_FREEFORM + }.toBundle(), + ) + } + } taskbarDesktopTaskListener?. onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding(displayId)) @@ -1211,6 +1227,7 @@ class DesktopTasksController( wct.reorder(task.token, true) return wct } + // TODO(b/365723620): Handle non running tasks that were launched after reboot. // If task is already visible, it must have been handled already and added to desktop mode. // Cascade task only if it's not visible yet. if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt new file mode 100644 index 000000000000..3f41d7cf4e86 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt @@ -0,0 +1,201 @@ +/* + * 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.wm.shell.desktopmode.persistence + +import android.content.Context +import android.util.ArraySet +import android.util.Log +import android.view.Display.DEFAULT_DISPLAY +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.Serializer +import androidx.datastore.dataStoreFile +import com.android.framework.protobuf.InvalidProtocolBufferException +import com.android.wm.shell.shared.annotations.ShellBackgroundThread +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first + +/** + * Persistent repository for storing desktop mode related data. + * + * The main constructor is public only for testing purposes. + */ +class DesktopPersistentRepository( + private val dataStore: DataStore<DesktopPersistentRepositories>, +) { + constructor( + context: Context, + @ShellBackgroundThread bgCoroutineScope: CoroutineScope, + ) : this( + DataStoreFactory.create( + serializer = DesktopPersistentRepositoriesSerializer, + produceFile = { context.dataStoreFile(DESKTOP_REPOSITORIES_DATASTORE_FILE) }, + scope = bgCoroutineScope)) + + /** Provides `dataStore.data` flow and handles exceptions thrown during collection */ + private val dataStoreFlow: Flow<DesktopPersistentRepositories> = + dataStore.data.catch { exception -> + // dataStore.data throws an IOException when an error is encountered when reading data + if (exception is IOException) { + Log.e( + TAG, + "Error in reading desktop mode related data from datastore, data is " + + "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE", + exception) + } else { + throw exception + } + } + + /** + * Reads and returns the [DesktopRepositoryState] proto object from the DataStore for a user. If + * the DataStore is empty or there's an error reading, it returns the default value of Proto. + */ + private suspend fun getDesktopRepositoryState( + userId: Int = DEFAULT_USER_ID + ): DesktopRepositoryState = + try { + dataStoreFlow + .first() + .desktopRepoByUserMap + .getOrDefault(userId, DesktopRepositoryState.getDefaultInstance()) + } catch (e: Exception) { + Log.e(TAG, "Unable to read from datastore", e) + DesktopRepositoryState.getDefaultInstance() + } + + /** + * Reads the [Desktop] of a desktop filtering by the [userId] and [desktopId]. Executes the + * [callback] using the [mainCoroutineScope]. + */ + suspend fun readDesktop( + userId: Int = DEFAULT_USER_ID, + desktopId: Int = DEFAULT_DESKTOP_ID, + ): Desktop = + try { + val repository = getDesktopRepositoryState(userId) + repository.getDesktopOrThrow(desktopId) + } catch (e: Exception) { + Log.e(TAG, "Unable to get desktop info from persistent repository", e) + Desktop.getDefaultInstance() + } + + /** Adds or updates a desktop stored in the datastore */ + suspend fun addOrUpdateDesktop( + userId: Int = DEFAULT_USER_ID, + desktopId: Int = 0, + visibleTasks: ArraySet<Int> = ArraySet(), + minimizedTasks: ArraySet<Int> = ArraySet(), + freeformTasksInZOrder: ArrayList<Int> = ArrayList(), + ) { + // TODO: b/367609270 - Improve the API to support multi-user + try { + dataStore.updateData { desktopPersistentRepositories: DesktopPersistentRepositories -> + val currentRepository = + desktopPersistentRepositories.getDesktopRepoByUserOrDefault( + userId, DesktopRepositoryState.getDefaultInstance()) + val desktop = + getDesktop(currentRepository, desktopId) + .toBuilder() + .updateTaskStates(visibleTasks, minimizedTasks) + .updateZOrder(freeformTasksInZOrder) + + desktopPersistentRepositories + .toBuilder() + .putDesktopRepoByUser( + userId, + currentRepository + .toBuilder() + .putDesktop(desktopId, desktop.build()) + .build()) + .build() + } + } catch (exception: IOException) { + Log.e( + TAG, + "Error in updating desktop mode related data, data is " + + "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE", + exception) + } + } + + private fun getDesktop(currentRepository: DesktopRepositoryState, desktopId: Int): Desktop = + // If there are no desktops set up, create one on the default display + currentRepository.getDesktopOrDefault( + desktopId, + Desktop.newBuilder().setDesktopId(desktopId).setDisplayId(DEFAULT_DISPLAY).build()) + + companion object { + private const val TAG = "DesktopPersistenceRepo" + private const val DESKTOP_REPOSITORIES_DATASTORE_FILE = "desktop_persistent_repositories.pb" + + private const val DEFAULT_USER_ID = 1000 + private const val DEFAULT_DESKTOP_ID = 0 + + object DesktopPersistentRepositoriesSerializer : Serializer<DesktopPersistentRepositories> { + + override val defaultValue: DesktopPersistentRepositories = + DesktopPersistentRepositories.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): DesktopPersistentRepositories = + try { + DesktopPersistentRepositories.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + + override suspend fun writeTo(t: DesktopPersistentRepositories, output: OutputStream) = + t.writeTo(output) + } + + private fun Desktop.Builder.updateTaskStates( + visibleTasks: ArraySet<Int>, + minimizedTasks: ArraySet<Int> + ): Desktop.Builder { + clearTasksByTaskId() + putAllTasksByTaskId( + visibleTasks.associateWith { + createDesktopTask(it, state = DesktopTaskState.VISIBLE) + }) + putAllTasksByTaskId( + minimizedTasks.associateWith { + createDesktopTask(it, state = DesktopTaskState.MINIMIZED) + }) + return this + } + + private fun Desktop.Builder.updateZOrder( + freeformTasksInZOrder: ArrayList<Int> + ): Desktop.Builder { + clearZOrderedTasks() + addAllZOrderedTasks(freeformTasksInZOrder) + return this + } + + private fun createDesktopTask( + taskId: Int, + state: DesktopTaskState = DesktopTaskState.VISIBLE + ): DesktopTask = + DesktopTask.newBuilder().setTaskId(taskId).setDesktopTaskState(state).build() + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/persistent_desktop_repositories.proto b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/persistent_desktop_repositories.proto new file mode 100644 index 000000000000..010523162cec --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/persistent_desktop_repositories.proto @@ -0,0 +1,33 @@ +syntax = "proto2"; + +option java_package = "com.android.wm.shell.desktopmode.persistence"; +option java_multiple_files = true; + +// Represents the state of a task in desktop. +enum DesktopTaskState { + VISIBLE = 0; + MINIMIZED = 1; +} + +message DesktopTask { + optional int32 task_id = 1; + optional DesktopTaskState desktop_task_state= 2; +} + +message Desktop { + optional int32 display_id = 1; + optional int32 desktop_id = 2; + // Stores a mapping between task id and the tasks. The key is the task id. + map<int32, DesktopTask> tasks_by_task_id = 3; + repeated int32 z_ordered_tasks = 4; +} + +message DesktopRepositoryState { + // Stores a mapping between a repository and the desktops in it. The key is the desktop id. + map<int32, Desktop> desktop = 1; +} + +message DesktopPersistentRepositories { + // Stores a mapping between a user and their desktop repository. The key is the user id. + map<int32, DesktopRepositoryState> desktop_repo_by_user = 1; +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 1573291aef63..3a2820ee3aa9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -358,12 +358,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { if (mode == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) { if (info.getType() == TRANSIT_CHANGE) { - int anim = getRotationAnimationHint(change, info, mDisplayController); + final int anim = getRotationAnimationHint(change, info, mDisplayController); isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS; if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) { - if (wallpaperTransit != WALLPAPER_TRANSITION_NONE) { - anim |= ScreenRotationAnimation.ANIMATION_HINT_HAS_WALLPAPER; - } startRotationAnimation(startTransaction, change, info, anim, animations, onAnimFinish); isDisplayRotationAnimationStarted = true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java index b9d11a3d0c06..5802e2ca8133 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -25,9 +25,12 @@ import static com.android.wm.shell.transition.DefaultTransitionHandler.buildSurf import static com.android.wm.shell.transition.Transitions.TAG; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.content.Context; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.hardware.HardwareBuffer; @@ -35,7 +38,6 @@ import android.util.Slog; import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; -import android.view.animation.AccelerateInterpolator; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.window.ScreenCapture; @@ -72,9 +74,6 @@ import java.util.ArrayList; */ class ScreenRotationAnimation { static final int MAX_ANIMATION_DURATION = 10 * 1000; - static final int ANIMATION_HINT_HAS_WALLPAPER = 1 << 8; - /** It must cover all WindowManager#ROTATION_ANIMATION_*. */ - private static final int ANIMATION_TYPE_MASK = 0xff; private final Context mContext; private final TransactionPool mTransactionPool; @@ -82,7 +81,7 @@ class ScreenRotationAnimation { /** The leash of the changing window container. */ private final SurfaceControl mSurfaceControl; - private final int mAnimType; + private final int mAnimHint; private final int mStartWidth; private final int mStartHeight; private final int mEndWidth; @@ -99,12 +98,6 @@ class ScreenRotationAnimation { private SurfaceControl mBackColorSurface; /** The leash using to animate screenshot layer. */ private final SurfaceControl mAnimLeash; - /** - * The container with background color for {@link #mSurfaceControl}. It is only created if - * {@link #mSurfaceControl} may be translucent. E.g. visible wallpaper with alpha < 1 (dimmed). - * That prevents flickering of alpha blending. - */ - private SurfaceControl mBackEffectSurface; // The current active animation to move from the old to the new rotated // state. Which animation is run here will depend on the old and new @@ -122,7 +115,7 @@ class ScreenRotationAnimation { Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash, int animHint) { mContext = context; mTransactionPool = pool; - mAnimType = animHint & ANIMATION_TYPE_MASK; + mAnimHint = animHint; mSurfaceControl = change.getLeash(); mStartWidth = change.getStartAbsBounds().width(); @@ -177,20 +170,11 @@ class ScreenRotationAnimation { } hardwareBuffer.close(); } - if ((animHint & ANIMATION_HINT_HAS_WALLPAPER) != 0) { - mBackEffectSurface = new SurfaceControl.Builder() - .setCallsite("ShellRotationAnimation").setParent(rootLeash) - .setEffectLayer().setOpaque(true).setName("BackEffect").build(); - t.reparent(mSurfaceControl, mBackEffectSurface) - .setColor(mBackEffectSurface, - new float[] {mStartLuma, mStartLuma, mStartLuma}) - .show(mBackEffectSurface); - } t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE); t.show(mAnimLeash); // Crop the real content in case it contains a larger child layer, e.g. wallpaper. - t.setCrop(getEnterSurface(), new Rect(0, 0, mEndWidth, mEndHeight)); + t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight)); if (!isCustomRotate()) { mBackColorSurface = new SurfaceControl.Builder() @@ -215,12 +199,7 @@ class ScreenRotationAnimation { } private boolean isCustomRotate() { - return mAnimType == ROTATION_ANIMATION_CROSSFADE || mAnimType == ROTATION_ANIMATION_JUMPCUT; - } - - /** Returns the surface which contains the real content to animate enter. */ - private SurfaceControl getEnterSurface() { - return mBackEffectSurface != null ? mBackEffectSurface : mSurfaceControl; + return mAnimHint == ROTATION_ANIMATION_CROSSFADE || mAnimHint == ROTATION_ANIMATION_JUMPCUT; } private void setScreenshotTransform(SurfaceControl.Transaction t) { @@ -281,7 +260,7 @@ class ScreenRotationAnimation { final boolean customRotate = isCustomRotate(); if (customRotate) { mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, - mAnimType == ROTATION_ANIMATION_JUMPCUT ? R.anim.rotation_animation_jump_exit + mAnimHint == ROTATION_ANIMATION_JUMPCUT ? R.anim.rotation_animation_jump_exit : R.anim.rotation_animation_xfade_exit); mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, R.anim.rotation_animation_enter); @@ -335,11 +314,7 @@ class ScreenRotationAnimation { } else { startDisplayRotation(animations, finishCallback, mainExecutor); startScreenshotRotationAnimation(animations, finishCallback, mainExecutor); - if (mBackEffectSurface != null && mStartLuma > 0.1f) { - // Animate from the color of background to black for smooth alpha blending. - buildLumaAnimation(animations, mStartLuma, 0f /* endLuma */, mBackEffectSurface, - animationScale, finishCallback, mainExecutor); - } + //startColorAnimation(mTransaction, animationScale); } return true; @@ -347,7 +322,7 @@ class ScreenRotationAnimation { private void startDisplayRotation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { - buildSurfaceAnimation(animations, mRotateEnterAnimation, getEnterSurface(), finishCallback, + buildSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, null /* clipRect */, false /* isActivity */); } @@ -366,17 +341,40 @@ class ScreenRotationAnimation { null /* clipRect */, false /* isActivity */); } - private void buildLumaAnimation(@NonNull ArrayList<Animator> animations, - float startLuma, float endLuma, SurfaceControl surface, float animationScale, - @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { - final long durationMillis = (long) (mContext.getResources().getInteger( - R.integer.config_screen_rotation_color_transition) * animationScale); - final LumaAnimation animation = new LumaAnimation(durationMillis); - // Align the end with the enter animation. - animation.setStartOffset(mRotateEnterAnimation.getDuration() - durationMillis); - final LumaAnimationAdapter adapter = new LumaAnimationAdapter(surface, startLuma, endLuma); - buildSurfaceAnimation(animations, animation, finishCallback, mTransactionPool, - mainExecutor, adapter); + private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) { + int colorTransitionMs = mContext.getResources().getInteger( + R.integer.config_screen_rotation_color_transition); + final float[] rgbTmpFloat = new float[3]; + final int startColor = Color.rgb(mStartLuma, mStartLuma, mStartLuma); + final int endColor = Color.rgb(mEndLuma, mEndLuma, mEndLuma); + final long duration = colorTransitionMs * (long) animationScale; + final Transaction t = mTransactionPool.acquire(); + + final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); + // Animation length is already expected to be scaled. + va.overrideDurationScale(1.0f); + va.setDuration(duration); + va.addUpdateListener(animation -> { + final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); + final float fraction = currentPlayTime / va.getDuration(); + applyColor(startColor, endColor, rgbTmpFloat, fraction, mBackColorSurface, t); + }); + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface, + t); + mTransactionPool.release(t); + } + + @Override + public void onAnimationEnd(Animator animation) { + applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface, + t); + mTransactionPool.release(t); + } + }); + animExecutor.execute(va::start); } public void kill() { @@ -391,47 +389,21 @@ class ScreenRotationAnimation { if (mBackColorSurface != null && mBackColorSurface.isValid()) { t.remove(mBackColorSurface); } - if (mBackEffectSurface != null && mBackEffectSurface.isValid()) { - t.remove(mBackEffectSurface); - } t.apply(); mTransactionPool.release(t); } - /** A no-op wrapper to provide animation duration. */ - private static class LumaAnimation extends Animation { - LumaAnimation(long durationMillis) { - setDuration(durationMillis); - } - } - - private static class LumaAnimationAdapter extends DefaultTransitionHandler.AnimationAdapter { - final float[] mColorArray = new float[3]; - final float mStartLuma; - final float mEndLuma; - final AccelerateInterpolator mInterpolation; - - LumaAnimationAdapter(@NonNull SurfaceControl leash, float startLuma, float endLuma) { - super(leash); - mStartLuma = startLuma; - mEndLuma = endLuma; - // Make the initial progress color lighter if the background is light. That avoids - // darker content when fading into the entering surface. - final float factor = Math.min(3f, (Math.max(0.5f, mStartLuma) - 0.5f) * 10); - Slog.d(TAG, "Luma=" + mStartLuma + " factor=" + factor); - mInterpolation = factor > 0.5f ? new AccelerateInterpolator(factor) : null; - } - - @Override - void applyTransformation(ValueAnimator animator) { - final float fraction = mInterpolation != null - ? mInterpolation.getInterpolation(animator.getAnimatedFraction()) - : animator.getAnimatedFraction(); - final float luma = mStartLuma + fraction * (mEndLuma - mStartLuma); - mColorArray[0] = luma; - mColorArray[1] = luma; - mColorArray[2] = luma; - mTransaction.setColor(mLeash, mColorArray); + private static void applyColor(int startColor, int endColor, float[] rgbFloat, + float fraction, SurfaceControl surface, SurfaceControl.Transaction t) { + final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor, + endColor); + Color middleColor = Color.valueOf(color); + rgbFloat[0] = middleColor.red(); + rgbFloat[1] = middleColor.green(); + rgbFloat[2] = middleColor.blue(); + if (surface.isValid()) { + t.setColor(surface, rgbFloat); } + t.apply(); } } 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 1537a1ea4d45..23f8e6ef1596 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 @@ -610,7 +610,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin || !Flags.enableHandleInputFix()) { return; } - ((AppHandleViewHolder) mWindowDecorViewHolder).disposeStatusBarInputLayer(); + asAppHandle(mWindowDecorViewHolder).disposeStatusBarInputLayer(); } private WindowDecorationViewHolder createViewHolder() { @@ -647,6 +647,22 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return viewHolder instanceof AppHandleViewHolder; } + @Nullable + private AppHandleViewHolder asAppHandle(WindowDecorationViewHolder viewHolder) { + if (viewHolder instanceof AppHandleViewHolder) { + return (AppHandleViewHolder) viewHolder; + } + return null; + } + + @Nullable + private AppHeaderViewHolder asAppHeader(WindowDecorationViewHolder viewHolder) { + if (viewHolder instanceof AppHeaderViewHolder) { + return (AppHeaderViewHolder) viewHolder; + } + return null; + } + @VisibleForTesting static void updateRelayoutParams( RelayoutParams relayoutParams, @@ -1025,7 +1041,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ void closeMaximizeMenu() { if (!isMaximizeMenuActive()) return; - mMaximizeMenu.close(); + mMaximizeMenu.close(() -> { + // Request the accessibility service to refocus on the maximize button after closing + // the menu. + final AppHeaderViewHolder appHeader = asAppHeader(mWindowDecorViewHolder); + if (appHeader != null) { + appHeader.requestAccessibilityFocus(); + } + return Unit.INSTANCE; + }); mMaximizeMenu = null; } @@ -1408,7 +1432,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin void setAnimatingTaskResizeOrReposition(boolean animatingTaskResizeOrReposition) { if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) return; - ((AppHeaderViewHolder) mWindowDecorViewHolder) + asAppHeader(mWindowDecorViewHolder) .setAnimatingTaskResizeOrReposition(animatingTaskResizeOrReposition); } @@ -1416,16 +1440,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button. */ void onMaximizeButtonHoverExit() { - ((AppHeaderViewHolder) mWindowDecorViewHolder) - .onMaximizeWindowHoverExit(); + asAppHeader(mWindowDecorViewHolder).onMaximizeWindowHoverExit(); } /** * Called when there is a {@link MotionEvent#ACTION_HOVER_ENTER} on the maximize window button. */ void onMaximizeButtonHoverEnter() { - ((AppHeaderViewHolder) mWindowDecorViewHolder) - .onMaximizeWindowHoverEnter(); + asAppHeader(mWindowDecorViewHolder).onMaximizeWindowHoverEnter(); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index 5b3f26318825..9a5b4f54dd36 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -23,8 +23,6 @@ import android.content.Context import android.content.res.ColorStateList import android.content.res.Resources import android.graphics.Bitmap -import android.graphics.BlendMode -import android.graphics.BlendModeColorFilter import android.graphics.Point import android.graphics.PointF import android.graphics.Rect @@ -568,9 +566,7 @@ class HandleMenu( appIconBitmap: Bitmap?, appName: CharSequence? ) { - appInfoPill.background.colorFilter = BlendModeColorFilter( - style.backgroundColor, BlendMode.MULTIPLY - ) + appInfoPill.background.setTint(style.backgroundColor) collapseMenuButton.apply { imageTintList = ColorStateList.valueOf(style.textColor) @@ -584,9 +580,7 @@ class HandleMenu( } private fun bindWindowingPill(style: MenuStyle) { - windowingPill.background.colorFilter = BlendModeColorFilter( - style.backgroundColor, BlendMode.MULTIPLY - ) + windowingPill.background.setTint(style.backgroundColor) // TODO: Remove once implemented. floatingBtn.visibility = View.GONE @@ -612,23 +606,19 @@ class HandleMenu( } screenshotBtn.apply { isGone = !SHOULD_SHOW_SCREENSHOT_BUTTON - background.colorFilter = - BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY - ) + background.setTint(style.backgroundColor) setTextColor(style.textColor) compoundDrawableTintList = ColorStateList.valueOf(style.textColor) } newWindowBtn.apply { isGone = !shouldShowNewWindowButton - background.colorFilter = - BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY) + background.setTint(style.backgroundColor) setTextColor(style.textColor) compoundDrawableTintList = ColorStateList.valueOf(style.textColor) } manageWindowBtn.apply { isGone = !shouldShowManageWindowsButton - background.colorFilter = - BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY) + background.setTint(style.backgroundColor) setTextColor(style.textColor) compoundDrawableTintList = ColorStateList.valueOf(style.textColor) } @@ -637,9 +627,7 @@ class HandleMenu( private fun bindOpenInBrowserPill(style: MenuStyle) { openInBrowserPill.apply { isGone = !shouldShowBrowserPill - background.colorFilter = BlendModeColorFilter( - style.backgroundColor, BlendMode.MULTIPLY - ) + background.setTint(style.backgroundColor) } browserBtn.apply { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt index 9590ccdc3b97..0c475f12f53b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt @@ -26,6 +26,7 @@ import android.view.View.SCALE_Y import android.view.View.TRANSLATION_Y import android.view.View.TRANSLATION_Z import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent import android.widget.Button import androidx.core.animation.doOnEnd import androidx.core.view.children @@ -83,7 +84,12 @@ class HandleMenuAnimator( animateWindowingPillOpen() animateMoreActionsPillOpen() animateOpenInBrowserPill() - runAnimations() + runAnimations { + appInfoPill.post { + appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent( + AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } } /** @@ -98,7 +104,12 @@ class HandleMenuAnimator( animateWindowingPillOpen() animateMoreActionsPillOpen() animateOpenInBrowserPill() - runAnimations() + runAnimations { + appInfoPill.post { + appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent( + AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 9c73e4a38aa9..0cb219ae4b81 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -51,6 +51,7 @@ import android.view.View.TRANSLATION_Z import android.view.ViewGroup import android.view.WindowManager import android.view.WindowlessWindowManager +import android.view.accessibility.AccessibilityEvent import android.widget.Button import android.widget.TextView import android.window.TaskConstants @@ -116,19 +117,24 @@ class MaximizeMenu( onHoverListener = onHoverListener, onOutsideTouchListener = onOutsideTouchListener ) - maximizeMenuView?.animateOpenMenu() + maximizeMenuView?.let { view -> + view.animateOpenMenu(onEnd = { + view.requestAccessibilityFocus() + }) + } } /** Closes the maximize window and releases its view. */ - fun close() { + fun close(onEnd: () -> Unit) { val view = maximizeMenuView val menu = maximizeMenu if (view == null) { menu?.releaseView() } else { - view.animateCloseMenu { + view.animateCloseMenu(onEnd = { menu?.releaseView() - } + onEnd.invoke() + }) } maximizeMenu = null maximizeMenuView = null @@ -351,7 +357,7 @@ class MaximizeMenu( } /** Animate the opening of the menu */ - fun animateOpenMenu() { + fun animateOpenMenu(onEnd: () -> Unit) { maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null) menuAnimatorSet = AnimatorSet() @@ -419,6 +425,7 @@ class MaximizeMenu( onEnd = { maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + onEnd.invoke() } ) menuAnimatorSet?.start() @@ -499,6 +506,14 @@ class MaximizeMenu( menuAnimatorSet?.start() } + /** Request that the accessibility service focus on the menu. */ + fun requestAccessibilityFocus() { + // Focus the first button in the menu by default. + maximizeButton.post { + maximizeButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } + /** Cancel the menu animation. */ private fun cancelAnimation() { menuAnimatorSet?.cancel() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index af6a819bb705..e9961655d979 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -30,6 +30,7 @@ import android.graphics.drawable.shapes.RoundRectShape import android.view.View import android.view.View.OnLongClickListener import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.view.accessibility.AccessibilityEvent import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView @@ -263,7 +264,11 @@ class AppHeaderViewHolder( override fun onHandleMenuOpened() {} - override fun onHandleMenuClosed() {} + override fun onHandleMenuClosed() { + openMenuButton.post { + openMenuButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } fun setAnimatingTaskResizeOrReposition(animatingTaskResizeOrReposition: Boolean) { // If animating a task resize or reposition, cancel any running hover animations @@ -309,6 +314,12 @@ class AppHeaderViewHolder( ) } + fun requestAccessibilityFocus() { + maximizeWindowButton.post { + maximizeWindowButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } + private fun getHeaderStyle(header: Header): HeaderStyle { return HeaderStyle( background = getHeaderBackground(header), diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt index 717ea306eb77..ce235d445fe5 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt @@ -72,7 +72,6 @@ open class StartAppMediaProjectionWithMaxDesktopWindows { @Test open fun startMediaProjection() { - // TODO(b/366455106) - handle max task Limit mediaProjectionAppHelper.startSingleAppMediaProjection(wmHelper, targetApp) mailApp.launchViaIntent(wmHelper) simpleApp.launchViaIntent(wmHelper) diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithDisplayRotations.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithDisplayRotations.kt index 1573b58853da..f5fb4cec5535 100644 --- a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithDisplayRotations.kt +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithDisplayRotations.kt @@ -20,13 +20,12 @@ import android.app.Instrumentation import android.platform.test.annotations.Postsubmit import android.tools.NavBar import android.tools.Rotation -import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.server.wm.flicker.helpers.DesktopModeAppHelper import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper import com.android.wm.shell.Utils import org.junit.After @@ -47,8 +46,7 @@ open class StartAppMediaProjectionWithDisplayRotations { private val initialRotation = Rotation.ROTATION_0 private val targetApp = CalculatorAppHelper(instrumentation) - private val mediaProjectionAppHelper = StartMediaProjectionAppHelper(instrumentation) - private val testApp = DesktopModeAppHelper(mediaProjectionAppHelper) + private val testApp = StartMediaProjectionAppHelper(instrumentation) @Rule @JvmField @@ -63,7 +61,7 @@ open class StartAppMediaProjectionWithDisplayRotations { @Test open fun startMediaProjectionAndRotate() { - mediaProjectionAppHelper.startSingleAppMediaProjection(wmHelper, targetApp) + testApp.startSingleAppMediaProjection(wmHelper, targetApp) wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_90) diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithDisplayRotations.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithDisplayRotations.kt index e80a895c1aa6..28f3cc758c22 100644 --- a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithDisplayRotations.kt +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithDisplayRotations.kt @@ -25,7 +25,6 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.server.wm.flicker.helpers.DesktopModeAppHelper import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper import com.android.wm.shell.Utils import org.junit.After @@ -45,8 +44,7 @@ open class StartScreenMediaProjectionWithDisplayRotations { val device = UiDevice.getInstance(instrumentation) private val initialRotation = Rotation.ROTATION_0 - private val mediaProjectionAppHelper = StartMediaProjectionAppHelper(instrumentation) - private val testApp = DesktopModeAppHelper(mediaProjectionAppHelper) + private val testApp = StartMediaProjectionAppHelper(instrumentation) @Rule @JvmField @@ -60,7 +58,7 @@ open class StartScreenMediaProjectionWithDisplayRotations { @Test open fun startMediaProjectionAndRotate() { - mediaProjectionAppHelper.startEntireScreenMediaProjection(wmHelper) + testApp.startEntireScreenMediaProjection(wmHelper) wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_90) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index e514dc38208e..f01ed84adc74 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -39,6 +39,7 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.never; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; @@ -599,6 +600,18 @@ public class ShellTaskOrganizerTests extends ShellTestCase { } @Test + public void testRecentTasks_visibilityChanges_notFreeForm_shouldNotNotifyTaskController() { + RunningTaskInfo task1_visible = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); + mOrganizer.onTaskAppeared(task1_visible, /* leash= */ null); + RunningTaskInfo task1_hidden = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); + task1_hidden.isVisible = false; + + mOrganizer.onTaskInfoChanged(task1_hidden); + + verify(mRecentTasksController, never()).onTaskRunningInfoChanged(task1_hidden); + } + + @Test public void testRecentTasks_windowingModeChanges_shouldNotifyTaskController() { RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); mOrganizer.onTaskAppeared(task1, /* leash= */ null); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt index b14f1633e8fd..628c9cdd9339 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt @@ -41,12 +41,22 @@ import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.TaskStackListenerImpl import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask +import com.android.wm.shell.desktopmode.persistence.Desktop +import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import junit.framework.Assert.assertEquals import junit.framework.Assert.assertTrue import kotlin.test.assertNotNull +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before import org.junit.Rule @@ -73,6 +83,7 @@ import org.mockito.quality.Strictness */ @SmallTest @RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE, FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE) class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @JvmField @Rule val setFlagsRule = SetFlagsRule() @@ -82,16 +93,19 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Mock lateinit var transitions: Transitions @Mock lateinit var resizeTransitionHandler: ToggleResizeDesktopTaskTransitionHandler @Mock lateinit var taskStackListener: TaskStackListenerImpl + @Mock lateinit var persistentRepository: DesktopPersistentRepository private lateinit var mockitoSession: StaticMockitoSession private lateinit var handler: DesktopActivityOrientationChangeHandler private lateinit var shellInit: ShellInit private lateinit var taskRepository: DesktopModeTaskRepository + private lateinit var testScope: CoroutineScope // Mock running tasks are registered here so we can get the list from mock shell task organizer. private val runningTasks = mutableListOf<RunningTaskInfo>() @Before fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) mockitoSession = mockitoSession() .strictness(Strictness.LENIENT) @@ -99,10 +113,15 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { .startMocking() doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) - taskRepository = DesktopModeTaskRepository() + taskRepository = + DesktopModeTaskRepository(context, shellInit, persistentRepository, testScope) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( + Desktop.getDefaultInstance() + ) handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer, taskStackListener, resizeTransitionHandler, taskRepository) @@ -115,6 +134,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { mockitoSession.finishMocking() runningTasks.clear() + testScope.cancel() } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index d3404f7bd261..bc40d89009bc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -17,27 +17,70 @@ package com.android.wm.shell.desktopmode import android.graphics.Rect +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner +import android.util.ArraySet import android.view.Display.DEFAULT_DISPLAY import android.view.Display.INVALID_DISPLAY import androidx.test.filters.SmallTest +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.desktopmode.persistence.Desktop +import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat import junit.framework.Assert.fail +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.spy +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi class DesktopModeTaskRepositoryTest : ShellTestCase() { private lateinit var repo: DesktopModeTaskRepository + private lateinit var shellInit: ShellInit + private lateinit var datastoreScope: CoroutineScope + + @Mock private lateinit var testExecutor: ShellExecutor + @Mock private lateinit var persistentRepository: DesktopPersistentRepository @Before fun setUp() { - repo = DesktopModeTaskRepository() + Dispatchers.setMain(StandardTestDispatcher()) + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + shellInit = spy(ShellInit(testExecutor)) + + repo = DesktopModeTaskRepository(context, shellInit, persistentRepository, datastoreScope) + whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( + Desktop.getDefaultInstance() + ) + shellInit.init() + } + + @After + fun tearDown() { + datastoreScope.cancel() } @Test @@ -455,6 +498,44 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun addOrMoveFreeformTaskToTop_noTaskExists_persistenceEnabled_addsToTop() = + runTest(StandardTestDispatcher()) { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(tasks).containsExactly(7, 6, 5).inOrder() + inOrder(persistentRepository).run { + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(5) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(6, 5) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(7, 6, 5) + ) + } + } + + @Test fun addOrMoveFreeformTaskToTop_alreadyExists_movesToTop() { repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) @@ -480,6 +561,55 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun minimizeTask_persistenceEnabled_taskIsPersistedAsMinimized() = + runTest(StandardTestDispatcher()) { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + + repo.minimizeTask(displayId = 0, taskId = 6) + + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(tasks).containsExactly(7, 6, 5).inOrder() + assertThat(repo.isMinimizedTask(taskId = 6)).isTrue() + inOrder(persistentRepository).run { + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(5) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(6, 5) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(7, 6, 5) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(arrayOf(6)), + freeformTasksInZOrder = arrayListOf(7, 6, 5) + ) + } + } + + @Test fun addOrMoveFreeformTaskToTop_taskIsUnminimized_noop() { repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) @@ -503,6 +633,33 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun removeFreeformTask_invalidDisplay_persistenceEnabled_removesTaskFromFreeformTasks() { + runTest(StandardTestDispatcher()) { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + + repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1) + + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(1) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = ArrayList() + ) + } + } + + @Test fun removeFreeformTask_validDisplay_removesTaskFromFreeformTasks() { repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) @@ -513,6 +670,33 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun removeFreeformTask_validDisplay_persistenceEnabled_removesTaskFromFreeformTasks() { + runTest(StandardTestDispatcher()) { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + + repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1) + + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(1) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = ArrayList() + ) + } + } + + @Test fun removeFreeformTask_validDisplay_differentDisplay_doesNotRemovesTask() { repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) @@ -523,6 +707,33 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun removeFreeformTask_validDisplayButDifferentDisplay_persistenceEnabled_doesNotRemoveTask() { + runTest(StandardTestDispatcher()) { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + + repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1) + + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(1) + ) + verify(persistentRepository, never()) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = ArrayList() + ) + } + } + + @Test fun removeFreeformTask_removesTaskBoundsBeforeMaximize() { val taskId = 1 repo.addActiveTask(THIRD_DISPLAY, taskId) @@ -709,5 +920,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { companion object { const val SECOND_DISPLAY = 1 const val THIRD_DISPLAY = 345 + private const val DEFAULT_USER_ID = 1000 + private const val DEFAULT_DESKTOP_ID = 0 } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 8f20841e76b3..ee545209904f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -93,6 +93,8 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreef import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplitScreenTask +import com.android.wm.shell.desktopmode.persistence.Desktop +import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.recents.RecentTasksController import com.android.wm.shell.recents.RecentsTransitionHandler @@ -117,6 +119,14 @@ import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assume.assumeTrue import org.junit.Before @@ -148,6 +158,7 @@ import org.mockito.quality.Strictness */ @SmallTest @RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) class DesktopTasksControllerTest : ShellTestCase() { @@ -183,6 +194,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock private lateinit var mockSurface: SurfaceControl @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener @Mock private lateinit var mockHandler: Handler + @Mock lateinit var persistentRepository: DesktopPersistentRepository private lateinit var mockitoSession: StaticMockitoSession private lateinit var controller: DesktopTasksController @@ -190,6 +202,7 @@ class DesktopTasksControllerTest : ShellTestCase() { private lateinit var taskRepository: DesktopModeTaskRepository private lateinit var desktopTasksLimiter: DesktopTasksLimiter private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener + private lateinit var testScope: CoroutineScope private val shellExecutor = TestShellExecutor() @@ -207,6 +220,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Before fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) mockitoSession = mockitoSession() .strictness(Strictness.LENIENT) @@ -214,8 +228,9 @@ class DesktopTasksControllerTest : ShellTestCase() { .startMocking() doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) - taskRepository = DesktopModeTaskRepository() + taskRepository = DesktopModeTaskRepository(context, shellInit, persistentRepository, testScope) desktopTasksLimiter = DesktopTasksLimiter( transitions, @@ -233,6 +248,9 @@ class DesktopTasksControllerTest : ShellTestCase() { whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> (i.arguments.first() as Rect).set(STABLE_BOUNDS) } + whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( + Desktop.getDefaultInstance() + ) val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN @@ -287,6 +305,7 @@ class DesktopTasksControllerTest : ShellTestCase() { mockitoSession.finishMocking() runningTasks.clear() + testScope.cancel() } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index 61d03cac035c..045e07796cb8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -35,13 +35,23 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_MINIMIZE_WINDOW import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask +import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.TransitionInfoBuilder import com.android.wm.shell.transition.Transitions import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import kotlin.test.assertFailsWith +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before import org.junit.Rule @@ -49,6 +59,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.any +import org.mockito.Mockito.spy import org.mockito.Mockito.`when` import org.mockito.kotlin.eq import org.mockito.kotlin.verify @@ -62,6 +73,7 @@ import org.mockito.quality.Strictness */ @SmallTest @RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi class DesktopTasksLimiterTest : ShellTestCase() { @JvmField @@ -72,19 +84,26 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Mock lateinit var transitions: Transitions @Mock lateinit var interactionJankMonitor: InteractionJankMonitor @Mock lateinit var handler: Handler + @Mock lateinit var testExecutor: ShellExecutor + @Mock lateinit var persistentRepository: DesktopPersistentRepository private lateinit var mockitoSession: StaticMockitoSession private lateinit var desktopTasksLimiter: DesktopTasksLimiter private lateinit var desktopTaskRepo: DesktopModeTaskRepository + private lateinit var shellInit: ShellInit + private lateinit var testScope: CoroutineScope @Before fun setUp() { mockitoSession = ExtendedMockito.mockitoSession().strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java).startMocking() doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) } + shellInit = spy(ShellInit(testExecutor)) + Dispatchers.setMain(StandardTestDispatcher()) + testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) - desktopTaskRepo = DesktopModeTaskRepository() - + desktopTaskRepo = + DesktopModeTaskRepository(context, shellInit, persistentRepository, testScope) desktopTasksLimiter = DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, MAX_TASK_LIMIT, interactionJankMonitor, mContext, handler) @@ -93,6 +112,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { @After fun tearDown() { mockitoSession.finishMocking() + testScope.cancel() } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt new file mode 100644 index 000000000000..9b9703fdf6dc --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt @@ -0,0 +1,198 @@ +/* + * 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.wm.shell.desktopmode.persistence + +import android.content.Context +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.util.ArraySet +import android.view.Display.DEFAULT_DISPLAY +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE +import com.android.wm.shell.ShellTestCase +import com.google.common.truth.Truth.assertThat +import java.io.File +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi +@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE, FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) +class DesktopPersistentRepositoryTest : ShellTestCase() { + private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext + private lateinit var testDatastore: DataStore<DesktopPersistentRepositories> + private lateinit var datastoreRepository: DesktopPersistentRepository + private lateinit var datastoreScope: CoroutineScope + + @Before + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + testDatastore = + DataStoreFactory.create( + serializer = + DesktopPersistentRepository.Companion.DesktopPersistentRepositoriesSerializer, + scope = datastoreScope) { + testContext.dataStoreFile(DESKTOP_REPOSITORY_STATES_DATASTORE_TEST_FILE) + } + datastoreRepository = DesktopPersistentRepository(testDatastore) + } + + @After + fun tearDown() { + File(ApplicationProvider.getApplicationContext<Context>().filesDir, "datastore") + .deleteRecursively() + + datastoreScope.cancel() + } + + @Test + fun readRepository_returnsCorrectDesktop() { + runTest(StandardTestDispatcher()) { + val task = createDesktopTask(1) + val desk = createDesktop(task) + val repositoryState = + DesktopRepositoryState.newBuilder().putDesktop(DEFAULT_DESKTOP_ID, desk) + val DesktopPersistentRepositories = + DesktopPersistentRepositories.newBuilder() + .putDesktopRepoByUser(DEFAULT_USER_ID, repositoryState.build()) + .build() + testDatastore.updateData { DesktopPersistentRepositories } + + val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) + + assertThat(actualDesktop).isEqualTo(desk) + } + } + + @Test + fun addOrUpdateTask_addNewTaskToDesktop() { + runTest(StandardTestDispatcher()) { + // Create a basic repository state + val task = createDesktopTask(1) + val DesktopPersistentRepositories = createRepositoryWithOneDesk(task) + testDatastore.updateData { DesktopPersistentRepositories } + // Create a new state to be initialized + val visibleTasks = ArraySet(listOf(1, 2)) + val minimizedTasks = ArraySet<Int>() + val freeformTasksInZOrder = ArrayList(listOf(2, 1)) + + // Update with new state + datastoreRepository.addOrUpdateDesktop( + visibleTasks = visibleTasks, + minimizedTasks = minimizedTasks, + freeformTasksInZOrder = freeformTasksInZOrder) + + val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) + assertThat(actualDesktop.tasksByTaskIdMap).hasSize(2) + assertThat(actualDesktop.getZOrderedTasks(0)).isEqualTo(2) + } + } + + @Test + fun addOrUpdateTask_changeTaskStateToMinimize_taskStateIsMinimized() { + runTest(StandardTestDispatcher()) { + val task = createDesktopTask(1) + val DesktopPersistentRepositories = createRepositoryWithOneDesk(task) + testDatastore.updateData { DesktopPersistentRepositories } + // Create a new state to be initialized + val visibleTasks = ArraySet(listOf(1)) + val minimizedTasks = ArraySet(listOf(1)) + val freeformTasksInZOrder = ArrayList(listOf(1)) + + // Update with new state + datastoreRepository.addOrUpdateDesktop( + visibleTasks = visibleTasks, + minimizedTasks = minimizedTasks, + freeformTasksInZOrder = freeformTasksInZOrder) + + val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) + assertThat(actualDesktop.tasksByTaskIdMap[task.taskId]?.desktopTaskState) + .isEqualTo(DesktopTaskState.MINIMIZED) + } + } + + @Test + fun removeTask_previouslyAddedTaskIsRemoved() { + runTest(StandardTestDispatcher()) { + val task = createDesktopTask(1) + val DesktopPersistentRepositories = createRepositoryWithOneDesk(task) + testDatastore.updateData { DesktopPersistentRepositories } + // Create a new state to be initialized + val visibleTasks = ArraySet<Int>() + val minimizedTasks = ArraySet<Int>() + val freeformTasksInZOrder = ArrayList<Int>() + + // Update with new state + datastoreRepository.addOrUpdateDesktop( + visibleTasks = visibleTasks, + minimizedTasks = minimizedTasks, + freeformTasksInZOrder = freeformTasksInZOrder) + + val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) + assertThat(actualDesktop.tasksByTaskIdMap).isEmpty() + assertThat(actualDesktop.zOrderedTasksList).isEmpty() + } + } + + private companion object { + const val DESKTOP_REPOSITORY_STATES_DATASTORE_TEST_FILE = "desktop_repo_test.pb" + const val DEFAULT_USER_ID = 1000 + const val DEFAULT_DESKTOP_ID = 0 + + fun createRepositoryWithOneDesk(task: DesktopTask): DesktopPersistentRepositories { + val desk = createDesktop(task) + val repositoryState = + DesktopRepositoryState.newBuilder().putDesktop(DEFAULT_DESKTOP_ID, desk) + val DesktopPersistentRepositories = + DesktopPersistentRepositories.newBuilder() + .putDesktopRepoByUser(DEFAULT_USER_ID, repositoryState.build()) + .build() + return DesktopPersistentRepositories + } + + fun createDesktop(task: DesktopTask): Desktop? = + Desktop.newBuilder() + .setDisplayId(DEFAULT_DISPLAY) + .addZOrderedTasks(task.taskId) + .putTasksByTaskId(task.taskId, task) + .build() + + fun createDesktopTask( + taskId: Int, + state: DesktopTaskState = DesktopTaskState.VISIBLE + ): DesktopTask = + DesktopTask.newBuilder().setTaskId(taskId).setDesktopTaskState(state).build() + } +} 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 a1867f3698fc..9c11ec34ef44 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 @@ -623,7 +623,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { .postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS)); mCloseMaxMenuRunnable.getValue().run(); - verify(menu).close(); + verify(menu).close(any()); assertFalse(decoration.isMaximizeMenuActive()); } @@ -642,7 +642,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { .postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS)); mCloseMaxMenuRunnable.getValue().run(); - verify(menu).close(); + verify(menu).close(any()); assertFalse(decoration.isMaximizeMenuActive()); } diff --git a/media/java/android/media/flags/editing.aconfig b/media/java/android/media/flags/editing.aconfig index bf6ec9635912..185f579df4b9 100644 --- a/media/java/android/media/flags/editing.aconfig +++ b/media/java/android/media/flags/editing.aconfig @@ -8,3 +8,10 @@ flag { description: "Add media metrics for transcoding/editing events." bug: "297487694" } + +flag { + name: "stagefrightrecorder_enable_b_frames" + namespace: "media_solutions" + description: "Enable B frames for Stagefright recorder." + bug: "341121900" +} diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_chevron.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_chevron.xml new file mode 100644 index 000000000000..16ca18ae2200 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_chevron.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="18dp" + android:height="18dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="@color/settingslib_materialColorOnSurfaceVariant" + android:autoMirrored="true"> + <path + android:fillColor="@android:color/white" + android:pathData="M321,880L250,809L579,480L250,151L321,80L721,480L321,880Z"/> +</vector>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_two_target_divider.xml b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_two_target_divider.xml new file mode 100644 index 000000000000..3f751812ee40 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_two_target_divider.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/two_target_divider" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="start|center_vertical" + android:orientation="horizontal" + android:paddingStart="?android:attr/listPreferredItemPaddingEnd" + android:paddingLeft="?android:attr/listPreferredItemPaddingEnd" + android:paddingVertical="@dimen/settingslib_expressive_space_extrasmall7"> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingEnd="@dimen/settingslib_expressive_space_extrasmall6" + android:src="@drawable/settingslib_expressive_icon_chevron"/> + + <View + android:layout_width="1dp" + android:layout_height="40dp" + android:background="?android:attr/listDivider" /> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts index 3011ce05c3a5..b69912a3fd36 100644 --- a/packages/SettingsLib/Spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/build.gradle.kts @@ -29,7 +29,7 @@ val androidTop: String = File(rootDir, "../../../../..").canonicalPath allprojects { extra["androidTop"] = androidTop - extra["jetpackComposeVersion"] = "1.7.0-rc01" + extra["jetpackComposeVersion"] = "1.7.0" } subprojects { diff --git a/packages/SettingsLib/Spa/gallery/res/values/strings.xml b/packages/SettingsLib/Spa/gallery/res/values/strings.xml index 18a6db035070..f942fd0662a5 100644 --- a/packages/SettingsLib/Spa/gallery/res/values/strings.xml +++ b/packages/SettingsLib/Spa/gallery/res/values/strings.xml @@ -26,6 +26,8 @@ <string name="single_line_summary_preference_summary" translatable="false">A very long summary to show case a preference which only shows a single line summary.</string> <!-- Footer text with two links. [DO NOT TRANSLATE] --> <string name="footer_with_two_links" translatable="false">Annotated string with <a href="https://www.android.com/">link 1</a> and <a href="https://source.android.com/">link 2</a>.</string> + <!-- TopIntroPreference preview text. [DO NOT TRANSLATE] --> + <string name="label_with_two_links" translatable="false"><a href="https://www.android.com/">Label</a></string> <!-- Sample title --> <string name="sample_title" translatable="false">Lorem ipsum</string> diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index 83d657ef380d..7139f5b468ca 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -42,12 +42,15 @@ import com.android.settingslib.spa.gallery.page.LoadingBarPageProvider import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider import com.android.settingslib.spa.gallery.scaffold.NonScrollablePagerPageProvider import com.android.settingslib.spa.gallery.page.SliderPageProvider +import com.android.settingslib.spa.gallery.preference.IntroPreferencePageProvider import com.android.settingslib.spa.gallery.preference.ListPreferencePageProvider import com.android.settingslib.spa.gallery.preference.MainSwitchPreferencePageProvider import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider import com.android.settingslib.spa.gallery.preference.PreferencePageProvider import com.android.settingslib.spa.gallery.preference.SwitchPreferencePageProvider +import com.android.settingslib.spa.gallery.preference.TopIntroPreferencePageProvider import com.android.settingslib.spa.gallery.preference.TwoTargetSwitchPreferencePageProvider +import com.android.settingslib.spa.gallery.preference.ZeroStatePreferencePageProvider import com.android.settingslib.spa.gallery.scaffold.PagerMainPageProvider import com.android.settingslib.spa.gallery.scaffold.SearchScaffoldPageProvider import com.android.settingslib.spa.gallery.scaffold.SuwScaffoldPageProvider @@ -82,6 +85,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { MainSwitchPreferencePageProvider, ListPreferencePageProvider, TwoTargetSwitchPreferencePageProvider, + ZeroStatePreferencePageProvider, ArgumentPageProvider, SliderPageProvider, SpinnerPageProvider, @@ -109,6 +113,8 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { SuwScaffoldPageProvider, BannerPageProvider, CopyablePageProvider, + IntroPreferencePageProvider, + TopIntroPreferencePageProvider, ), rootPages = listOf( HomePageProvider.createSettingsPage(), diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt new file mode 100644 index 000000000000..603fceed9900 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt @@ -0,0 +1,78 @@ +/* + * 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.settingslib.spa.gallery.preference + +import android.os.Bundle +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AirplanemodeActive +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.android.settingslib.spa.framework.common.SettingsEntry +import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.widget.preference.IntroPreference +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel + +private const val TITLE = "Sample IntroPreference" + +object IntroPreferencePageProvider : SettingsPageProvider { + override val name = "IntroPreference" + private val owner = createSettingsPage() + + override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { + val entryList = mutableListOf<SettingsEntry>() + entryList.add( + SettingsEntryBuilder.create("IntroPreference", owner) + .setUiLayoutFn { SampleIntroPreference() } + .build() + ) + + return entryList + } + + fun buildInjectEntry(): SettingsEntryBuilder { + return SettingsEntryBuilder.createInject(owner).setUiLayoutFn { + Preference( + object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + } + ) + } + } + + override fun getTitle(arguments: Bundle?): String { + return TITLE + } +} + +@Composable +private fun SampleIntroPreference() { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + IntroPreference( + title = "Preferred network type", + descriptions = listOf("Description"), + imageVector = Icons.Outlined.AirplanemodeActive, + ) + } +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt index ce9678bab684..1626b025e2f7 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt @@ -39,6 +39,9 @@ object PreferenceMainPageProvider : SettingsPageProvider { ListPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), TwoTargetSwitchPreferencePageProvider.buildInjectEntry() .setLink(fromPage = owner).build(), + ZeroStatePreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + IntroPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + TopIntroPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ) } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt new file mode 100644 index 000000000000..b251266e0574 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt @@ -0,0 +1,83 @@ +/* + * 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.settingslib.spa.gallery.preference + +import android.os.Bundle +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.android.settingslib.spa.gallery.R +import com.android.settingslib.spa.framework.common.SettingsEntry +import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.preference.TopIntroPreference +import com.android.settingslib.spa.widget.preference.TopIntroPreferenceModel + +private const val TITLE = "Sample TopIntroPreference" + +object TopIntroPreferencePageProvider : SettingsPageProvider { + override val name = "TopIntroPreference" + private val owner = createSettingsPage() + + override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { + val entryList = mutableListOf<SettingsEntry>() + entryList.add( + SettingsEntryBuilder.create("TopIntroPreference", owner) + .setUiLayoutFn { SampleTopIntroPreference() } + .build() + ) + + return entryList + } + + fun buildInjectEntry(): SettingsEntryBuilder { + return SettingsEntryBuilder.createInject(owner).setUiLayoutFn { + Preference( + object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + } + ) + } + } + + override fun getTitle(arguments: Bundle?): String { + return TITLE + } +} + +@Composable +private fun SampleTopIntroPreference() { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + TopIntroPreference( + object : TopIntroPreferenceModel { + override val text = + "Additional text needed for the page. This can sit on the right side of the screen in 2 column.\n" + + "Example collapsed text area that you will not see until you expand this block." + override val expandText = "Expand" + override val collapseText = "Collapse" + override val labelText = R.string.label_with_two_links + } + ) + } +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt new file mode 100644 index 000000000000..4a9c5c8fad4f --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt @@ -0,0 +1,92 @@ +/* + * 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.settingslib.spa.gallery.preference + +import android.os.Bundle +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.History +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.common.SettingsEntry +import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.preference.ZeroStatePreference + +private const val TITLE = "Sample ZeroStatePreference" + +object ZeroStatePreferencePageProvider : SettingsPageProvider { + override val name = "ZeroStatePreference" + private val owner = createSettingsPage() + + override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { + val entryList = mutableListOf<SettingsEntry>() + entryList.add( + SettingsEntryBuilder.create("ZeroStatePreference", owner) + .setUiLayoutFn { + SampleZeroStatePreference() + }.build() + ) + + return entryList + } + + fun buildInjectEntry(): SettingsEntryBuilder { + return SettingsEntryBuilder.createInject(owner) + .setUiLayoutFn { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } + } + + override fun getTitle(arguments: Bundle?): String { + return TITLE + } +} + +@Composable +private fun SampleZeroStatePreference() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + ZeroStatePreference( + Icons.Filled.History, + "No recent search history", + "Description" + ) + } +} + + +@Preview(showBackground = true) +@Composable +private fun SwitchPreferencePagePreview() { + SettingsTheme { + ZeroStatePreferencePageProvider.Page(null) + } +} diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts index f0c2ea6f5353..790aa9ff8d3f 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/spa/build.gradle.kts @@ -54,15 +54,16 @@ android { dependencies { api(project(":SettingsLibColor")) api("androidx.appcompat:appcompat:1.7.0") - api("androidx.compose.material3:material3:1.3.0-rc01") + api("androidx.compose.material3:material3:1.3.0") api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion") api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion") api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion") api("androidx.lifecycle:lifecycle-livedata-ktx") api("androidx.lifecycle:lifecycle-runtime-compose") - api("androidx.navigation:navigation-compose:2.8.0-rc01") + api("androidx.navigation:navigation-compose:2.8.1") api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha") api("com.google.android.material:material:1.11.0") + api("androidx.graphics:graphics-shapes-android:1.0.1") debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion") implementation("com.airbnb.android:lottie-compose:6.4.0") diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt index 1f3e24254027..f8c791aab0d0 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt @@ -21,7 +21,9 @@ import androidx.compose.ui.unit.dp object SettingsDimension { val paddingTiny = 2.dp - val paddingSmall = 4.dp + val paddingExtraSmall = 4.dp + val paddingSmall = if (isSpaExpressiveEnabled) 8.dp else 4.dp + val paddingExtraSmall5 = 10.dp val paddingLarge = 16.dp val paddingExtraLarge = 24.dp @@ -56,6 +58,7 @@ object SettingsDimension { val itemDividerHeight = 32.dp val iconLarge = 48.dp + val introIconSize = 40.dp /** The size when app icon is displayed in list. */ val appIconItemSize = 32.dp diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt index 15def728d8b3..f948d5163177 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import com.android.settingslib.spa.framework.util.SystemProperties /** * The Material 3 Theme for Settings. @@ -41,4 +42,5 @@ fun SettingsTheme(content: @Composable () -> Unit) { } } -const val isSpaExpressiveEnabled = false
\ No newline at end of file +val isSpaExpressiveEnabled + by lazy { SystemProperties.getBoolean("is_expressive_design_enabled", false) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SystemProperties.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SystemProperties.kt new file mode 100644 index 000000000000..ed4936bbf8c9 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SystemProperties.kt @@ -0,0 +1,35 @@ +/* + * 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.settingslib.spa.framework.util + +import android.annotation.SuppressLint +import android.util.Log + +@SuppressLint("PrivateApi") +object SystemProperties { + private const val TAG = "SystemProperties" + + fun getBoolean(key: String, default: Boolean): Boolean = try { + val systemProperties = Class.forName("android.os.SystemProperties") + systemProperties + .getMethod("getBoolean", String::class.java, Boolean::class.java) + .invoke(systemProperties, key, default) as Boolean + } catch (e: Exception) { + Log.e(TAG, "getBoolean: $key", e) + default + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt new file mode 100644 index 000000000000..22a57554eeaf --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt @@ -0,0 +1,145 @@ +/* + * 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.settingslib.spa.widget.preference + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AirplanemodeActive +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.theme.SettingsDimension + +@Composable +fun IntroPreference( + title: String, + descriptions: List<String>? = null, + imageVector: ImageVector? = null, +) { + IntroPreference(title = title, descriptions = descriptions, icon = { IntroIcon(imageVector) }) +} + +@Composable +fun IntroAppPreference( + title: String, + descriptions: List<String>? = null, + appIcon: @Composable (() -> Unit), +) { + IntroPreference(title = title, descriptions = descriptions, icon = { IntroAppIcon(appIcon) }) +} + +@Composable +internal fun IntroPreference( + title: String, + descriptions: List<String>?, + icon: @Composable (() -> Unit), +) { + Column( + modifier = + Modifier.fillMaxWidth() + .padding( + horizontal = SettingsDimension.paddingExtraLarge, + vertical = SettingsDimension.paddingLarge, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + icon() + IntroTitle(title) + IntroDescription(descriptions) + } +} + +@Composable +private fun IntroIcon(imageVector: ImageVector?) { + if (imageVector != null) { + Box( + modifier = + Modifier.size(SettingsDimension.itemIconContainerSize) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.secondaryContainer), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = imageVector, + contentDescription = null, + modifier = Modifier.size(SettingsDimension.introIconSize), + tint = MaterialTheme.colorScheme.onSecondary, + ) + } + } +} + +@Composable +private fun IntroAppIcon(appIcon: @Composable () -> Unit) { + Box( + modifier = Modifier.size(SettingsDimension.itemIconContainerSize).clip(CircleShape), + contentAlignment = Alignment.Center, + ) { + appIcon() + } +} + +@Composable +private fun IntroTitle(title: String) { + Box(modifier = Modifier.padding(top = SettingsDimension.paddingLarge)) { + Text( + text = title, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + } +} + +@Composable +private fun IntroDescription(descriptions: List<String>?) { + if (descriptions != null) { + for (description in descriptions) { + if (description.isEmpty()) continue + Text( + text = description, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = SettingsDimension.paddingExtraSmall), + ) + } + } +} + +@Preview +@Composable +private fun IntroPreferencePreview() { + IntroPreference( + title = "Preferred network type", + descriptions = listOf("Description", "Version"), + imageVector = Icons.Outlined.AirplanemodeActive, + ) +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TopIntroPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TopIntroPreference.kt new file mode 100644 index 000000000000..7e619591c8a9 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TopIntroPreference.kt @@ -0,0 +1,152 @@ +/* + * 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.settingslib.spa.widget.preference + +import androidx.annotation.StringRes +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.toMediumWeight +import com.android.settingslib.spa.framework.util.annotatedStringResource + +/** The widget model for [TopIntroPreference] widget. */ +interface TopIntroPreferenceModel { + /** The content of this [TopIntroPreference]. */ + val text: String + + /** The text clicked to expand this [TopIntroPreference]. */ + val expandText: String + + /** The text clicked to collapse this [TopIntroPreference]. */ + val collapseText: String + + /** The text clicked to open other resources. Should be a resource Id. */ + val labelText: Int? +} + +@Composable +fun TopIntroPreference(model: TopIntroPreferenceModel) { + var expanded by remember { mutableStateOf(false) } + Column(Modifier.background(MaterialTheme.colorScheme.surfaceContainer)) { + // TopIntroPreference content. + Column( + modifier = + Modifier.padding( + horizontal = SettingsDimension.paddingExtraLarge, + vertical = SettingsDimension.paddingSmall, + ) + .animateContentSize() + ) { + Text( + text = model.text, + style = MaterialTheme.typography.bodyLarge, + maxLines = if (expanded) MAX_LINE else MIN_LINE, + ) + if (expanded) TopIntroAnnotatedText(model.labelText) + } + + // TopIntroPreference collapse bar. + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.fillMaxWidth() + .clickable(onClick = { expanded = !expanded }) + .padding( + top = SettingsDimension.paddingSmall, + bottom = SettingsDimension.paddingLarge, + start = SettingsDimension.paddingExtraLarge, + end = SettingsDimension.paddingExtraLarge, + ), + ) { + Icon( + imageVector = + if (expanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown, + contentDescription = null, + modifier = + Modifier.size(SettingsDimension.itemIconSize) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHighest), + ) + Text( + text = if (expanded) model.collapseText else model.expandText, + modifier = Modifier.padding(start = SettingsDimension.paddingSmall), + style = MaterialTheme.typography.bodyLarge.toMediumWeight(), + color = MaterialTheme.colorScheme.onSurface, + ) + } + } +} + +@Composable +private fun TopIntroAnnotatedText(@StringRes id: Int?) { + if (id != null) { + Box( + Modifier.padding( + top = SettingsDimension.paddingExtraSmall5, + bottom = SettingsDimension.paddingExtraSmall5, + end = SettingsDimension.paddingLarge, + ) + ) { + Text( + text = annotatedStringResource(id), + style = MaterialTheme.typography.bodyLarge.toMediumWeight(), + color = MaterialTheme.colorScheme.primary, + ) + } + } +} + +@Preview +@Composable +private fun TopIntroPreferencePreview() { + TopIntroPreference( + object : TopIntroPreferenceModel { + override val text = + "Additional text needed for the page. This can sit on the right side of the screen in 2 column.\n" + + "Example collapsed text area that you will not see until you expand this block." + override val expandText = "Expand" + override val collapseText = "Collapse" + override val labelText = androidx.appcompat.R.string.abc_prepend_shortcut_label + } + ) +} + +const val MIN_LINE = 2 +const val MAX_LINE = 10 diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt new file mode 100644 index 000000000000..3f2e7723c585 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt @@ -0,0 +1,132 @@ +/* + * 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.settingslib.spa.widget.preference + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.History +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Matrix +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.asComposePath +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.graphics.shapes.CornerRounding +import androidx.graphics.shapes.RoundedPolygon +import androidx.graphics.shapes.star +import androidx.graphics.shapes.toPath + +@Composable +fun ZeroStatePreference(icon: ImageVector, text: String? = null, description: String? = null) { + val zeroStateShape = remember { + RoundedPolygon.star( + numVerticesPerRadius = 6, + innerRadius = 0.75f, + rounding = CornerRounding(0.3f) + ) + } + val clip = remember(zeroStateShape) { + RoundedPolygonShape(polygon = zeroStateShape) + } + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = Modifier + .clip(clip) + .background(MaterialTheme.colorScheme.primary) + .size(160.dp) + ) { + Icon( + imageVector = icon, + modifier = Modifier + .align(Alignment.Center) + .size(72.dp), + tint = MaterialTheme.colorScheme.onPrimary, + contentDescription = null, + ) + } + if (text != null) { + Text( + text = text, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 24.dp), + ) + } + if (description != null) { + Box { + Text( + text = description, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@Preview +@Composable +private fun ZeroStatePreferencePreview() { + ZeroStatePreference( + Icons.Filled.History, + "No recent search history", + "Description" + ) +} + +class RoundedPolygonShape( + private val polygon: RoundedPolygon, + private var matrix: Matrix = Matrix() +) : Shape { + private var path = Path() + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + path.rewind() + path = polygon.toPath().asComposePath() + + matrix.reset() + matrix.scale(size.width / 2f, size.height / 2f) + matrix.translate(1f, 1f) + matrix.rotateZ(30.0f) + + path.transform(matrix) + return Outline.Generic(path) + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/tests/res/values/strings.xml b/packages/SettingsLib/Spa/tests/res/values/strings.xml index fb8f878230d5..346f69bb7a42 100644 --- a/packages/SettingsLib/Spa/tests/res/values/strings.xml +++ b/packages/SettingsLib/Spa/tests/res/values/strings.xml @@ -28,5 +28,7 @@ <string name="test_annotated_string_resource">Annotated string with <b>bold</b> and <a href="https://www.android.com/">link</a>.</string> + <string name="test_top_intro_preference_label"><a href="https://www.android.com/">Label</a></string> + <string name="test_link"><a href="https://www.android.com/">link</a></string> </resources> diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SystemPropertiesTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SystemPropertiesTest.kt new file mode 100644 index 000000000000..0827fa9e0ae0 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SystemPropertiesTest.kt @@ -0,0 +1,30 @@ +/* + * 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.settingslib.spa.framework.util + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SystemPropertiesTest { + + @Test + fun getBoolean_noCrash() { + SystemProperties.getBoolean("is_expressive_design_enabled", false) + } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/IntroPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/IntroPreferenceTest.kt new file mode 100644 index 000000000000..5d801451adcb --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/IntroPreferenceTest.kt @@ -0,0 +1,51 @@ +/* + * 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.settingslib.spa.widget.preference + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class IntroPreferenceTest { + @get:Rule val composeTestRule = createComposeRule() + + @Test + fun title_displayed() { + composeTestRule.setContent { IntroPreference(title = TITLE) } + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() + } + + @Test + fun description_displayed() { + composeTestRule.setContent { IntroPreference(title = TITLE, descriptions = DESCRIPTION) } + + composeTestRule.onNodeWithText(DESCRIPTION.component1()).assertIsDisplayed() + composeTestRule.onNodeWithText(DESCRIPTION.component2()).assertIsNotDisplayed() + } + + private companion object { + const val TITLE = "Title" + val DESCRIPTION = listOf("Description", "") + } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TopIntroPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TopIntroPreferenceTest.kt new file mode 100644 index 000000000000..62a71d4763b3 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TopIntroPreferenceTest.kt @@ -0,0 +1,75 @@ +/* + * 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.settingslib.spa.widget.preference + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.test.R +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TopIntroPreferenceTest { + @get:Rule val composeTestRule = createComposeRule() + + @Test + fun content_collapsed_displayed() { + composeTestRule.setContent { + TopIntroPreference( + object : TopIntroPreferenceModel { + override val text = TEXT + override val expandText = EXPAND_TEXT + override val collapseText = COLLAPSE_TEXT + override val labelText = R.string.test_top_intro_preference_label + } + ) + } + + composeTestRule.onNodeWithText(TEXT).assertIsDisplayed() + composeTestRule.onNodeWithText(EXPAND_TEXT).assertIsDisplayed() + } + + @Test + fun content_expended_displayed() { + composeTestRule.setContent { + TopIntroPreference( + object : TopIntroPreferenceModel { + override val text = TEXT + override val expandText = EXPAND_TEXT + override val collapseText = COLLAPSE_TEXT + override val labelText = R.string.test_top_intro_preference_label + } + ) + } + + composeTestRule.onNodeWithText(TEXT).assertIsDisplayed() + composeTestRule.onNodeWithText(EXPAND_TEXT).assertIsDisplayed().performClick() + composeTestRule.onNodeWithText(COLLAPSE_TEXT).assertIsDisplayed() + composeTestRule.onNodeWithText(LABEL_TEXT).assertIsDisplayed() + } + + private companion object { + const val TEXT = "Text" + const val EXPAND_TEXT = "Expand" + const val COLLAPSE_TEXT = "Collapse" + const val LABEL_TEXT = "Label" + } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ZeroStatePreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ZeroStatePreferenceTest.kt new file mode 100644 index 000000000000..99ac27c36e46 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ZeroStatePreferenceTest.kt @@ -0,0 +1,56 @@ +/* + * 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.settingslib.spa.widget.preference + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.History +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ZeroStatePreferenceTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun title_displayed() { + composeTestRule.setContent { + ZeroStatePreference(Icons.Filled.History, TITLE) + } + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() + } + + @Test + fun description_displayed() { + composeTestRule.setContent { + ZeroStatePreference(Icons.Filled.History, TITLE, DESCRIPTION) + } + + composeTestRule.onNodeWithText(DESCRIPTION).assertIsDisplayed() + } + + private companion object { + const val TITLE = "Title" + const val DESCRIPTION = "Description" + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/TwoTargetPreference/res/layout-v35/settingslib_expressive_preference_two_target.xml b/packages/SettingsLib/TwoTargetPreference/res/layout-v35/settingslib_expressive_preference_two_target.xml new file mode 100644 index 000000000000..4347ef29037d --- /dev/null +++ b/packages/SettingsLib/TwoTargetPreference/res/layout-v35/settingslib_expressive_preference_two_target.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:gravity="center_vertical" + android:background="?android:attr/selectableItemBackground" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:clipToPadding="false"> + + <include layout="@layout/settingslib_expressive_preference_icon_frame"/> + + <include layout="@layout/settingslib_expressive_preference_text_frame" /> + + <include layout="@layout/settingslib_expressive_two_target_divider" /> + + <!-- Preference should place its actual preference widget here. --> + <LinearLayout + android:id="@android:id/widget_frame" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:minWidth="@dimen/two_target_min_width" + android:gravity="center" + android:orientation="vertical" /> + +</LinearLayout> diff --git a/packages/SettingsLib/TwoTargetPreference/src/com/android/settingslib/widget/TwoTargetPreference.java b/packages/SettingsLib/TwoTargetPreference/src/com/android/settingslib/widget/TwoTargetPreference.java index b125f716fe52..58ff0ce1932a 100644 --- a/packages/SettingsLib/TwoTargetPreference/src/com/android/settingslib/widget/TwoTargetPreference.java +++ b/packages/SettingsLib/TwoTargetPreference/src/com/android/settingslib/widget/TwoTargetPreference.java @@ -72,7 +72,10 @@ public class TwoTargetPreference extends Preference { } private void init(Context context) { - setLayoutResource(R.layout.preference_two_target); + int resID = SettingsThemeHelper.isExpressiveTheme(context) + ? R.layout.settingslib_expressive_preference_two_target + : R.layout.preference_two_target; + setLayoutResource(resID); mSmallIconSize = context.getResources().getDimensionPixelSize( R.dimen.two_target_pref_small_icon_size); mMediumIconSize = context.getResources().getDimensionPixelSize( diff --git a/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java index e41126f03c60..2475c8e9dfd1 100644 --- a/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java @@ -31,6 +31,8 @@ import androidx.preference.PreferenceViewHolder; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.core.instrumentation.SettingsJankMonitor; +import com.android.settingslib.widget.SettingsThemeHelper; +import com.android.settingslib.widget.theme.R; /** * A custom preference that provides inline switch toggle. It has a mandatory field for title, and @@ -62,7 +64,9 @@ public class PrimarySwitchPreference extends RestrictedPreference { @Override protected int getSecondTargetResId() { - return androidx.preference.R.layout.preference_widget_switch_compat; + return SettingsThemeHelper.isExpressiveTheme(getContext()) + ? R.layout.settingslib_expressive_preference_switch + : androidx.preference.R.layout.preference_widget_switch_compat; } @Override diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index f64c3059e3a4..749ad0a993b3 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -967,7 +967,7 @@ public class SettingsProvider extends ContentProvider { for (int i = 0; i < nameCount; i++) { String name = names.get(i); Setting setting = settingsState.getSettingLocked(name); - pw.print("_id:"); pw.print(toDumpString(setting.getId())); + pw.print("_id:"); pw.print(toDumpString(String.valueOf(setting.getId()))); pw.print(" name:"); pw.print(toDumpString(name)); if (setting.getPackageName() != null) { pw.print(" pkg:"); pw.print(setting.getPackageName()); @@ -2785,7 +2785,7 @@ public class SettingsProvider extends ContentProvider { switch (column) { case Settings.NameValueTable._ID -> { - values[i] = setting.getId(); + values[i] = String.valueOf(setting.getId()); } case Settings.NameValueTable.NAME -> { values[i] = setting.getName(); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 452edd924a26..3c634f067a0d 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -517,6 +517,7 @@ final class SettingsState { } String namespace = name.substring(0, slashIdx); + namespace = namespace.intern(); // Many configs have the same namespace. String fullFlagName = name.substring(slashIdx + 1); boolean isLocal = false; @@ -566,7 +567,7 @@ final class SettingsState { } try { localCounter = Integer.parseInt(markerSetting.value); - } catch(NumberFormatException e) { + } catch (NumberFormatException e) { // reset local counter markerSetting.value = "0"; } @@ -1363,7 +1364,10 @@ final class SettingsState { } try { - if (writeSingleSetting(mVersion, serializer, setting.getId(), + if (writeSingleSetting( + mVersion, + serializer, + Long.toString(setting.getId()), setting.getName(), setting.getValue(), setting.getDefaultValue(), setting.getPackageName(), @@ -1632,7 +1636,7 @@ final class SettingsState { TypedXmlPullParser parser = Xml.resolvePullParser(in); parseStateLocked(parser); return true; - } catch (XmlPullParserException | IOException e) { + } catch (XmlPullParserException | IOException | NumberFormatException e) { Slog.e(LOG_TAG, "parse settings xml failed", e); return false; } finally { @@ -1652,7 +1656,7 @@ final class SettingsState { } private void parseStateLocked(TypedXmlPullParser parser) - throws IOException, XmlPullParserException { + throws IOException, XmlPullParserException, NumberFormatException { final int outerDepth = parser.getDepth(); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT @@ -1708,7 +1712,7 @@ final class SettingsState { @GuardedBy("mLock") private void parseSettingsLocked(TypedXmlPullParser parser) - throws IOException, XmlPullParserException { + throws IOException, XmlPullParserException, NumberFormatException { mVersion = parser.getAttributeInt(null, ATTR_VERSION); @@ -1776,7 +1780,7 @@ final class SettingsState { } } mSettings.put(name, new Setting(name, value, defaultValue, packageName, tag, - fromSystem, id, isPreservedInRestore)); + fromSystem, Long.valueOf(id), isPreservedInRestore)); if (DEBUG_PERSISTENCE) { Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value); @@ -1866,7 +1870,7 @@ final class SettingsState { private String value; private String defaultValue; private String packageName; - private String id; + private long id; private String tag; // Whether the default is set by the system private boolean defaultFromSystem; @@ -1898,30 +1902,27 @@ final class SettingsState { } public Setting(String name, String value, String defaultValue, - String packageName, String tag, boolean fromSystem, String id) { + String packageName, String tag, boolean fromSystem, long id) { this(name, value, defaultValue, packageName, tag, fromSystem, id, /* isOverrideableByRestore */ false); } Setting(String name, String value, String defaultValue, - String packageName, String tag, boolean fromSystem, String id, + String packageName, String tag, boolean fromSystem, long id, boolean isValuePreservedInRestore) { - mNextId = Math.max(mNextId, Long.parseLong(id) + 1); - if (NULL_VALUE.equals(value)) { - value = null; - } + mNextId = Math.max(mNextId, id + 1); init(name, value, tag, defaultValue, packageName, fromSystem, id, isValuePreservedInRestore); } private void init(String name, String value, String tag, String defaultValue, - String packageName, boolean fromSystem, String id, + String packageName, boolean fromSystem, long id, boolean isValuePreservedInRestore) { this.name = name; - this.value = value; + this.value = internValue(value); this.tag = tag; - this.defaultValue = defaultValue; - this.packageName = packageName; + this.defaultValue = internValue(defaultValue); + this.packageName = TextUtils.safeIntern(packageName); this.id = id; this.defaultFromSystem = fromSystem; this.isValuePreservedInRestore = isValuePreservedInRestore; @@ -1959,7 +1960,7 @@ final class SettingsState { return isValuePreservedInRestore; } - public String getId() { + public long getId() { return id; } @@ -1992,9 +1993,6 @@ final class SettingsState { private boolean update(String value, boolean setDefault, String packageName, String tag, boolean forceNonSystemPackage, boolean overrideableByRestore, boolean resetToDefault) { - if (NULL_VALUE.equals(value)) { - value = null; - } final boolean callerSystem = !forceNonSystemPackage && !isNull() && (isCalledFromSystem(packageName) || isSystemPackage(mContext, packageName)); @@ -2039,7 +2037,7 @@ final class SettingsState { } init(name, value, tag, defaultValue, packageName, defaultFromSystem, - String.valueOf(mNextId++), isPreserved); + mNextId++, isPreserved); return true; } @@ -2051,6 +2049,32 @@ final class SettingsState { + " defaultFromSystem=" + defaultFromSystem + "}"; } + /** + * Interns a string if it's a common setting value. + * Otherwise returns the given string. + */ + static String internValue(String str) { + if (str == null) { + return null; + } + switch (str) { + case "true": + return "true"; + case "false": + return "false"; + case "0": + return "0"; + case "1": + return "1"; + case "": + return ""; + case "null": + return null; // explicit null has special handling + default: + return str; + } + } + private boolean shouldPreserveSetting(boolean overrideableByRestore, boolean resetToDefault, String packageName, String value) { if (resetToDefault) { diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index c49ffb49a1da..f8383d94b1ab 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -149,16 +149,6 @@ flag { } flag { - name: "modes_dialog_single_rows" - namespace: "systemui" - description: "[Experiment] Display one entry per grid row in the Modes Dialog." - bug: "366034002" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "pss_app_selector_recents_split_screen" namespace: "systemui" description: "Allows recent apps selected for partial screenshare to be launched in split screen mode" @@ -599,6 +589,16 @@ flag { } flag { + name: "clipboard_use_description_mimetype" + namespace: "systemui" + description: "Read item mimetype from description rather than checking URI" + bug: "357197236" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "screenshot_action_dismiss_system_windows" namespace: "systemui" description: "Dismiss existing system windows when starting action from screenshot UI" 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 1b99a9644575..fe4a65b8bbd0 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 @@ -130,8 +130,8 @@ object Notifications { fun SceneScope.HeadsUpNotificationSpace( stackScrollView: NotificationScrollView, viewModel: NotificationsPlaceholderViewModel, + useHunBounds: () -> Boolean = { true }, modifier: Modifier = Modifier, - isPeekFromBottom: Boolean = false, ) { Box( modifier = @@ -141,17 +141,25 @@ fun SceneScope.HeadsUpNotificationSpace( .notificationHeadsUpHeight(stackScrollView) .debugBackground(viewModel, DEBUG_HUN_COLOR) .onGloballyPositioned { coordinates: LayoutCoordinates -> - val positionInWindow = coordinates.positionInWindow() - val boundsInWindow = coordinates.boundsInWindow() - debugLog(viewModel) { - "HUNS onGloballyPositioned:" + - " size=${coordinates.size}" + - " bounds=$boundsInWindow" + // This element is sometimes opted out of the shared element system, so there + // can be multiple instances of it during a transition. Thus we need to + // determine which instance should feed its bounds to NSSL to avoid providing + // conflicting values + val useBounds = useHunBounds() + if (useBounds) { + val positionInWindow = coordinates.positionInWindow() + val boundsInWindow = coordinates.boundsInWindow() + debugLog(viewModel) { + "HUNS onGloballyPositioned:" + + " size=${coordinates.size}" + + " bounds=$boundsInWindow" + } + // Note: boundsInWindow doesn't scroll off the screen, so use + // positionInWindow + // for top bound, which can scroll off screen while snoozing + stackScrollView.setHeadsUpTop(positionInWindow.y) + stackScrollView.setHeadsUpBottom(boundsInWindow.bottom) } - // Note: boundsInWindow doesn't scroll off the screen, so use positionInWindow - // for top bound, which can scroll off screen while snoozing - stackScrollView.setHeadsUpTop(positionInWindow.y) - stackScrollView.setHeadsUpBottom(boundsInWindow.bottom) } ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index d91958adaa1b..0c69dbd5655c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -416,11 +416,11 @@ private fun SceneScope.QuickSettingsScene( HeadsUpNotificationSpace( stackScrollView = notificationStackScrollView, viewModel = notificationsPlaceholderViewModel, + useHunBounds = { shouldUseQuickSettingsHunBounds(layoutState.transitionState) }, modifier = Modifier.align(Alignment.BottomCenter) .navigationBarsPadding() .padding(horizontal = shadeHorizontalPadding), - isPeekFromBottom = true, ) NotificationScrollingStack( shadeSession = shadeSession, @@ -446,3 +446,7 @@ private fun SceneScope.QuickSettingsScene( ) } } + +private fun shouldUseQuickSettingsHunBounds(state: TransitionState): Boolean { + return state is TransitionState.Idle && state.currentScene == Scenes.QuickSettings +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index f64d0ed31287..58fbf430b20c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -77,6 +77,10 @@ val SceneContainerTransitions = transitions { } from(Scenes.Lockscreen, to = Scenes.QuickSettings) { lockscreenToQuickSettingsTransition() } from(Scenes.Lockscreen, to = Scenes.Gone) { lockscreenToGoneTransition() } + from(Scenes.QuickSettings, to = Scenes.Shade) { + reversed { shadeToQuickSettingsTransition() } + sharedElement(Notifications.Elements.HeadsUpNotificationPlaceholder, enabled = false) + } from(Scenes.Shade, to = Scenes.QuickSettings) { shadeToQuickSettingsTransition() } // Overlay transitions diff --git a/packages/SystemUI/tests/src/com/android/keyguard/AuthKeyguardMessageAreaTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/AuthKeyguardMessageAreaTest.java index 6ee8ffd91ab0..6ee8ffd91ab0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/AuthKeyguardMessageAreaTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/AuthKeyguardMessageAreaTest.java diff --git a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt index 049d77a7a454..049d77a7a454 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/BouncerPanelExpansionCalculatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/BouncerPanelExpansionCalculatorTest.kt index bb2340aafbb5..bb2340aafbb5 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/BouncerPanelExpansionCalculatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/BouncerPanelExpansionCalculatorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/EmergencyButtonControllerTest.kt index c42e25b20e0d..c42e25b20e0d 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/EmergencyButtonControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt index d170e4840842..d170e4840842 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java index 2bb9e68a357a..2bb9e68a357a 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index 892375d002c1..892375d002c1 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerWithCoroutinesTest.kt index c2c0f5713d9b..c2c0f5713d9b 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerWithCoroutinesTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchTest.java index 0bf9d12a09d5..0bf9d12a09d5 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchTest.java diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.java index dd58ea7db2bc..dd58ea7db2bc 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.java diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java index bd811814eb24..bd811814eb24 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewTest.java index d96518abc007..d96518abc007 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewTest.java diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusAreaViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardStatusAreaViewTest.kt index 64e499674d9f..64e499674d9f 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusAreaViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardStatusAreaViewTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardStatusViewControllerBaseTest.java index 2b4fc5bd5cc5..2b4fc5bd5cc5 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardStatusViewControllerBaseTest.java diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardStatusViewControllerWithCoroutinesTest.kt index c29439d89753..c29439d89753 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardStatusViewControllerWithCoroutinesTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardStatusViewTest.kt index 16d2f0205c84..16d2f0205c84 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardStatusViewTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt index 2e41246a62a1..2e41246a62a1 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUserSwitcherAnchorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardUserSwitcherAnchorTest.kt index e7b4262419ce..e7b4262419ce 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUserSwitcherAnchorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardUserSwitcherAnchorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/NumPadAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/NumPadAnimatorTest.kt index 18976e135e9c..18976e135e9c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/NumPadAnimatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/NumPadAnimatorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/PinShapeHintingViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/PinShapeHintingViewTest.kt index d8f2b1016657..d8f2b1016657 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/PinShapeHintingViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/PinShapeHintingViewTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/PinShapeNonHintingViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/PinShapeNonHintingViewTest.kt index 447cf65ba293..447cf65ba293 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/PinShapeNonHintingViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/PinShapeNonHintingViewTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt index c7d11ef16100..c7d11ef16100 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/TestScopeProvider.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/TestScopeProvider.kt index 6c35734c6eb4..6c35734c6eb4 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/TestScopeProvider.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/TestScopeProvider.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt index 5247a89896d1..5247a89896d1 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index 492543f215b7..af3ddfca14b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -310,6 +310,41 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { .isEqualTo(displayId) } + @Test + fun afterSuccessfulAuthentication_focusIsNotRequested() = + testScope.runTest { + val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) + val textInputFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested) + lockDeviceAndOpenPasswordBouncer() + + // remove focus from text field + underTest.onTextFieldFocusChanged(false) + runCurrent() + + // focus should be requested + assertThat(textInputFocusRequested).isTrue() + + // simulate text field getting focus + underTest.onTextFieldFocusChanged(true) + runCurrent() + + // focus should not be requested anymore + assertThat(textInputFocusRequested).isFalse() + + // authenticate successfully. + underTest.onPasswordInputChanged("password") + underTest.onAuthenticateKeyPressed() + runCurrent() + + assertThat(authResult).isTrue() + + // remove focus from text field + underTest.onTextFieldFocusChanged(false) + runCurrent() + // focus should not be requested again + assertThat(textInputFocusRequested).isFalse() + } + private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.currentScene) val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer @@ -327,10 +362,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { switchToScene(Scenes.Bouncer) } - private suspend fun TestScope.setLockout( - isLockedOut: Boolean, - failedAttemptCount: Int = 5, - ) { + private suspend fun TestScope.setLockout(isLockedOut: Boolean, failedAttemptCount: Int = 5) { if (isLockedOut) { repeat(failedAttemptCount) { kosmos.fakeAuthenticationRepository.reportAuthenticationAttempt(false) @@ -350,7 +382,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { kosmos.fakeUserRepository.selectedUser.value = SelectedUserModel( userInfo = userInfo, - selectionStatus = SelectionStatus.SELECTION_COMPLETE + selectionStatus = SelectionStatus.SELECTION_COMPLETE, ) advanceTimeBy(PasswordBouncerViewModel.DELAY_TO_FETCH_IMES) } @@ -374,7 +406,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { subtypes = List(auxiliarySubtypes + nonAuxiliarySubtypes) { InputMethodModel.Subtype(subtypeId = it, isAuxiliary = it < auxiliarySubtypes) - } + }, ) } @@ -383,9 +415,6 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { private const val WRONG_PASSWORD = "Wrong password" private val USER_INFOS = - listOf( - UserInfo(100, "First user", 0), - UserInfo(101, "Second user", 0), - ) + listOf(UserInfo(100, "First user", 0), UserInfo(101, "Second user", 0)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt index 2ba4bf930f3f..e25c1a71a5a6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt @@ -34,8 +34,6 @@ import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.keyguard.data.repository.realKeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.DozeStateModel -import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB @@ -50,8 +48,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope -import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest -import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds @@ -220,15 +216,11 @@ class CommunalSceneTransitionInteractorTest : SysuiTestCase() { @Test fun transition_from_hub_end_in_dream() = testScope.runTest { - // Device is dreaming and not dozing. - kosmos.powerInteractor.setAwakeForTest() - kosmos.fakeKeyguardRepository.setDozeTransitionModel( - DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) - ) + // Device is dreaming and occluded. kosmos.fakeKeyguardRepository.setKeyguardOccluded(true) kosmos.fakeKeyguardRepository.setDreaming(true) kosmos.fakeKeyguardRepository.setDreamingWithOverlay(true) - advanceTimeBy(600L) + runCurrent() sceneTransitions.value = hubToBlank @@ -663,7 +655,7 @@ class CommunalSceneTransitionInteractorTest : SysuiTestCase() { from = LOCKSCREEN, to = OCCLUDED, animator = null, - modeOnCanceled = TransitionModeOnCanceled.RESET + modeOnCanceled = TransitionModeOnCanceled.RESET, ) ) @@ -750,7 +742,7 @@ class CommunalSceneTransitionInteractorTest : SysuiTestCase() { from = LOCKSCREEN, to = OCCLUDED, animator = null, - modeOnCanceled = TransitionModeOnCanceled.RESET + modeOnCanceled = TransitionModeOnCanceled.RESET, ) ) @@ -852,8 +844,8 @@ class CommunalSceneTransitionInteractorTest : SysuiTestCase() { to = ALTERNATE_BOUNCER, animator = null, ownerName = "external", - modeOnCanceled = TransitionModeOnCanceled.RESET - ), + modeOnCanceled = TransitionModeOnCanceled.RESET, + ) ) val allSteps by collectValues(keyguardTransitionRepository.transitions) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartableTest.kt index 9da68853a5aa..52ed23122fde 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartableTest.kt @@ -19,11 +19,13 @@ package com.android.systemui.inputdevice.tutorial import android.content.Context import android.content.Intent import android.os.UserHandle +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.inputdevice.tutorial.ui.TutorialNotificationCoordinator +import com.android.systemui.shared.Flags import com.android.systemui.testKosmos import org.junit.Test import org.junit.runner.RunWith @@ -43,16 +45,17 @@ class KeyboardTouchpadTutorialCoreStartableTest : SysuiTestCase() { KeyboardTouchpadTutorialCoreStartable( { mock<TutorialNotificationCoordinator>() }, broadcastDispatcher, - context + context, ) @Test + @EnableFlags(Flags.FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL) fun registersBroadcastReceiverStartingActivityAsSystemUser() { underTest.start() broadcastDispatcher.sendIntentToMatchingReceiversOnly( context, - Intent("com.android.systemui.action.KEYBOARD_TOUCHPAD_TUTORIAL") + Intent("com.android.systemui.action.KEYBOARD_TOUCHPAD_TUTORIAL"), ) verify(context).startActivityAsUser(any(), eq(UserHandle.SYSTEM)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index bd6cffff6162..93754fd7e778 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.doze.DozeMachine import com.android.systemui.doze.DozeTransitionCallback import com.android.systemui.doze.DozeTransitionListener import com.android.systemui.dreams.DreamOverlayCallbackController +import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.keyguard.shared.model.BiometricUnlockMode import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.DozeStateModel @@ -288,6 +289,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + @DisableSceneContainer fun dozeAmount() = testScope.runTest { val values = mutableListOf<Float>() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index fac931273ac7..ff0a4a16fe2a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -26,8 +26,10 @@ import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository +import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository import com.android.systemui.communal.domain.interactor.setCommunalAvailable import com.android.systemui.communal.shared.model.CommunalScenes @@ -50,10 +52,12 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos +import com.google.common.truth.Truth import junit.framework.Assert.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -219,6 +223,28 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT } @Test + @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR, FLAG_SCENE_CONTAINER) + @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + fun testTransitionToGlanceableHub_onWakeup_ifAvailable() = + testScope.runTest { + // Hub is available. + whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + kosmos.setCommunalAvailable(true) + runCurrent() + + // Device turns on. + powerInteractor.setAwakeForTest() + advanceTimeBy(50L) + runCurrent() + + // We transition to the hub when waking up. + Truth.assertThat(kosmos.communalSceneRepository.currentScene.value) + .isEqualTo(CommunalScenes.Communal) + // No transitions are directly started by this interactor. + assertThat(transitionRepository).noTransitionsStarted() + } + + @Test @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testTransitionToOccluded_onWakeup_whenOccludingActivityOnTop() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt index 638c957c9fa7..a08fbbf75805 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt @@ -16,12 +16,19 @@ package com.android.systemui.keyguard.domain.interactor +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.flag.junit.FlagsParameterization +import android.service.dream.dreamManager import androidx.test.filters.SmallTest import com.android.systemui.Flags +import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.domain.interactor.setCommunalAvailable +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -36,6 +43,7 @@ import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInterac import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -43,26 +51,52 @@ import org.junit.Before import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.reset import org.mockito.Mockito.spy +import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @OptIn(ExperimentalCoroutinesApi::class) @SmallTest -@RunWith(AndroidJUnit4::class) -class FromDreamingTransitionInteractorTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiTestCase() { + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + .andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags!!) + } + private val kosmos = testKosmos().apply { this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository()) } private val testScope = kosmos.testScope - private val underTest = kosmos.fromDreamingTransitionInteractor + private val underTest by lazy { kosmos.fromDreamingTransitionInteractor } private val powerInteractor = kosmos.powerInteractor private val transitionRepository = kosmos.fakeKeyguardTransitionRepository @Before fun setup() { + runBlocking { + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + testScope, + ) + reset(transitionRepository) + kosmos.setCommunalAvailable(true) + } underTest.start() } @@ -86,10 +120,7 @@ class FromDreamingTransitionInteractorTest : SysuiTestCase() { runCurrent() assertThat(transitionRepository) - .startedTransition( - from = KeyguardState.DREAMING, - to = KeyguardState.OCCLUDED, - ) + .startedTransition(from = KeyguardState.DREAMING, to = KeyguardState.OCCLUDED) } @Test @@ -126,7 +157,7 @@ class FromDreamingTransitionInteractorTest : SysuiTestCase() { transitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, to = KeyguardState.DREAMING, - testScope + testScope, ) kosmos.fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockMode.NONE) @@ -139,10 +170,7 @@ class FromDreamingTransitionInteractorTest : SysuiTestCase() { advanceTimeBy(60L) assertThat(transitionRepository) - .startedTransition( - from = KeyguardState.DREAMING, - to = KeyguardState.LOCKSCREEN, - ) + .startedTransition(from = KeyguardState.DREAMING, to = KeyguardState.LOCKSCREEN) } @Test @@ -164,4 +192,25 @@ class FromDreamingTransitionInteractorTest : SysuiTestCase() { to = KeyguardState.ALTERNATE_BOUNCER, ) } + + @Test + @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + @DisableFlags(Flags.FLAG_SCENE_CONTAINER) + fun testTransitionToGlanceableHubOnWake() = + testScope.runTest { + whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + kosmos.setCommunalAvailable(true) + runCurrent() + + // Device wakes up. + powerInteractor.setAwakeForTest() + advanceTimeBy(150L) + runCurrent() + + // We transition to the hub when waking up. + assertThat(kosmos.communalSceneRepository.currentScene.value) + .isEqualTo(CommunalScenes.Communal) + // No transitions are directly started by this interactor. + assertThat(transitionRepository).noTransitionsStarted() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt index b843fd508616..3fb3eead6469 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt @@ -29,15 +29,21 @@ import com.android.systemui.common.ui.data.repository.fakeConfigurationRepositor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.flags.EnableSceneContainer -import com.android.systemui.keyguard.data.repository.fakeCommandQueue import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.shared.model.CameraLaunchType import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeTransitionModel 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.DOZING +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest @@ -67,12 +73,11 @@ class KeyguardInteractorTest : SysuiTestCase() { private val testScope = kosmos.testScope private val repository by lazy { kosmos.fakeKeyguardRepository } private val sceneInteractor by lazy { kosmos.sceneInteractor } - private val fromGoneTransitionInteractor by lazy { kosmos.fromGoneTransitionInteractor } - private val commandQueue by lazy { kosmos.fakeCommandQueue } private val configRepository by lazy { kosmos.fakeConfigurationRepository } private val bouncerRepository by lazy { kosmos.keyguardBouncerRepository } private val shadeRepository by lazy { kosmos.shadeRepository } private val powerInteractor by lazy { kosmos.powerInteractor } + private val keyguardRepository by lazy { kosmos.keyguardRepository } private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } private val transitionState: MutableStateFlow<ObservableTransitionState> = @@ -178,8 +183,8 @@ class KeyguardInteractorTest : SysuiTestCase() { assertThat(dismissAlpha).isEqualTo(1f) keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, + from = AOD, + to = LOCKSCREEN, testScope, ) @@ -204,8 +209,8 @@ class KeyguardInteractorTest : SysuiTestCase() { assertThat(dismissAlpha.size).isEqualTo(1) keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, + from = AOD, + to = LOCKSCREEN, testScope, ) @@ -266,13 +271,13 @@ class KeyguardInteractorTest : SysuiTestCase() { keyguardTransitionRepository.sendTransitionSteps( listOf( TransitionStep( - from = KeyguardState.AOD, + from = AOD, to = KeyguardState.GONE, value = 0f, - transitionState = TransitionState.STARTED, + transitionState = STARTED, ), TransitionStep( - from = KeyguardState.AOD, + from = AOD, to = KeyguardState.GONE, value = 0.1f, transitionState = TransitionState.RUNNING, @@ -302,7 +307,7 @@ class KeyguardInteractorTest : SysuiTestCase() { shadeRepository.setLegacyShadeExpansion(0f) keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.AOD, + from = AOD, to = KeyguardState.GONE, testScope, ) @@ -324,8 +329,8 @@ class KeyguardInteractorTest : SysuiTestCase() { shadeRepository.setLegacyShadeExpansion(0f) keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, + from = AOD, + to = LOCKSCREEN, testScope, ) @@ -346,8 +351,8 @@ class KeyguardInteractorTest : SysuiTestCase() { shadeRepository.setLegacyShadeExpansion(1f) keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, + from = AOD, + to = LOCKSCREEN, testScope, ) @@ -370,13 +375,13 @@ class KeyguardInteractorTest : SysuiTestCase() { keyguardTransitionRepository.sendTransitionSteps( listOf( TransitionStep( - from = KeyguardState.AOD, + from = AOD, to = KeyguardState.GONE, value = 0f, - transitionState = TransitionState.STARTED, + transitionState = STARTED, ), TransitionStep( - from = KeyguardState.AOD, + from = AOD, to = KeyguardState.GONE, value = 0.1f, transitionState = TransitionState.RUNNING, @@ -468,4 +473,63 @@ class KeyguardInteractorTest : SysuiTestCase() { runCurrent() assertThat(isAnimate).isFalse() } + + @Test + @EnableSceneContainer + fun dozeAmount_updatedByAodTransitionWhenAodEnabled() = + testScope.runTest { + val dozeAmount by collectLastValue(underTest.dozeAmount) + + keyguardRepository.setAodAvailable(true) + + sendTransitionStep(TransitionStep(to = AOD, value = 0f, transitionState = STARTED)) + assertThat(dozeAmount).isEqualTo(0f) + + sendTransitionStep(TransitionStep(to = AOD, value = 0.5f, transitionState = RUNNING)) + assertThat(dozeAmount).isEqualTo(0.5f) + + sendTransitionStep(TransitionStep(to = AOD, value = 1f, transitionState = FINISHED)) + assertThat(dozeAmount).isEqualTo(1f) + + sendTransitionStep(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) + assertThat(dozeAmount).isEqualTo(1f) + + sendTransitionStep(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) + assertThat(dozeAmount).isEqualTo(0.5f) + + sendTransitionStep(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) + assertThat(dozeAmount).isEqualTo(0f) + } + + @Test + @EnableSceneContainer + fun dozeAmount_updatedByDozeTransitionWhenAodDisabled() = + testScope.runTest { + val dozeAmount by collectLastValue(underTest.dozeAmount) + + keyguardRepository.setAodAvailable(false) + + sendTransitionStep(TransitionStep(to = DOZING, value = 0f, transitionState = STARTED)) + assertThat(dozeAmount).isEqualTo(0f) + + sendTransitionStep(TransitionStep(to = DOZING, value = 0.5f, transitionState = RUNNING)) + assertThat(dozeAmount).isEqualTo(0.5f) + + sendTransitionStep(TransitionStep(to = DOZING, value = 1f, transitionState = FINISHED)) + assertThat(dozeAmount).isEqualTo(1f) + + sendTransitionStep(TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED)) + assertThat(dozeAmount).isEqualTo(1f) + + sendTransitionStep(TransitionStep(DOZING, LOCKSCREEN, 0.5f, RUNNING)) + assertThat(dozeAmount).isEqualTo(0.5f) + + sendTransitionStep(TransitionStep(DOZING, LOCKSCREEN, 1f, FINISHED)) + assertThat(dozeAmount).isEqualTo(0f) + } + + private suspend fun sendTransitionStep(step: TransitionStep) { + keyguardTransitionRepository.sendTransitionStep(step) + testScope.runCurrent() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt new file mode 100644 index 000000000000..2558d583b001 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt @@ -0,0 +1,139 @@ +/* + * 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.keyguard.domain.interactor + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.policy.IKeyguardDismissCallback +import com.android.internal.policy.IKeyguardStateCallback +import com.android.keyguard.trustManager +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.dismissCallbackRegistry +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.domain.interactor.keyguardStateCallbackInteractor +import com.android.systemui.testKosmos +import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.time.fakeSystemClock +import kotlin.test.Test +import kotlinx.coroutines.test.currentTime +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.anyInt +import org.mockito.kotlin.atLeast +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyguardStateCallbackInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var underTest: KeyguardStateCallbackInteractor + private lateinit var callback: IKeyguardStateCallback + private lateinit var systemClock: FakeSystemClock + + @Before + fun setUp() { + systemClock = kosmos.fakeSystemClock + systemClock.setCurrentTimeMillis(testScope.currentTime) + + underTest = kosmos.keyguardStateCallbackInteractor + underTest.start() + + callback = mock<IKeyguardStateCallback>() + } + + @Test + fun test_addCallback_passesInitialValues() = + testScope.runTest { + underTest.addCallback(callback) + + verify(callback).onShowingStateChanged(anyBoolean(), anyInt()) + verify(callback).onInputRestrictedStateChanged(anyBoolean()) + verify(callback).onTrustedChanged(anyBoolean()) + verify(callback).onSimSecureStateChanged(anyBoolean()) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun test_lockscreenVisibility_notifyDismissSucceeded_ifNotVisible() = + testScope.runTest { + underTest.addCallback(callback) + + val dismissCallback = mock<IKeyguardDismissCallback>() + kosmos.dismissCallbackRegistry.addCallback(dismissCallback) + runCurrent() + + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope = testScope, + ) + + systemClock.advanceTime(1) // Required for DismissCallbackRegistry's bgExecutor + verify(dismissCallback).onDismissSucceeded() + + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + testScope = testScope, + ) + + Mockito.verifyNoMoreInteractions(dismissCallback) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun test_lockscreenVisibility_reportsKeyguardShowingChanged() = + testScope.runTest { + underTest.addCallback(callback) + + Mockito.clearInvocations(callback) + Mockito.clearInvocations(kosmos.trustManager) + + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope = testScope, + ) + runCurrent() + + verify(callback, atLeastOnce()).onShowingStateChanged(eq(false), anyInt()) + verify(kosmos.trustManager, atLeastOnce()).reportKeyguardShowingChanged() + + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + testScope = testScope, + ) + + verify(callback, atLeastOnce()).onShowingStateChanged(eq(true), anyInt()) + verify(kosmos.trustManager, atLeast(2)).reportKeyguardShowingChanged() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 77106aec2fb4..a617484d7d94 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -1465,10 +1465,8 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest // WHEN the keyguard is occluded and device wakes up and is no longer dreaming keyguardRepository.setDreaming(false) - testScheduler.advanceTimeBy(150) // The dreaming signal is debounced. - runCurrent() keyguardRepository.setKeyguardOccluded(true) - powerInteractor.setAwakeForTest() + testScheduler.advanceTimeBy(150) // The dreaming and occluded signals are debounced. runCurrent() // THEN a transition to OCCLUDED should occur @@ -2059,12 +2057,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest fun glanceableHubToOccluded_communalKtfRefactor() = testScope.runTest { // GIVEN device is not dreaming - powerInteractor.setAwakeForTest() keyguardRepository.setDreaming(false) - keyguardRepository.setDozeTransitionModel( - DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) - ) - advanceTimeBy(600.milliseconds) // GIVEN a prior transition has run to GLANCEABLE_HUB communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") @@ -2073,6 +2066,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest // WHEN the keyguard is occluded keyguardRepository.setKeyguardOccluded(true) + advanceTimeBy(200.milliseconds) runCurrent() assertThat(transitionRepository) @@ -2218,6 +2212,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest advanceTimeBy(10.milliseconds) keyguardRepository.setKeyguardOccluded(true) advanceTimeBy(200.milliseconds) + runCurrent() assertThat(transitionRepository) .startedTransition( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt index f31eb7f50405..309e3a8be14a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt @@ -27,11 +27,18 @@ import com.android.systemui.doze.util.BurnInHelperWrapper import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.FakeCommandQueue import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.keyguard.shared.model.TransitionState.STARTED +import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.PowerInteractorFactory +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.domain.interactor.shadeLockscreenInteractor @@ -62,6 +69,7 @@ class UdfpsKeyguardInteractorTest(flags: FlagsParameterization) : SysuiTestCase( val kosmos = testKosmos() val testScope = kosmos.testScope val keyguardRepository = kosmos.fakeKeyguardRepository + val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } val shadeRepository = kosmos.fakeShadeRepository val shadeTestUtil by lazy { kosmos.shadeTestUtil } @@ -132,8 +140,15 @@ class UdfpsKeyguardInteractorTest(flags: FlagsParameterization) : SysuiTestCase( assertThat(burnInOffsets?.x).isEqualTo(0) // WHEN we're in the middle of the doze amount change - keyguardRepository.setDozeAmount(.50f) - runCurrent() + if (SceneContainerFlag.isEnabled) { + sendTransitionSteps( + TransitionStep(to = DOZING, value = 0.0f, transitionState = STARTED), + TransitionStep(to = DOZING, value = 0.5f, transitionState = RUNNING), + ) + } else { + keyguardRepository.setDozeAmount(.50f) + runCurrent() + } // THEN burn in is updated (between 0 and the full offset) assertThat(burnInOffsets?.progress).isGreaterThan(0f) @@ -144,8 +159,14 @@ class UdfpsKeyguardInteractorTest(flags: FlagsParameterization) : SysuiTestCase( assertThat(burnInOffsets?.x).isLessThan(burnInXOffset) // WHEN we're fully dozing - keyguardRepository.setDozeAmount(1f) - runCurrent() + if (SceneContainerFlag.isEnabled) { + sendTransitionSteps( + TransitionStep(to = DOZING, value = 1.0f, transitionState = FINISHED) + ) + } else { + keyguardRepository.setDozeAmount(1f) + runCurrent() + } // THEN burn in offsets are updated to final current values (for the given time) assertThat(burnInOffsets?.progress).isEqualTo(burnInProgress) @@ -217,7 +238,9 @@ class UdfpsKeyguardInteractorTest(flags: FlagsParameterization) : SysuiTestCase( } private fun setAwake() { - keyguardRepository.setDozeAmount(0f) + if (!SceneContainerFlag.isEnabled) { + keyguardRepository.setDozeAmount(0f) + } keyguardRepository.dozeTimeTick() bouncerRepository.setAlternateVisible(false) @@ -225,4 +248,11 @@ class UdfpsKeyguardInteractorTest(flags: FlagsParameterization) : SysuiTestCase( bouncerRepository.setPrimaryShow(false) powerInteractor.setAwakeForTest() } + + private suspend fun sendTransitionSteps(vararg steps: TransitionStep) { + steps.forEach { step -> + keyguardTransitionRepository.sendTransitionStep(step) + testScope.runCurrent() + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt index e58cf152be93..79a303db079a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt @@ -85,12 +85,12 @@ class IconTilesInteractorTest : SysuiTestCase() { runCurrent() // Assert that the tile is removed from the large tiles after resizing - underTest.resize(largeTile) + underTest.resize(largeTile, toIcon = true) runCurrent() assertThat(latest).doesNotContain(largeTile) // Assert that the tile is added to the large tiles after resizing - underTest.resize(largeTile) + underTest.resize(largeTile, toIcon = false) runCurrent() assertThat(latest).contains(largeTile) } @@ -122,7 +122,7 @@ class IconTilesInteractorTest : SysuiTestCase() { val newTile = TileSpec.create("newTile") // Remove the large tile from the current tiles - underTest.resize(newTile) + underTest.resize(newTile, toIcon = false) runCurrent() // Assert that it's still small diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt index 484a8ff973c1..3910903af4aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt @@ -24,7 +24,6 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.model.GridCell -import com.android.systemui.qs.panels.ui.model.SpacerGridCell import com.android.systemui.qs.panels.ui.model.TileGridCell import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec @@ -39,13 +38,6 @@ class EditTileListStateTest : SysuiTestCase() { private val underTest = EditTileListState(TestEditTiles, 4) @Test - fun noDrag_listUnchanged() { - underTest.tiles.forEach { assertThat(it).isNotInstanceOf(SpacerGridCell::class.java) } - assertThat(underTest.tiles.map { (it as TileGridCell).tile.tileSpec }) - .containsExactly(*TestEditTiles.map { it.tile.tileSpec }.toTypedArray()) - } - - @Test fun startDrag_listHasSpacers() { underTest.onStarted(TestEditTiles[0]) @@ -109,16 +101,6 @@ class EditTileListStateTest : SysuiTestCase() { } @Test - fun droppedNewTile_spacersDisappear() { - underTest.onStarted(TestEditTiles[0]) - underTest.onDrop() - - assertThat(underTest.tiles.toStrings()).isEqualTo(listOf("a", "b", "c", "d", "e")) - assertThat(underTest.isMoving(TestEditTiles[0].tile.tileSpec)).isFalse() - assertThat(underTest.dragInProgress).isFalse() - } - - @Test fun movedTileOutOfBounds_tileDisappears() { underTest.onStarted(TestEditTiles[0]) underTest.movedOutOfBounds() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt index fa72d740b2f0..4acf3ee7878b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt @@ -27,23 +27,25 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class MutableSelectionStateTest : SysuiTestCase() { - private val underTest = MutableSelectionState() + private val underTest = MutableSelectionState({}, {}) @Test fun selectTile_isCorrectlySelected() { - assertThat(underTest.isSelected(TEST_SPEC)).isFalse() + assertThat(underTest.selection?.tileSpec).isNotEqualTo(TEST_SPEC) - underTest.select(TEST_SPEC) - assertThat(underTest.isSelected(TEST_SPEC)).isTrue() + underTest.select(TEST_SPEC, manual = true) + assertThat(underTest.selection?.tileSpec).isEqualTo(TEST_SPEC) + assertThat(underTest.selection?.manual).isTrue() underTest.unSelect() - assertThat(underTest.isSelected(TEST_SPEC)).isFalse() + assertThat(underTest.selection).isNull() val newSpec = TileSpec.create("newSpec") - underTest.select(TEST_SPEC) - underTest.select(newSpec) - assertThat(underTest.isSelected(TEST_SPEC)).isFalse() - assertThat(underTest.isSelected(newSpec)).isTrue() + underTest.select(TEST_SPEC, manual = true) + underTest.select(newSpec, manual = false) + assertThat(underTest.selection?.tileSpec).isNotEqualTo(TEST_SPEC) + assertThat(underTest.selection?.tileSpec).isEqualTo(newSpec) + assertThat(underTest.selection?.manual).isFalse() } @Test @@ -51,12 +53,12 @@ class MutableSelectionStateTest : SysuiTestCase() { assertThat(underTest.resizingState).isNull() // Resizing starts but no tile is selected - underTest.onResizingDragStart(TileWidths(0, 0, 1)) {} + underTest.onResizingDragStart(TileWidths(0, 0, 1)) assertThat(underTest.resizingState).isNull() // Resizing starts with a selected tile - underTest.select(TEST_SPEC) - underTest.onResizingDragStart(TileWidths(0, 0, 1)) {} + underTest.select(TEST_SPEC, manual = true) + underTest.onResizingDragStart(TileWidths(0, 0, 1)) assertThat(underTest.resizingState).isNotNull() } @@ -66,8 +68,8 @@ class MutableSelectionStateTest : SysuiTestCase() { val spec = TileSpec.create("testSpec") // Resizing starts with a selected tile - underTest.select(spec) - underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {} + underTest.select(spec, manual = true) + underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() underTest.onResizingDragEnd() @@ -77,8 +79,8 @@ class MutableSelectionStateTest : SysuiTestCase() { @Test fun unselect_clearsResizingState() { // Resizing starts with a selected tile - underTest.select(TEST_SPEC) - underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {} + underTest.select(TEST_SPEC, manual = true) + underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() underTest.unSelect() @@ -88,8 +90,8 @@ class MutableSelectionStateTest : SysuiTestCase() { @Test fun onResizingDrag_updatesResizingState() { // Resizing starts with a selected tile - underTest.select(TEST_SPEC) - underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {} + underTest.select(TEST_SPEC, manual = true) + underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() underTest.onResizingDrag(5f) @@ -105,11 +107,15 @@ class MutableSelectionStateTest : SysuiTestCase() { @Test fun onResizingDrag_receivesResizeCallback() { var resized = false - val onResize: () -> Unit = { resized = !resized } + val onResize: (TileSpec) -> Unit = { + assertThat(it).isEqualTo(TEST_SPEC) + resized = !resized + } + val underTest = MutableSelectionState(onResize = onResize, {}) // Resizing starts with a selected tile - underTest.select(TEST_SPEC) - underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10), onResize) + underTest.select(TEST_SPEC, true) + underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() // Drag under the threshold @@ -125,6 +131,37 @@ class MutableSelectionStateTest : SysuiTestCase() { assertThat(resized).isFalse() } + @Test + fun onResizingEnded_receivesResizeEndCallback() { + var resizeEnded = false + val onResizeEnd: (TileSpec) -> Unit = { resizeEnded = true } + val underTest = MutableSelectionState({}, onResizeEnd = onResizeEnd) + + // Resizing starts with a selected tile + underTest.select(TEST_SPEC, true) + underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) + + underTest.onResizingDragEnd() + assertThat(resizeEnded).isTrue() + } + + @Test + fun onResizingEnded_setsSelectionAutomatically() { + val underTest = MutableSelectionState({}, {}) + + // Resizing starts with a selected tile + underTest.select(TEST_SPEC, manual = true) + underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) + + // Assert the selection was manual + assertThat(underTest.selection?.manual).isTrue() + + underTest.onResizingDragEnd() + + // Assert the selection is no longer manual due to the resizing + assertThat(underTest.selection?.manual).isFalse() + } + companion object { private val TEST_SPEC = TileSpec.create("testSpec") } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt index f72a2e861be5..aefbc6b3b646 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt @@ -35,6 +35,7 @@ import com.android.systemui.jank.interactionJankMonitor import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor +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 com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus @@ -107,6 +108,7 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest uiEventLogger, { kosmos.interactionJankMonitor }, JavaAdapter(testScope.backgroundScope), + { kosmos.keyguardInteractor }, { kosmos.keyguardTransitionInteractor }, { kosmos.shadeInteractor }, { kosmos.deviceUnlockedInteractor }, @@ -139,6 +141,7 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest } @Test + @DisableSceneContainer fun testSetDozeAmountInternal_onlySetsOnce() { val listener = mock(StatusBarStateController.StateListener::class.java) underTest.addCallback(listener) @@ -190,6 +193,7 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest } @Test + @DisableSceneContainer fun testSetDozeAmount_immediatelyChangesDozeAmount_lockscreenTransitionFromAod() { // Put controller in AOD state underTest.setAndInstrumentDozeAmount(null, 1f, false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 425f16ec7da1..4adf6936b5ab 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -44,6 +44,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.BurnInModel import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB import com.android.systemui.keyguard.shared.model.KeyguardState.GONE @@ -363,6 +364,69 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S showLockscreen() assertThat(alpha).isEqualTo(1f) + // Go to dozing + keyguardTransitionRepository.sendTransitionSteps( + from = LOCKSCREEN, + to = DOZING, + testScope, + ) + assertThat(alpha).isEqualTo(1f) + + // Start transitioning to glanceable hub + val progress = 0.6f + kosmos.setTransition( + sceneTransition = Transition(from = Scenes.Lockscreen, to = Scenes.Communal), + stateTransition = + TransitionStep( + transitionState = TransitionState.STARTED, + from = DOZING, + to = GLANCEABLE_HUB, + value = 0f, + ), + ) + runCurrent() + kosmos.setTransition( + sceneTransition = + Transition( + from = Scenes.Lockscreen, + to = Scenes.Communal, + progress = flowOf(progress), + ), + stateTransition = + TransitionStep( + transitionState = TransitionState.RUNNING, + from = DOZING, + to = GLANCEABLE_HUB, + value = progress, + ), + ) + runCurrent() + // Keep notifications hidden during the transition from dream to hub + assertThat(alpha).isEqualTo(0) + + // Finish transition to glanceable hub + kosmos.setTransition( + sceneTransition = Idle(Scenes.Communal), + stateTransition = + TransitionStep( + transitionState = TransitionState.FINISHED, + from = DOZING, + to = GLANCEABLE_HUB, + value = 1f, + ), + ) + assertThat(alpha).isEqualTo(0f) + } + + @Test + fun glanceableHubAlpha_dozingToHub() = + testScope.runTest { + val alpha by collectLastValue(underTest.glanceableHubAlpha) + + // Start on lockscreen, notifications should be unhidden. + showLockscreen() + assertThat(alpha).isEqualTo(1f) + // Transition to dream, notifications should be hidden so that transition // from dream->hub doesn't cause notification flicker. showDream() diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt index 873d1b3cc03d..4185aed3095c 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt @@ -86,6 +86,9 @@ sealed class AuthMethodBouncerViewModel( _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED clearInput() + if (authenticationResult == AuthenticationResult.SUCCEEDED) { + onSuccessfulAuthentication() + } } awaitCancellation() } @@ -116,6 +119,9 @@ sealed class AuthMethodBouncerViewModel( /** Returns the input entered so far. */ protected abstract fun getInput(): List<Any> + /** Invoked after a successful authentication. */ + protected open fun onSuccessfulAuthentication() = Unit + /** Perform authentication result haptics */ private fun performAuthenticationHapticFeedback(result: AuthenticationResult) { if (result == AuthenticationResult.SKIPPED) return diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt index 2493cf1a101b..1427d787ea86 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt @@ -81,50 +81,59 @@ constructor( val selectedUserId: StateFlow<Int> = _selectedUserId.asStateFlow() private val requests = Channel<Request>(Channel.BUFFERED) + private var wasSuccessfullyAuthenticated = false override suspend fun onActivated(): Nothing { - coroutineScope { - launch { super.onActivated() } - launch { - requests.receiveAsFlow().collect { request -> - when (request) { - is OnImeSwitcherButtonClicked -> { - inputMethodInteractor.showInputMethodPicker( - displayId = request.displayId, - showAuxiliarySubtypes = false, - ) - } - is OnImeDismissed -> { - interactor.onImeHiddenByUser() + try { + coroutineScope { + launch { super.onActivated() } + launch { + requests.receiveAsFlow().collect { request -> + when (request) { + is OnImeSwitcherButtonClicked -> { + inputMethodInteractor.showInputMethodPicker( + displayId = request.displayId, + showAuxiliarySubtypes = false, + ) + } + is OnImeDismissed -> { + interactor.onImeHiddenByUser() + } } } } + launch { + combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus -> + hasInput && !hasFocus && !wasSuccessfullyAuthenticated + } + .collect { _isTextFieldFocusRequested.value = it } + } + launch { + selectedUserInteractor.selectedUser.collect { _selectedUserId.value = it } + } + launch { + // Re-fetch the currently-enabled IMEs whenever the selected user changes, and + // whenever + // the UI subscribes to the `isImeSwitcherButtonVisible` flow. + combine( + // InputMethodManagerService sometimes takes + // some time to update its internal state when the + // selected user changes. + // As a workaround, delay fetching the IME info. + selectedUserInteractor.selectedUser.onEach { + delay(DELAY_TO_FETCH_IMES) + }, + _isImeSwitcherButtonVisible.onSubscriberAdded(), + ) { selectedUserId, _ -> + inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId) + } + .collect { _isImeSwitcherButtonVisible.value = it } + } + awaitCancellation() } - launch { - combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus -> - hasInput && !hasFocus - } - .collect { _isTextFieldFocusRequested.value = it } - } - launch { selectedUserInteractor.selectedUser.collect { _selectedUserId.value = it } } - launch { - // Re-fetch the currently-enabled IMEs whenever the selected user changes, and - // whenever - // the UI subscribes to the `isImeSwitcherButtonVisible` flow. - combine( - // InputMethodManagerService sometimes takes some time to update its - // internal - // state when the selected user changes. As a workaround, delay fetching the - // IME - // info. - selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) }, - _isImeSwitcherButtonVisible.onSubscriberAdded() - ) { selectedUserId, _ -> - inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId) - } - .collect { _isImeSwitcherButtonVisible.value = it } - } - awaitCancellation() + } finally { + // reset whenever the view model is "deactivated" + wasSuccessfullyAuthenticated = false } } @@ -141,6 +150,10 @@ constructor( return _password.value.toCharArray().toList() } + override fun onSuccessfulAuthentication() { + wasSuccessfullyAuthenticated = true + } + /** Notifies that the user has changed the password input. */ fun onPasswordInputChanged(newPassword: String) { if (newPassword.isNotEmpty()) { diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt index 12597a7679fa..99c026cb028f 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt @@ -24,6 +24,7 @@ import android.text.TextUtils import android.util.Log import android.util.Size import android.view.textclassifier.TextLinks +import com.android.systemui.Flags.clipboardUseDescriptionMimetype import com.android.systemui.res.R import java.io.IOException @@ -70,11 +71,11 @@ data class ClipboardModel( context: Context, utils: ClipboardOverlayUtils, clipData: ClipData, - source: String + source: String, ): ClipboardModel { val sensitive = clipData.description?.extras?.getBoolean(EXTRA_IS_SENSITIVE) ?: false val item = clipData.getItemAt(0)!! - val type = getType(context, item) + val type = getType(context, item, clipData.description.getMimeType(0)) val remote = utils.isRemoteCopy(context, clipData, source) return ClipboardModel( clipData, @@ -84,18 +85,26 @@ data class ClipboardModel( item.textLinks, item.uri, sensitive, - remote + remote, ) } - private fun getType(context: Context, item: ClipData.Item): Type { + private fun getType(context: Context, item: ClipData.Item, mimeType: String): Type { return if (!TextUtils.isEmpty(item.text)) { Type.TEXT } else if (item.uri != null) { - if (context.contentResolver.getType(item.uri)?.startsWith("image") == true) { - Type.IMAGE + if (clipboardUseDescriptionMimetype()) { + if (mimeType.startsWith("image")) { + Type.IMAGE + } else { + Type.URI + } } else { - Type.URI + if (context.contentResolver.getType(item.uri)?.startsWith("image") == true) { + Type.IMAGE + } else { + Type.URI + } } } else { Type.OTHER @@ -107,6 +116,6 @@ data class ClipboardModel( TEXT, IMAGE, URI, - OTHER + OTHER, } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt index 04393feaae37..1bd541e1088a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt @@ -65,7 +65,9 @@ constructor( keyguardTransitionInteractor .transitionValue(Scenes.Communal, KeyguardState.GLANCEABLE_HUB) .map { it == 1f }, - not(keyguardInteractor.isDreaming), + // Use isDreamingAny because isDreaming is false in doze and doesn't change again + // when the screen turns on, which causes the dream to not start underneath the hub. + not(keyguardInteractor.isDreamingAny), // TODO(b/362830856): Remove this workaround. keyguardInteractor.isKeyguardShowing, not(communalSceneInteractor.isLaunchingWidget), diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt index c7538bb4f696..905eda14e2d5 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt @@ -87,7 +87,9 @@ constructor( */ private val nextKeyguardStateInternal = combine( - keyguardInteractor.isAbleToDream, + // Don't use delayed dreaming signal as otherwise we might go to occluded or lock + // screen when closing hub if dream just started under the hub. + keyguardInteractor.isDreamingWithOverlay, keyguardInteractor.isKeyguardOccluded, keyguardInteractor.isKeyguardGoingAway, keyguardInteractor.isKeyguardShowing, @@ -156,7 +158,7 @@ constructor( private suspend fun handleIdle( prevTransition: ObservableTransitionState, - idle: ObservableTransitionState.Idle + idle: ObservableTransitionState.Idle, ) { if ( prevTransition is ObservableTransitionState.Transition && @@ -186,7 +188,7 @@ constructor( internalTransitionInteractor.updateTransition( currentTransitionId!!, 1f, - TransitionState.FINISHED + TransitionState.FINISHED, ) resetTransitionData() } @@ -204,7 +206,7 @@ constructor( internalTransitionInteractor.updateTransition( currentTransitionId!!, 1f, - TransitionState.FINISHED + TransitionState.FINISHED, ) resetTransitionData() } @@ -217,7 +219,7 @@ constructor( private suspend fun handleTransition( prevTransition: ObservableTransitionState, - transition: ObservableTransitionState.Transition + transition: ObservableTransitionState.Transition, ) { if ( prevTransition.isTransitioning(from = transition.fromContent, to = transition.toContent) @@ -295,7 +297,7 @@ constructor( internalTransitionInteractor.updateTransition( currentTransitionId!!, progress.coerceIn(0f, 1f), - TransitionState.RUNNING + TransitionState.RUNNING, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt index 28db3b861278..f90f02aad892 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt @@ -70,7 +70,14 @@ constructor( ) { private val keyguardOccludedByApp: Flow<Boolean> = if (KeyguardWmStateRefactor.isEnabled) { - keyguardTransitionInteractor.currentKeyguardState.map { it == KeyguardState.OCCLUDED } + combine( + keyguardTransitionInteractor.currentKeyguardState, + communalSceneInteractor.isIdleOnCommunal, + ::Pair, + ) + .map { (currentState, isIdleOnCommunal) -> + currentState == KeyguardState.OCCLUDED && !isIdleOnCommunal + } } else { combine( keyguardInteractor.isKeyguardOccluded, @@ -120,7 +127,7 @@ constructor( // On fingerprint success when the screen is on and not dreaming, go to the home screen fingerprintUnlockSuccessEvents .sample( - combine(powerInteractor.isInteractive, keyguardInteractor.isDreaming, ::Pair), + combine(powerInteractor.isInteractive, keyguardInteractor.isDreaming, ::Pair) ) .collect { (interactive, dreaming) -> if (interactive && !dreaming) { @@ -148,7 +155,7 @@ constructor( } }, /* cancel= */ null, - /* afterKeyguardGone */ false + /* afterKeyguardGone */ false, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt index 77c54ec1eac3..3992c3fb70b0 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt @@ -39,6 +39,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import com.android.app.tracing.coroutines.createCoroutineTracingContext class HomeControlsDreamService @Inject @@ -53,7 +54,7 @@ constructor( ) : DreamService() { private val serviceJob = SupervisorJob() - private val serviceScope = CoroutineScope(bgDispatcher + serviceJob) + private val serviceScope = CoroutineScope(bgDispatcher + serviceJob + createCoroutineTracingContext("HomeControlsDreamService")) private val logger = DreamLogger(logBuffer, TAG) private lateinit var taskFragmentComponent: TaskFragmentComponent private val wakeLock: WakeLock by lazy { diff --git a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt index 7e2c9f89fa67..4caf95b707b1 100644 --- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt +++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.education.dagger +import com.android.app.tracing.coroutines.createCoroutineTracingContext import com.android.systemui.CoreStartable import com.android.systemui.Flags import com.android.systemui.contextualeducation.GestureType @@ -56,7 +57,7 @@ interface ContextualEducationModule { fun provideEduDataStoreScope( @Background bgDispatcher: CoroutineDispatcher ): CoroutineScope { - return CoroutineScope(bgDispatcher + SupervisorJob()) + return CoroutineScope(bgDispatcher + SupervisorJob() + createCoroutineTracingContext("EduDataStoreScope")) } @EduClock diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index ba23eb341b89..0a38ce07a798 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -3314,7 +3314,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, setShowingLocked(false, "onKeyguardExitFinished: " + reason); mWakeAndUnlocking = false; - mDismissCallbackRegistry.notifyDismissSucceeded(); + + if (!KeyguardWmStateRefactor.isEnabled()) { + mDismissCallbackRegistry.notifyDismissSucceeded(); + } + resetKeyguardDonePendingLocked(); mHideAnimationRun = false; adjustStatusBarLocked(); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 49303e089036..130242f55600 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -58,6 +58,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapLatest @@ -486,19 +487,34 @@ constructor( override val isDreaming: MutableStateFlow<Boolean> = MutableStateFlow(false) - override val linearDozeAmount: Flow<Float> = conflatedCallbackFlow { - val callback = - object : StatusBarStateController.StateListener { - override fun onDozeAmountChanged(linear: Float, eased: Float) { - trySendWithFailureLogging(linear, TAG, "updated dozeAmount") - } - } + private val _preSceneLinearDozeAmount: Flow<Float> = + if (SceneContainerFlag.isEnabled) { + emptyFlow() + } else { + conflatedCallbackFlow { + val callback = + object : StatusBarStateController.StateListener { + override fun onDozeAmountChanged(linear: Float, eased: Float) { + trySendWithFailureLogging(linear, TAG, "updated dozeAmount") + } + } - statusBarStateController.addCallback(callback) - trySendWithFailureLogging(statusBarStateController.dozeAmount, TAG, "initial dozeAmount") + statusBarStateController.addCallback(callback) + trySendWithFailureLogging( + statusBarStateController.dozeAmount, + TAG, + "initial dozeAmount" + ) - awaitClose { statusBarStateController.removeCallback(callback) } - } + awaitClose { statusBarStateController.removeCallback(callback) } + } + } + + override val linearDozeAmount: Flow<Float> + get() { + SceneContainerFlag.assertInLegacyMode() + return _preSceneLinearDozeAmount + } override val dozeTransitionModel: Flow<DozeTransitionModel> = conflatedCallbackFlow { val callback = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index b0820a747e17..8c7fe5f87a3f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -150,7 +150,9 @@ constructor( if (!SceneContainerFlag.isEnabled) { startTransitionTo(KeyguardState.GLANCEABLE_HUB) } - } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) { + } else if (isCommunalAvailable && dreamManager.canStartDreaming(false)) { + // Using false for isScreenOn as canStartDreaming returns false if any + // dream, including doze, is active. // This case handles tapping the power button to transition through // dream -> off -> hub. if (!SceneContainerFlag.isEnabled) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 2434b29c0cdd..9a0a85823929 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -17,9 +17,12 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.app.DreamManager import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launch import com.android.systemui.Flags.communalSceneKtfRefactor +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.shared.model.CommunalScenes @@ -60,10 +63,12 @@ constructor( @Main mainDispatcher: CoroutineDispatcher, keyguardInteractor: KeyguardInteractor, private val glanceableHubTransitions: GlanceableHubTransitions, + private val communalInteractor: CommunalInteractor, private val communalSceneInteractor: CommunalSceneInteractor, private val communalSettingsInteractor: CommunalSettingsInteractor, powerInteractor: PowerInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, + private val dreamManager: DreamManager, private val deviceEntryInteractor: DeviceEntryInteractor, ) : TransitionInteractor( @@ -76,6 +81,7 @@ constructor( keyguardInteractor = keyguardInteractor, ) { + @SuppressLint("MissingPermission") override fun start() { listenForDreamingToAlternateBouncer() listenForDreamingToOccluded() @@ -86,6 +92,8 @@ constructor( listenForTransitionToCamera(scope, keyguardInteractor) if (!communalSceneKtfRefactor()) { listenForDreamingToGlanceableHub() + } else { + listenForDreamingToGlanceableHubFromPowerButton() } listenForDreamingToPrimaryBouncer() } @@ -112,6 +120,34 @@ constructor( } } + /** + * Normally when pressing power button from the dream, the devices goes from DREAMING to DOZING, + * then [FromDozingTransitionInteractor] handles the transition to GLANCEABLE_HUB. However if + * the power button is pressed quickly, we may need to go directly from DREAMING to + * GLANCEABLE_HUB as the transition to DOZING has not occurred yet. + */ + @SuppressLint("MissingPermission") + private fun listenForDreamingToGlanceableHubFromPowerButton() { + if (!communalSettingsInteractor.isCommunalFlagEnabled()) return + if (SceneContainerFlag.isEnabled) return + scope.launch { + powerInteractor.isAwake + .debounce(50L) + .filterRelevantKeyguardStateAnd { isAwake -> isAwake } + .sample(communalInteractor.isCommunalAvailable) + .collect { isCommunalAvailable -> + if (isCommunalAvailable && dreamManager.canStartDreaming(false)) { + // This case handles tapping the power button to transition through + // dream -> off -> hub. + communalSceneInteractor.snapToScene( + newScene = CommunalScenes.Communal, + loggingReason = "from dreaming to hub", + ) + } + } + } + } + private fun listenForDreamingToPrimaryBouncer() { // TODO(b/336576536): Check if adaptation for scene framework is needed if (SceneContainerFlag.isEnabled) return @@ -144,7 +180,7 @@ constructor( } else { startTransitionTo( KeyguardState.LOCKSCREEN, - ownerReason = "Dream has ended and device is awake" + ownerReason = "Dream has ended and device is awake", ) } } @@ -158,15 +194,14 @@ constructor( scope.launch { combine( keyguardInteractor.isKeyguardOccluded, - keyguardInteractor.isAbleToDream - // Debounce the dreaming signal since there is a race condition between - // the occluded and dreaming signals. We therefore add a small delay - // to give enough time for occluded to flip to false when the dream - // ends, to avoid transitioning to OCCLUDED erroneously when exiting - // the dream. - .debounce(100.milliseconds), - ::Pair + keyguardInteractor.isDreaming, + ::Pair, ) + // Debounce signals since there is a race condition between the occluded and + // dreaming signals when starting or stopping dreaming. We therefore add a small + // delay to give enough time for occluded to flip to false when the dream + // ends, to avoid transitioning to OCCLUDED erroneously when exiting the dream. + .debounce(100.milliseconds) .filterRelevantKeyguardStateAnd { (isOccluded, isDreaming) -> isOccluded && !isDreaming } @@ -194,12 +229,12 @@ constructor( if (dismissable) { startTransitionTo( KeyguardState.GONE, - ownerReason = "No longer dreaming; dismissable" + ownerReason = "No longer dreaming; dismissable", ) } else { startTransitionTo( KeyguardState.LOCKSCREEN, - ownerReason = "No longer dreaming" + ownerReason = "No longer dreaming", ) } } 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 index 7759298cb32a..6b6a3dce630a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -202,15 +202,15 @@ constructor( scope.launch { combine( keyguardInteractor.isKeyguardOccluded, - keyguardInteractor.isAbleToDream - // Debounce the dreaming signal since there is a race condition between - // the occluded and dreaming signals. We therefore add a small delay - // to give enough time for occluded to flip to false when the dream - // ends, to avoid transitioning to OCCLUDED erroneously when exiting - // the dream. - .debounce(100.milliseconds), + keyguardInteractor.isDreaming, ::Pair, ) + // Debounce signals since there is a race condition between the occluded and + // dreaming signals when starting or stopping dreaming. We therefore add a small + // delay to give enough time for occluded to flip to false when the dream + // ends, to avoid transitioning to OCCLUDED erroneously when exiting the dream + // or when the dream starts underneath the hub. + .debounce(200.milliseconds) .sampleFilter( // When launching activities from widgets on the hub, we have a // custom occlusion animation. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt index 4457f1dfaf09..9b9bdd1bde9b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt @@ -23,12 +23,10 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.DismissCallbackRegistry -import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.keyguard.shared.model.DismissAction import com.android.systemui.keyguard.shared.model.KeyguardDone -import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.kotlin.Utils.Companion.toQuad @@ -37,7 +35,6 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -59,7 +56,6 @@ constructor( trustRepository: TrustRepository, alternateBouncerInteractor: AlternateBouncerInteractor, powerInteractor: PowerInteractor, - keyguardTransitionInteractor: KeyguardTransitionInteractor, ) { /* * Updates when a biometric has authenticated the device and is requesting to dismiss @@ -165,14 +161,4 @@ constructor( } } } - - init { - if (KeyguardWmStateRefactor.isEnabled) { - scope.launch { - keyguardTransitionInteractor.currentKeyguardState - .filter { it == KeyguardState.GONE } - .collect { dismissCallbackRegistry.notifyDismissSucceeded() } - } - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index e444092bd175..e6ee11215595 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -140,12 +140,6 @@ constructor( _notificationPlaceholderBounds.value = position } - /** - * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at - * all. - */ - val dozeAmount: Flow<Float> = repository.linearDozeAmount - /** Whether the system is in doze mode. */ val isDozing: StateFlow<Boolean> = repository.isDozing @@ -155,6 +149,23 @@ constructor( /** Whether Always-on Display mode is available. */ val isAodAvailable: StateFlow<Boolean> = repository.isAodAvailable + /** + * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at + * all. + */ + val dozeAmount: Flow<Float> = + if (SceneContainerFlag.isEnabled) { + isAodAvailable.flatMapLatest { isAodAvailable -> + if (isAodAvailable) { + keyguardTransitionInteractor.transitionValue(AOD) + } else { + keyguardTransitionInteractor.transitionValue(DOZING) + } + } + } else { + repository.linearDozeAmount + } + /** Doze transition information. */ val dozeTransitionModel: Flow<DozeTransitionModel> = repository.dozeTransitionModel @@ -164,8 +175,8 @@ constructor( val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay /** - * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true, - * but not vice-versa. Also accounts for [isDreamingWithOverlay] + * Whether the system is dreaming. [KeyguardRepository.isDreaming] will be always be true when + * [isDozing] is true, but not vice-versa. Also accounts for [isDreamingWithOverlay]. */ val isDreaming: StateFlow<Boolean> = merge(repository.isDreaming, repository.isDreamingWithOverlay) @@ -175,6 +186,9 @@ constructor( initialValue = false, ) + /** Whether any dreaming is running, including the doze dream. */ + val isDreamingAny: Flow<Boolean> = repository.isDreaming + /** Whether the system is dreaming and the active dream is hosted in lockscreen */ val isActiveDreamLockscreenHosted: StateFlow<Boolean> = repository.isActiveDreamLockscreenHosted diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt index 7fd348b8b40e..6fe4ff5122d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.domain.interactor +import android.app.trust.TrustManager import android.os.DeadObjectException import android.os.RemoteException import com.android.internal.policy.IKeyguardStateCallback @@ -24,6 +25,7 @@ import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -32,7 +34,6 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -53,6 +54,9 @@ constructor( private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val trustInteractor: TrustInteractor, private val simBouncerInteractor: SimBouncerInteractor, + private val dismissCallbackRegistry: DismissCallbackRegistry, + private val wmLockscreenVisibilityInteractor: WindowManagerLockscreenVisibilityInteractor, + private val trustManager: TrustManager, ) : CoreStartable { private val callbacks = mutableListOf<IKeyguardStateCallback>() @@ -62,28 +66,31 @@ constructor( } applicationScope.launch { - combine( - selectedUserInteractor.selectedUser, - keyguardTransitionInteractor.currentKeyguardState, - keyguardTransitionInteractor.startedKeyguardTransitionStep, - ::Triple, - ) - .collectLatest { (selectedUser, _, _) -> - val iterator = callbacks.iterator() - withContext(backgroundDispatcher) { - while (iterator.hasNext()) { - val callback = iterator.next() - try { - callback.onShowingStateChanged(!isIdleInGone(), selectedUser) - callback.onInputRestrictedStateChanged(!isIdleInGone()) - } catch (e: RemoteException) { - if (e is DeadObjectException) { - iterator.remove() - } + wmLockscreenVisibilityInteractor.lockscreenVisibility.collectLatest { visible -> + val iterator = callbacks.iterator() + withContext(backgroundDispatcher) { + while (iterator.hasNext()) { + val callback = iterator.next() + try { + callback.onShowingStateChanged( + visible, + selectedUserInteractor.getSelectedUserId(), + ) + callback.onInputRestrictedStateChanged(visible) + + trustManager.reportKeyguardShowingChanged() + + if (!visible) { + dismissCallbackRegistry.notifyDismissSucceeded() + } + } catch (e: RemoteException) { + if (e is DeadObjectException) { + iterator.remove() } } } } + } } applicationScope.launch { 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 0b8f7417a49d..cef9a4eaf2bd 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 @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.preview +import com.android.app.tracing.coroutines.createCoroutineTracingContext import android.app.WallpaperColors import android.content.BroadcastReceiver import android.content.Context @@ -187,7 +188,7 @@ constructor( private var themeStyle: Style? = null init { - coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job()) + coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job() + createCoroutineTracingContext("KeyguardPreviewRenderer")) disposables += DisposableHandle { coroutineScope.cancel() } clockController.setFallbackWeatherData(WeatherData.getPlaceholderWeatherData()) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToGlanceableHubTransitionViewModel.kt index aee34e1e713b..1e42e196bbc7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToGlanceableHubTransitionViewModel.kt @@ -26,6 +26,7 @@ import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.scene.shared.model.Scenes import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf @SysUISingleton class DozingToGlanceableHubTransitionViewModel @@ -35,10 +36,16 @@ constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTra animationFlow .setup( duration = TO_GLANCEABLE_HUB_DURATION, - edge = Edge.create(DOZING, Scenes.Communal) + edge = Edge.create(DOZING, Scenes.Communal), ) .setupWithoutSceneContainer(edge = Edge.create(DOZING, GLANCEABLE_HUB)) override val deviceEntryParentViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(1f) + + /** + * Hide notifications when transitioning directly from dozing to hub, such as when pressing + * power button when dozing and docked. + */ + val notificationAlpha: Flow<Float> = flowOf(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt index 4b62eab08775..0d55709e94d6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt @@ -179,14 +179,6 @@ constructor( } else { button(KeyguardQuickAffordancePosition.BOTTOM_START) } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = - KeyguardQuickAffordanceViewModel( - slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId() - ), - ) /** An observable for the view-model of the "end button" quick affordance. */ val endButton: Flow<KeyguardQuickAffordanceViewModel> = @@ -200,14 +192,6 @@ constructor( } else { button(KeyguardQuickAffordancePosition.BOTTOM_END) } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = - KeyguardQuickAffordanceViewModel( - slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId() - ), - ) /** * Notifies that a slot with the given ID has been selected in the preview experience that is diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt index c2b5d98699b4..555969859a1f 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt @@ -26,7 +26,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.lifecycleScope import com.android.app.tracing.coroutines.createCoroutineTracingContext -import com.android.app.tracing.coroutines.launch +import com.android.app.tracing.coroutines.traceCoroutine import com.android.systemui.Flags.coroutineTracing import com.android.systemui.util.Assert import com.android.systemui.util.Compile @@ -45,6 +45,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch /** * Runs the given [block] every time the [View] becomes attached (or immediately after calling this @@ -137,7 +138,7 @@ private fun createLifecycleOwnerAndRun( ): ViewLifecycleOwner { return ViewLifecycleOwner(view).apply { onCreate() - lifecycleScope.launch(nameForTrace, coroutineContext) { block(view) } + lifecycleScope.launch(coroutineContext) { traceCoroutine(nameForTrace) { block(view) } } } } @@ -367,7 +368,8 @@ private val ViewTreeObserver.isWindowVisible * an extension function, and plumbing dagger-injected instances for static usage has little * benefit. */ -private val MAIN_DISPATCHER_SINGLETON = Dispatchers.Main + createCoroutineTracingContext() +private val MAIN_DISPATCHER_SINGLETON = + Dispatchers.Main + createCoroutineTracingContext("RepeatWhenAttached") private const val DEFAULT_TRACE_NAME = "repeatWhenAttached" private const val CURRENT_CLASS_NAME = "com.android.systemui.lifecycle.RepeatWhenAttachedKt" private const val JAVA_ADAPTER_CLASS_NAME = "com.android.systemui.util.kotlin.JavaAdapterKt" diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt index 64402052c984..544dbddeb3f0 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt @@ -16,6 +16,7 @@ package com.android.systemui.mediaprojection.appselector +import com.android.app.tracing.coroutines.createCoroutineTracingContext import android.app.Activity import android.content.ComponentName import android.content.Context @@ -133,7 +134,7 @@ interface MediaProjectionAppSelectorModule { @MediaProjectionAppSelector @MediaProjectionAppSelectorScope fun provideCoroutineScope(@Application applicationScope: CoroutineScope): CoroutineScope = - CoroutineScope(applicationScope.coroutineContext + SupervisorJob()) + CoroutineScope(applicationScope.coroutineContext + SupervisorJob() + createCoroutineTracingContext("MediaProjectionAppSelectorScope")) } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java index b3c697e06a92..1216a8879751 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java @@ -551,6 +551,12 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, } @Override + public void appTransitionStarting(int displayId, long startTime, long duration, + boolean forced) { + appTransitionPending(false); + } + + @Override public void appTransitionCancelled(int displayId) { appTransitionPending(false); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt index 02a607db0a64..fc59a50e88ad 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt @@ -40,7 +40,7 @@ constructor( private val currentTilesInteractor: CurrentTilesInteractor, private val preferencesInteractor: QSPreferencesInteractor, @PanelsLog private val logBuffer: LogBuffer, - @Application private val applicationScope: CoroutineScope + @Application private val applicationScope: CoroutineScope, ) { val largeTilesSpecs = @@ -64,14 +64,15 @@ constructor( fun isIconTile(spec: TileSpec): Boolean = !largeTilesSpecs.value.contains(spec) - fun resize(spec: TileSpec) { + fun resize(spec: TileSpec, toIcon: Boolean) { if (!isCurrent(spec)) { return } - if (largeTilesSpecs.value.contains(spec)) { + val isIcon = !largeTilesSpecs.value.contains(spec) + if (toIcon && !isIcon) { preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value - spec) - } else { + } else if (!toIcon && isIcon) { preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value + spec) } } @@ -85,7 +86,7 @@ constructor( LOG_BUFFER_LARGE_TILES_SPECS_CHANGE_TAG, LogLevel.DEBUG, { str1 = specs.toString() }, - { "Large tiles change: $str1" } + { "Large tiles change: $str1" }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt index a4f977b08b70..770fd785723a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt @@ -60,10 +60,37 @@ class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val c return _tiles.filterIsInstance<TileGridCell>().map { it.tile.tileSpec } } - fun indexOf(tileSpec: TileSpec): Int { + private fun indexOf(tileSpec: TileSpec): Int { return _tiles.indexOfFirst { it is TileGridCell && it.tile.tileSpec == tileSpec } } + /** + * Whether the tile with this [TileSpec] is currently an icon in the [EditTileListState] + * + * @return true if the tile is an icon, false if it's large, null if the tile isn't in the list + */ + fun isIcon(tileSpec: TileSpec): Boolean? { + val index = indexOf(tileSpec) + return if (index != -1) { + val cell = _tiles[index] + cell as TileGridCell + return cell.isIcon + } else { + null + } + } + + /** Toggle the size of the tile corresponding to the [TileSpec] */ + fun toggleSize(tileSpec: TileSpec) { + val fromIndex = indexOf(tileSpec) + if (fromIndex != -1) { + val cell = _tiles.removeAt(fromIndex) + cell as TileGridCell + _tiles.add(fromIndex, cell.copy(width = if (cell.isIcon) 2 else 1)) + regenerateGrid(fromIndex) + } + } + override fun isMoving(tileSpec: TileSpec): Boolean { return _draggedCell.value?.let { it.tile.tileSpec == tileSpec } ?: false } @@ -71,8 +98,8 @@ class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val c override fun onStarted(cell: SizedTile<EditTileViewModel>) { _draggedCell.value = cell - // Add visible spacers to the grid to indicate where the user can move a tile - regenerateGrid(includeSpacers = true) + // Add spacers to the grid to indicate where the user can move a tile + regenerateGrid() } override fun onMoved(target: Int, insertAfter: Boolean) { @@ -86,7 +113,7 @@ class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val c val insertionIndex = if (insertAfter) target + 1 else target if (fromIndex != -1) { val cell = _tiles.removeAt(fromIndex) - regenerateGrid(includeSpacers = true) + regenerateGrid() _tiles.add(insertionIndex.coerceIn(0, _tiles.size), cell) } else { // Add the tile with a temporary row which will get reassigned when @@ -94,7 +121,7 @@ class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val c _tiles.add(insertionIndex.coerceIn(0, _tiles.size), TileGridCell(draggedTile, 0)) } - regenerateGrid(includeSpacers = true) + regenerateGrid() } override fun movedOutOfBounds() { @@ -109,12 +136,27 @@ class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val c _draggedCell.value = null // Remove the spacers - regenerateGrid(includeSpacers = false) + regenerateGrid() + } + + /** Regenerate the list of [GridCell] with their new potential rows */ + private fun regenerateGrid() { + _tiles.filterIsInstance<TileGridCell>().toGridCells(columns).let { + _tiles.clear() + _tiles.addAll(it) + } } - private fun regenerateGrid(includeSpacers: Boolean) { - _tiles.filterIsInstance<TileGridCell>().toGridCells(columns, includeSpacers).let { + /** + * Regenerate the list of [GridCell] with their new potential rows from [fromIndex], leaving + * cells before that untouched. + */ + private fun regenerateGrid(fromIndex: Int) { + val fromRow = _tiles[fromIndex].row + val (pre, post) = _tiles.partition { it.row < fromRow } + post.filterIsInstance<TileGridCell>().toGridCells(columns, startingRow = fromRow).let { _tiles.clear() + _tiles.addAll(pre) _tiles.addAll(it) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index 0e76e18fab8e..30bafaece923 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -132,15 +132,23 @@ object TileType @Composable fun DefaultEditTileGrid( - currentListState: EditTileListState, + listState: EditTileListState, otherTiles: List<SizedTile<EditTileViewModel>>, columns: Int, modifier: Modifier, onRemoveTile: (TileSpec) -> Unit, onSetTiles: (List<TileSpec>) -> Unit, - onResize: (TileSpec) -> Unit, + onResize: (TileSpec, toIcon: Boolean) -> Unit, ) { - val selectionState = rememberSelectionState() + val currentListState by rememberUpdatedState(listState) + val selectionState = + rememberSelectionState( + onResize = { currentListState.toggleSize(it) }, + onResizeEnd = { spec -> + // Commit the size currently in the list + currentListState.isIcon(spec)?.let { onResize(spec, it) } + }, + ) CompositionLocalProvider(LocalOverscrollConfiguration provides null) { Column( @@ -149,11 +157,11 @@ fun DefaultEditTileGrid( modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()), ) { AnimatedContent( - targetState = currentListState.dragInProgress, + targetState = listState.dragInProgress, modifier = Modifier.wrapContentSize(), label = "", ) { dragIsInProgress -> - EditGridHeader(Modifier.dragAndDropRemoveZone(currentListState, onRemoveTile)) { + EditGridHeader(Modifier.dragAndDropRemoveZone(listState, onRemoveTile)) { if (dragIsInProgress) { RemoveTileTarget() } else { @@ -162,11 +170,11 @@ fun DefaultEditTileGrid( } } - CurrentTilesGrid(currentListState, selectionState, columns, onResize, onSetTiles) + CurrentTilesGrid(listState, selectionState, columns, onResize, onSetTiles) // Hide available tiles when dragging AnimatedVisibility( - visible = !currentListState.dragInProgress, + visible = !listState.dragInProgress, enter = fadeIn(), exit = fadeOut(), ) { @@ -177,7 +185,7 @@ fun DefaultEditTileGrid( ) { EditGridHeader { Text(text = "Hold and drag to add tiles.") } - AvailableTileGrid(otherTiles, selectionState, columns, currentListState) + AvailableTileGrid(otherTiles, selectionState, columns, listState) } } @@ -186,7 +194,7 @@ fun DefaultEditTileGrid( modifier = Modifier.fillMaxWidth() .weight(1f) - .dragAndDropRemoveZone(currentListState, onRemoveTile) + .dragAndDropRemoveZone(listState, onRemoveTile) ) } } @@ -229,7 +237,7 @@ private fun CurrentTilesGrid( listState: EditTileListState, selectionState: MutableSelectionState, columns: Int, - onResize: (TileSpec) -> Unit, + onResize: (TileSpec, toIcon: Boolean) -> Unit, onSetTiles: (List<TileSpec>) -> Unit, ) { val currentListState by rememberUpdatedState(listState) @@ -242,19 +250,6 @@ private fun CurrentTilesGrid( ) val gridState = rememberLazyGridState() var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) } - var droppedSpec by remember { mutableStateOf<TileSpec?>(null) } - - // Select the tile that was dropped. A delay is introduced to avoid clipping issues on the - // selected border and resizing handle, as well as letting the selection animation play. - LaunchedEffect(droppedSpec) { - droppedSpec?.let { - delay(200) - selectionState.select(it) - - // Reset droppedSpec in case a tile is dropped twice in a row - droppedSpec = null - } - } TileLazyGrid( state = gridState, @@ -270,14 +265,17 @@ private fun CurrentTilesGrid( ) .dragAndDropTileList(gridState, { gridContentOffset }, listState) { spec -> onSetTiles(currentListState.tileSpecs()) - droppedSpec = spec + selectionState.select(spec, manual = false) } .onGloballyPositioned { coordinates -> gridContentOffset = coordinates.positionInRoot() } .testTag(CURRENT_TILES_GRID_TEST_TAG), ) { - EditTiles(listState.tiles, listState, selectionState, onResize) + EditTiles(listState.tiles, listState, selectionState) { spec -> + // Toggle the current size of the tile + currentListState.isIcon(spec)?.let { onResize(spec, !it) } + } } } @@ -348,11 +346,19 @@ private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any { } } +/** + * Adds a list of [GridCell] to the lazy grid + * + * @param cells the list of [GridCell] + * @param dragAndDropState the [DragAndDropState] for this grid + * @param selectionState the [MutableSelectionState] for this grid + * @param onToggleSize the callback when a tile's size is toggled + */ fun LazyGridScope.EditTiles( cells: List<GridCell>, dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, - onResize: (TileSpec) -> Unit, + onToggleSize: (spec: TileSpec) -> Unit, ) { items( count = cells.size, @@ -378,7 +384,7 @@ fun LazyGridScope.EditTiles( index = index, dragAndDropState = dragAndDropState, selectionState = selectionState, - onResize = onResize, + onToggleSize = onToggleSize, ) } is SpacerGridCell -> SpacerGridCell() @@ -392,16 +398,28 @@ private fun LazyGridItemScope.TileGridCell( index: Int, dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, - onResize: (TileSpec) -> Unit, + onToggleSize: (spec: TileSpec) -> Unit, ) { - val selected = selectionState.isSelected(cell.tile.tileSpec) val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1) + var selected by remember { mutableStateOf(false) } val selectionAlpha by animateFloatAsState( targetValue = if (selected) 1f else 0f, label = "QSEditTileSelectionAlpha", ) + LaunchedEffect(selectionState.selection?.tileSpec) { + selectionState.selection?.let { + // A delay is introduced on automatic selections such as dragged tiles or reflow caused + // by resizing. This avoids clipping issues on the border and resizing handle, as well + // as letting the selection animation play correctly. + if (!it.manual) { + delay(250) + } + } + selected = selectionState.selection?.tileSpec == cell.tile.tileSpec + } + val modifier = Modifier.animateItem() .semantics(mergeDescendants = true) { @@ -411,7 +429,7 @@ private fun LazyGridItemScope.TileGridCell( listOf( // TODO(b/367748260): Add final accessibility actions CustomAccessibilityAction("Toggle size") { - onResize(cell.tile.tileSpec) + onToggleSize(cell.tile.tileSpec) true } ) @@ -438,11 +456,9 @@ private fun LazyGridItemScope.TileGridCell( if (selected) { SelectedTile( - tileSpec = cell.tile.tileSpec, isIcon = cell.isIcon, selectionAlpha = { selectionAlpha }, selectionState = selectionState, - onResize = onResize, modifier = modifier.zIndex(2f), // 2f to display this tile over neighbors when dragged content = content, ) @@ -458,11 +474,9 @@ private fun LazyGridItemScope.TileGridCell( @Composable private fun SelectedTile( - tileSpec: TileSpec, isIcon: Boolean, selectionAlpha: () -> Float, selectionState: MutableSelectionState, - onResize: (TileSpec) -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { @@ -492,9 +506,7 @@ private fun SelectedTile( selectionState = selectionState, transition = selectionAlpha, tileWidths = { tileWidths }, - ) { - onResize(tileSpec) - } + ) } Layout(contents = listOf(content, handle)) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index 8a9606545fc3..e6edba513189 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -101,7 +101,7 @@ constructor( val (currentTiles, otherTiles) = sizedTiles.partition { it.tile.isCurrent } val currentListState = rememberEditListState(currentTiles, columns) DefaultEditTileGrid( - currentListState = currentListState, + listState = currentListState, otherTiles = otherTiles, columns = columns, modifier = modifier, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt index 2ea32e640984..441d96289d86 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt @@ -27,28 +27,39 @@ import com.android.systemui.qs.pipeline.shared.TileSpec /** Creates the state of the current selected tile that is remembered across compositions. */ @Composable -fun rememberSelectionState(): MutableSelectionState { - return remember { MutableSelectionState() } +fun rememberSelectionState( + onResize: (TileSpec) -> Unit, + onResizeEnd: (TileSpec) -> Unit, +): MutableSelectionState { + return remember { MutableSelectionState(onResize, onResizeEnd) } } +/** + * Holds the selected [TileSpec] and whether the selection was manual, i.e. caused by a tap from the + * user. + */ +data class Selection(val tileSpec: TileSpec, val manual: Boolean) + /** Holds the state of the current selection. */ -class MutableSelectionState { - private var _selectedTile = mutableStateOf<TileSpec?>(null) +class MutableSelectionState( + val onResize: (TileSpec) -> Unit, + private val onResizeEnd: (TileSpec) -> Unit, +) { + private var _selection = mutableStateOf<Selection?>(null) private var _resizingState = mutableStateOf<ResizingState?>(null) + /** The [Selection] if a tile is selected, null if not. */ + val selection by _selection + /** The [ResizingState] of the selected tile is currently being resized, null if not. */ val resizingState by _resizingState - fun isSelected(tileSpec: TileSpec): Boolean { - return _selectedTile.value?.let { it == tileSpec } ?: false - } - - fun select(tileSpec: TileSpec) { - _selectedTile.value = tileSpec + fun select(tileSpec: TileSpec, manual: Boolean) { + _selection.value = Selection(tileSpec, manual) } fun unSelect() { - _selectedTile.value = null + _selection.value = null onResizingDragEnd() } @@ -56,14 +67,21 @@ class MutableSelectionState { _resizingState.value?.onDrag(offset) } - fun onResizingDragStart(tileWidths: TileWidths, onResize: () -> Unit) { - if (_selectedTile.value == null) return - - _resizingState.value = ResizingState(tileWidths, onResize) + fun onResizingDragStart(tileWidths: TileWidths) { + _selection.value?.let { + _resizingState.value = ResizingState(tileWidths) { onResize(it.tileSpec) } + } } fun onResizingDragEnd() { _resizingState.value = null + _selection.value?.let { + onResizeEnd(it.tileSpec) + + // Mark the selection as automatic in case the tile ends up moving to a different + // row with its new size. + _selection.value = it.copy(manual = false) + } } } @@ -76,10 +94,10 @@ fun Modifier.selectableTile(tileSpec: TileSpec, selectionState: MutableSelection return pointerInput(Unit) { detectTapGestures( onTap = { - if (selectionState.isSelected(tileSpec)) { + if (selectionState.selection?.tileSpec == tileSpec) { selectionState.unSelect() } else { - selectionState.select(tileSpec) + selectionState.select(tileSpec, manual = true) } } ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt index e3acf3863254..7c62e5995ce8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt @@ -45,7 +45,6 @@ fun ResizingHandle( selectionState: MutableSelectionState, transition: () -> Float, tileWidths: () -> TileWidths? = { null }, - onResize: () -> Unit = {}, ) { if (enabled) { // Manually creating the touch target around the resizing dot to ensure that the next tile @@ -56,9 +55,7 @@ fun ResizingHandle( Modifier.size(minTouchTargetSize).pointerInput(Unit) { detectHorizontalDragGestures( onHorizontalDrag = { _, offset -> selectionState.onResizingDrag(offset) }, - onDragStart = { - tileWidths()?.let { selectionState.onResizingDragStart(it, onResize) } - }, + onDragStart = { tileWidths()?.let { selectionState.onResizingDragStart(it) } }, onDragEnd = selectionState::onResizingDragEnd, onDragCancel = selectionState::onResizingDragEnd, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt index b16a7075607f..b1841c4c5ffa 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt @@ -27,6 +27,7 @@ import com.android.systemui.qs.shared.model.CategoryAndName sealed interface GridCell { val row: Int val span: GridItemSpan + val s: String } /** @@ -39,6 +40,7 @@ data class TileGridCell( override val row: Int, override val width: Int, override val span: GridItemSpan = GridItemSpan(width), + override val s: String = "${tile.tileSpec.spec}-$row-$width", ) : GridCell, SizedTile<EditTileViewModel>, CategoryAndName by tile { val key: String = "${tile.tileSpec.spec}-$row" @@ -53,22 +55,30 @@ data class TileGridCell( data class SpacerGridCell( override val row: Int, override val span: GridItemSpan = GridItemSpan(1), + override val s: String = "spacer", ) : GridCell +/** + * Generates a list of [GridCell] from a list of [SizedTile] + * + * Builds rows based on the tiles' widths, and fill each hole with a [SpacerGridCell] + * + * @param startingRow The row index the grid is built from, used in cases where only end rows need + * to be regenerated + */ fun List<SizedTile<EditTileViewModel>>.toGridCells( columns: Int, - includeSpacers: Boolean = false, + startingRow: Int = 0, ): List<GridCell> { return splitInRowsSequence(this, columns) .flatMapIndexed { rowIndex, sizedTiles -> - val row: List<GridCell> = sizedTiles.map { TileGridCell(it, rowIndex) } + val correctedRowIndex = rowIndex + startingRow + val row: List<GridCell> = sizedTiles.map { TileGridCell(it, correctedRowIndex) } - if (includeSpacers) { - // Fill the incomplete rows with spacers - val numSpacers = columns - sizedTiles.sumOf { it.width } - row.toMutableList().apply { repeat(numSpacers) { add(SpacerGridCell(rowIndex)) } } - } else { - row + // Fill the incomplete rows with spacers + val numSpacers = columns - sizedTiles.sumOf { it.width } + row.toMutableList().apply { + repeat(numSpacers) { add(SpacerGridCell(correctedRowIndex)) } } } .toList() diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt index b604e18b1e76..4e698edf4e34 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt @@ -27,7 +27,7 @@ interface IconTilesViewModel { fun isIconTile(spec: TileSpec): Boolean - fun resize(spec: TileSpec) + fun resize(spec: TileSpec, toIcon: Boolean) } @SysUISingleton @@ -37,5 +37,5 @@ class IconTilesViewModelImpl @Inject constructor(private val interactor: IconTil override fun isIconTile(spec: TileSpec): Boolean = interactor.isIconTile(spec) - override fun resize(spec: TileSpec) = interactor.resize(spec) + override fun resize(spec: TileSpec, toIcon: Boolean) = interactor.resize(spec, toIcon) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileCoroutineScopeFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileCoroutineScopeFactory.kt index d0437a7210f1..b8f4ab40bb1d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileCoroutineScopeFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileCoroutineScopeFactory.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.base.viewmodel +import com.android.app.tracing.coroutines.createCoroutineTracingContext import com.android.systemui.dagger.qualifiers.Application import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -27,5 +28,5 @@ class QSTileCoroutineScopeFactory constructor(@Application private val applicationScope: CoroutineScope) { fun create(): CoroutineScope = - CoroutineScope(applicationScope.coroutineContext + SupervisorJob()) + CoroutineScope(applicationScope.coroutineContext + SupervisorJob() + createCoroutineTracingContext("QSTileScope")) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt index 246fe3883e19..ae56c2aad4e9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.qs.tiles.dialog +import com.android.app.tracing.coroutines.createCoroutineTracingContext import android.util.Log import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.DialogCuj @@ -62,7 +63,7 @@ constructor( } return } else { - coroutineScope = CoroutineScope(bgDispatcher) + coroutineScope = CoroutineScope(bgDispatcher + createCoroutineTracingContext("InternetDialogScope")) dialog = dialogFactory .create(aboveStatusBar, canConfigMobileData, canConfigWifi, coroutineScope) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt index cca947ff7e77..ac75932b8fee 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.impl.location.domain.interactor +import com.android.app.tracing.coroutines.createCoroutineTracingContext import android.content.Intent import android.provider.Settings import com.android.systemui.dagger.qualifiers.Application @@ -52,7 +53,7 @@ constructor( val wasEnabled: Boolean = input.data.isEnabled if (keyguardController.isMethodSecure() && keyguardController.isShowing()) { activityStarter.postQSRunnableDismissingKeyguard { - CoroutineScope(applicationScope.coroutineContext).launch { + CoroutineScope(applicationScope.coroutineContext + createCoroutineTracingContext("LocationTileScope")).launch { locationController.setLocationEnabled(!wasEnabled) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt index b25c61cba2b7..468e180a6e41 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.impl.saver.domain +import com.android.app.tracing.coroutines.createCoroutineTracingContext import android.content.Context import android.content.DialogInterface import android.content.SharedPreferences @@ -44,7 +45,7 @@ class DataSaverDialogDelegate( setTitle(R.string.data_saver_enable_title) setMessage(R.string.data_saver_description) setPositiveButton(R.string.data_saver_enable_button) { _: DialogInterface?, _ -> - CoroutineScope(backgroundContext).launch { + CoroutineScope(backgroundContext + createCoroutineTracingContext("DataSaverDialogScope")).launch { dataSaverController.setDataSaverEnabled(true) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 5eef8ea1999d..769abafed69f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -207,8 +207,8 @@ public class KeyguardIndicationController { protected boolean mPowerPluggedInWireless; protected boolean mPowerPluggedInDock; protected int mChargingSpeed; + protected boolean mPowerCharged; - private boolean mPowerCharged; /** Whether the battery defender is triggered. */ private boolean mBatteryDefender; /** Whether the battery defender is triggered with the device plugged. */ @@ -1100,14 +1100,15 @@ public class KeyguardIndicationController { String percentage = NumberFormat.getPercentInstance().format(mBatteryLevel / 100f); return mContext.getResources().getString( R.string.keyguard_plugged_in_incompatible_charger, percentage); - } else if (mPowerCharged) { - return mContext.getResources().getString(R.string.keyguard_charged); } return computePowerChargingStringIndication(); } protected String computePowerChargingStringIndication() { + if (mPowerCharged) { + return mContext.getResources().getString(R.string.keyguard_charged); + } final boolean hasChargingTime = mChargingTimeRemaining > 0; int chargingId; if (mPowerPluggedInWired) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index 7f5551274d55..8a6ec2aa27c9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -50,8 +50,8 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteract import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus; import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; +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 com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.res.R; import com.android.systemui.scene.data.model.SceneStack; @@ -115,6 +115,7 @@ public class StatusBarStateControllerImpl implements private final UiEventLogger mUiEventLogger; private final Lazy<InteractionJankMonitor> mInteractionJankMonitorLazy; private final JavaAdapter mJavaAdapter; + private final Lazy<KeyguardInteractor> mKeyguardInteractorLazy; private final Lazy<KeyguardTransitionInteractor> mKeyguardTransitionInteractorLazy; private final Lazy<ShadeInteractor> mShadeInteractorLazy; private final Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy; @@ -185,6 +186,7 @@ public class StatusBarStateControllerImpl implements UiEventLogger uiEventLogger, Lazy<InteractionJankMonitor> interactionJankMonitorLazy, JavaAdapter javaAdapter, + Lazy<KeyguardInteractor> keyguardInteractor, Lazy<KeyguardTransitionInteractor> keyguardTransitionInteractor, Lazy<ShadeInteractor> shadeInteractorLazy, Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy, @@ -195,6 +197,7 @@ public class StatusBarStateControllerImpl implements mUiEventLogger = uiEventLogger; mInteractionJankMonitorLazy = interactionJankMonitorLazy; mJavaAdapter = javaAdapter; + mKeyguardInteractorLazy = keyguardInteractor; mKeyguardTransitionInteractorLazy = keyguardTransitionInteractor; mShadeInteractorLazy = shadeInteractorLazy; mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy; @@ -233,8 +236,8 @@ public class StatusBarStateControllerImpl implements this::onStatusBarStateChanged); mJavaAdapter.alwaysCollectFlow( - mKeyguardTransitionInteractorLazy.get().transitionValue(KeyguardState.AOD), - this::onAodKeyguardStateTransitionValueChanged); + mKeyguardInteractorLazy.get().getDozeAmount(), + this::setDozeAmountInternal); } } @@ -404,6 +407,7 @@ public class StatusBarStateControllerImpl implements @Override public void setAndInstrumentDozeAmount(View view, float dozeAmount, boolean animated) { + SceneContainerFlag.assertInLegacyMode(); if (mDarkAnimator != null && mDarkAnimator.isRunning()) { if (animated && mDozeAmountTarget == dozeAmount) { return; @@ -439,6 +443,7 @@ public class StatusBarStateControllerImpl implements } private void startDozeAnimation() { + SceneContainerFlag.assertInLegacyMode(); if (mDozeAmount == 0f || mDozeAmount == 1f) { mDozeInterpolator = mIsDozing ? Interpolators.FAST_OUT_SLOW_IN @@ -457,6 +462,7 @@ public class StatusBarStateControllerImpl implements @VisibleForTesting protected ObjectAnimator createDarkAnimator() { + SceneContainerFlag.assertInLegacyMode(); ObjectAnimator darkAnimator = ObjectAnimator.ofFloat( this, SET_DARK_AMOUNT_PROPERTY, mDozeAmountTarget); darkAnimator.setInterpolator(Interpolators.LINEAR); @@ -710,14 +716,6 @@ public class StatusBarStateControllerImpl implements updateStateAndNotifyListeners(newState); } - private void onAodKeyguardStateTransitionValueChanged(float value) { - if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { - return; - } - - setDozeAmountInternal(value); - } - private static final Map<SceneKey, Integer> sStatusBarStateByLockedSceneKey = Map.of( Scenes.Lockscreen, StatusBarState.KEYGUARD, Scenes.Bouncer, StatusBarState.KEYGUARD, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java index 9b96931348d6..6907eefa8b56 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java @@ -106,7 +106,8 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements @VisibleForTesting public final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed = new ArraySet<>(); - private boolean mIsExpanded; + private boolean mIsShadeOrQsExpanded; + private boolean mIsQsExpanded; private int mStatusBarState; private AnimationStateHandler mAnimationStateHandler; @@ -178,6 +179,10 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements }); javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(), this::onShadeOrQsExpanded); + if (SceneContainerFlag.isEnabled()) { + javaAdapter.alwaysCollectFlow(shadeInteractor.isQsExpanded(), + this::onQsExpanded); + } if (NotificationThrottleHun.isEnabled()) { mVisualStabilityProvider.addPersistentReorderingBannedListener( mOnReorderingBannedListener); @@ -287,14 +292,19 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements } private void onShadeOrQsExpanded(Boolean isExpanded) { - if (isExpanded != mIsExpanded) { - mIsExpanded = isExpanded; + if (isExpanded != mIsShadeOrQsExpanded) { + mIsShadeOrQsExpanded = isExpanded; if (!SceneContainerFlag.isEnabled() && isExpanded) { mHeadsUpAnimatingAway.setValue(false); } } } + private void onQsExpanded(Boolean isQsExpanded) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; + if (isQsExpanded != mIsQsExpanded) mIsQsExpanded = isQsExpanded; + } + /** * Set that we are exiting the headsUp pinned mode, but some notifications might still be * animating out. This is used to keep the touchable regions in a reasonable state. @@ -490,7 +500,10 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements @Override protected boolean shouldHeadsUpBecomePinned(NotificationEntry entry) { - boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsExpanded; + boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsShadeOrQsExpanded; + if (SceneContainerFlag.isEnabled()) { + pin |= mIsQsExpanded; + } if (mBypassController.getBypassEnabled()) { pin |= mStatusBarState == StatusBarState.KEYGUARD; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 560028cb5640..7b6a2cb62b14 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -444,11 +444,9 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView if (onFinishedRunnable != null) { onFinishedRunnable.run(); } - if (mRunWithoutInterruptions) { - enableAppearDrawing(false); - } // We need to reset the View state, even if the animation was cancelled + enableAppearDrawing(false); onAppearAnimationFinished(isAppearing); if (mRunWithoutInterruptions) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index e7c67f93eb78..3c6962a4c2a0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -1260,6 +1260,7 @@ public class NotificationStackScrollLayout @Override public void setHeadsUpBottom(float headsUpBottom) { mAmbientState.setHeadsUpBottom(headsUpBottom); + mStateAnimator.setHeadsUpAppearHeightBottom(Math.round(headsUpBottom)); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index e34eb61c5cbd..8ca26bea9705 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -45,6 +45,7 @@ import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters +import com.android.systemui.keyguard.ui.viewmodel.DozingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel @@ -112,6 +113,7 @@ constructor( private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel, private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, + dozingToGlanceableHubTransitionViewModel: DozingToGlanceableHubTransitionViewModel, private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel, private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, @@ -506,6 +508,7 @@ constructor( merge( lockscreenToGlanceableHubTransitionViewModel.notificationAlpha, glanceableHubToLockscreenTransitionViewModel.notificationAlpha, + dozingToGlanceableHubTransitionViewModel.notificationAlpha, ) // Manually emit on start because [notificationAlpha] only starts emitting // when transitions start. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 68163b28dd09..4ef328cf1623 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -149,7 +149,7 @@ class MobileIconInteractorImpl( override val isForceHidden: Flow<Boolean>, connectionRepository: MobileConnectionRepository, private val context: Context, - val carrierIdOverrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl() + val carrierIdOverrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl(), ) : MobileIconInteractor { override val tableLogBuffer: TableLogBuffer = connectionRepository.tableLogBuffer @@ -182,7 +182,7 @@ class MobileIconInteractorImpl( .stateIn( scope, SharingStarted.WhileSubscribed(), - connectionRepository.networkName.value + connectionRepository.networkName.value, ) override val carrierName = @@ -198,7 +198,7 @@ class MobileIconInteractorImpl( .stateIn( scope, SharingStarted.WhileSubscribed(), - connectionRepository.carrierName.value.name + connectionRepository.carrierName.value.name, ) /** What the mobile icon would be before carrierId overrides */ @@ -219,10 +219,7 @@ class MobileIconInteractorImpl( .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value) override val networkTypeIconGroup = - combine( - defaultNetworkType, - carrierIdIconOverrideExists, - ) { networkType, overrideExists -> + combine(defaultNetworkType, carrierIdIconOverrideExists) { networkType, overrideExists -> // DefaultIcon comes out of the icongroup lookup, we check for overrides here if (overrideExists) { val iconOverride = @@ -316,30 +313,30 @@ class MobileIconInteractorImpl( /** Whether or not to show the error state of [SignalDrawable] */ private val showExclamationMark: StateFlow<Boolean> = - combine( - defaultSubscriptionHasDataEnabled, + combine(defaultSubscriptionHasDataEnabled, isDefaultConnectionFailed, isInService) { + isDefaultDataEnabled, isDefaultConnectionFailed, - isInService, - ) { isDefaultDataEnabled, isDefaultConnectionFailed, isInService -> + isInService -> !isDefaultDataEnabled || isDefaultConnectionFailed || !isInService } .stateIn(scope, SharingStarted.WhileSubscribed(), true) private val cellularShownLevel: StateFlow<Int> = - combine( + combine(level, isInService, connectionRepository.inflateSignalStrength) { level, isInService, - connectionRepository.inflateSignalStrength, - ) { level, isInService, inflate -> + inflate -> if (isInService) { if (inflate) level + 1 else level } else 0 } .stateIn(scope, SharingStarted.WhileSubscribed(), 0) - // Satellite level is unaffected by the isInService or inflateSignalStrength properties + // Satellite level is unaffected by the inflateSignalStrength property // See b/346904529 for details - private val satelliteShownLevel: StateFlow<Int> = level + private val satelliteShownLevel: StateFlow<Int> = + combine(level, isInService) { level, isInService -> if (isInService) level else 0 } + .stateIn(scope, SharingStarted.WhileSubscribed(), 0) private val cellularIcon: Flow<SignalIconModel.Cellular> = combine( @@ -362,7 +359,7 @@ class MobileIconInteractorImpl( level = it, icon = SatelliteIconModel.fromSignalStrength(it) - ?: SatelliteIconModel.fromSignalStrength(0)!! + ?: SatelliteIconModel.fromSignalStrength(0)!!, ) } @@ -383,11 +380,7 @@ class MobileIconInteractorImpl( } } .distinctUntilChanged() - .logDiffsForTable( - tableLogBuffer, - columnPrefix = "icon", - initialValue = initial, - ) + .logDiffsForTable(tableLogBuffer, columnPrefix = "icon", initialValue = initial) .stateIn(scope, SharingStarted.WhileSubscribed(), initial) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index deae576662e3..bad6f80c3735 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel +import com.android.app.tracing.coroutines.createCoroutineTracingContext import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -29,6 +30,7 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMob import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -114,7 +116,7 @@ constructor( private fun createViewModel(subId: Int): Pair<MobileIconViewModel, CoroutineScope> { // Create a child scope so we can cancel it - val vmScope = scope.createChildScope() + val vmScope = scope.createChildScope(createCoroutineTracingContext("MobileIconViewModel")) val vm = MobileIconViewModel( subId, @@ -128,8 +130,8 @@ constructor( return Pair(vm, vmScope) } - private fun CoroutineScope.createChildScope() = - CoroutineScope(coroutineContext + Job(coroutineContext[Job])) + private fun CoroutineScope.createChildScope(extraContext: CoroutineContext) = + CoroutineScope(coroutineContext + Job(coroutineContext[Job]) + extraContext) private fun invalidateCaches(subIds: List<Int>) { reuseCache.keys diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt index 4cb66c19a0bb..eea4c212e40e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt @@ -89,19 +89,30 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa launch { viewModel.primaryOngoingActivityChip.collect { primaryChipModel -> OngoingActivityChipBinder.bind(primaryChipModel, primaryChipView) - when (primaryChipModel) { - is OngoingActivityChipModel.Shown -> - listener.onOngoingActivityStatusChanged( - hasPrimaryOngoingActivity = true, - hasSecondaryOngoingActivity = false, - shouldAnimate = true, - ) - is OngoingActivityChipModel.Hidden -> - listener.onOngoingActivityStatusChanged( - hasPrimaryOngoingActivity = false, - hasSecondaryOngoingActivity = false, - shouldAnimate = primaryChipModel.shouldAnimate, - ) + if (StatusBarSimpleFragment.isEnabled) { + when (primaryChipModel) { + is OngoingActivityChipModel.Shown -> + primaryChipView.show(shouldAnimateChange = true) + is OngoingActivityChipModel.Hidden -> + primaryChipView.hide( + shouldAnimateChange = primaryChipModel.shouldAnimate + ) + } + } else { + when (primaryChipModel) { + is OngoingActivityChipModel.Shown -> + listener.onOngoingActivityStatusChanged( + hasPrimaryOngoingActivity = true, + hasSecondaryOngoingActivity = false, + shouldAnimate = true, + ) + is OngoingActivityChipModel.Hidden -> + listener.onOngoingActivityStatusChanged( + hasPrimaryOngoingActivity = false, + hasSecondaryOngoingActivity = false, + shouldAnimate = primaryChipModel.shouldAnimate, + ) + } } } } @@ -118,14 +129,22 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa // TODO(b/364653005): Don't show the secondary chip if there isn't // enough space for it. OngoingActivityChipBinder.bind(chips.secondary, secondaryChipView) - listener.onOngoingActivityStatusChanged( - hasPrimaryOngoingActivity = - chips.primary is OngoingActivityChipModel.Shown, - hasSecondaryOngoingActivity = - chips.secondary is OngoingActivityChipModel.Shown, - // TODO(b/364653005): Figure out the animation story here. - shouldAnimate = true, - ) + + if (StatusBarSimpleFragment.isEnabled) { + primaryChipView.adjustVisibility(chips.primary.toVisibilityModel()) + secondaryChipView.adjustVisibility( + chips.secondary.toVisibilityModel() + ) + } else { + listener.onOngoingActivityStatusChanged( + hasPrimaryOngoingActivity = + chips.primary is OngoingActivityChipModel.Shown, + hasSecondaryOngoingActivity = + chips.secondary is OngoingActivityChipModel.Shown, + // TODO(b/364653005): Figure out the animation story here. + shouldAnimate = true, + ) + } } } } @@ -164,6 +183,15 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa } } + private fun OngoingActivityChipModel.toVisibilityModel(): + CollapsedStatusBarViewModel.VisibilityModel { + return CollapsedStatusBarViewModel.VisibilityModel( + visibility = if (this is OngoingActivityChipModel.Shown) View.VISIBLE else View.GONE, + // TODO(b/364653005): Figure out the animation story here. + shouldAnimateChange = true, + ) + } + private fun animateLightsOutView(view: View, visible: Boolean) { view.animate().cancel() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt index 692e0e4f55f8..366ea3516965 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt @@ -96,8 +96,6 @@ interface CollapsedStatusBarViewModel { val isNotificationIconContainerVisible: Flow<VisibilityModel> val isSystemInfoVisible: Flow<VisibilityModel> - // TODO(b/364360986): Add isOngoingActivityChipVisible: Flow<VisibilityModel> - /** * Apps can request a low profile mode [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE] where * status bar and navigation icons dim. In this mode, a notification dot appears where the @@ -211,7 +209,7 @@ constructor( isStatusBarAllowed && visibilityViaDisableFlags.areNotificationIconsAllowed VisibilityModel( showNotificationIconContainer.toVisibilityInt(), - visibilityViaDisableFlags.animate + visibilityViaDisableFlags.animate, ) } override val isSystemInfoVisible: Flow<VisibilityModel> = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt index 27bc6d36c1e6..76389f39e484 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt @@ -71,7 +71,7 @@ fun ModeTile(viewModel: ModeTileViewModel) { .semantics { stateDescription = viewModel.stateDescription }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = - Arrangement.spacedBy(space = 8.dp, alignment = Alignment.Start), + Arrangement.spacedBy(space = 12.dp, alignment = Alignment.Start), ) { Icon(icon = viewModel.icon, modifier = Modifier.size(24.dp)) Column { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt index 5953ea598929..5392e38823c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.systemui.Flags import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel @Composable @@ -34,8 +33,8 @@ fun ModeTileGrid(viewModel: ModesDialogViewModel) { val tiles by viewModel.tiles.collectAsStateWithLifecycle(initialValue = emptyList()) LazyVerticalGrid( - columns = GridCells.Fixed(if (Flags.modesDialogSingleRows()) 1 else 2), - modifier = Modifier.fillMaxWidth().heightIn(max = 300.dp), + columns = GridCells.Fixed(1), + modifier = Modifier.fillMaxWidth().heightIn(max = 320.dp), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/GlobalCoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/GlobalCoroutinesModule.kt index 8ecf250e2bbd..2af84c7e46f0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/GlobalCoroutinesModule.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/GlobalCoroutinesModule.kt @@ -37,7 +37,7 @@ class GlobalCoroutinesModule { @Application fun applicationScope( @Main dispatcherContext: CoroutineContext, - ): CoroutineScope = CoroutineScope(dispatcherContext) + ): CoroutineScope = CoroutineScope(dispatcherContext + createCoroutineTracingContext("ApplicationScope")) @Provides @Singleton @@ -51,15 +51,7 @@ class GlobalCoroutinesModule { @Provides @Singleton @Main - fun mainCoroutineContext(@Tracing tracingCoroutineContext: CoroutineContext): CoroutineContext { - return Dispatchers.Main.immediate + tracingCoroutineContext - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Provides - @Tracing - @Singleton - fun tracingCoroutineContext(): CoroutineContext { - return createCoroutineTracingContext() + fun mainCoroutineContext(): CoroutineContext { + return Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt index a03221e03467..3c0682822564 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt @@ -91,10 +91,9 @@ class SysUICoroutinesModule { @Background @SysUISingleton fun bgCoroutineContext( - @Tracing tracingCoroutineContext: CoroutineContext, @Background bgCoroutineDispatcher: CoroutineDispatcher, ): CoroutineContext { - return bgCoroutineDispatcher + tracingCoroutineContext + return bgCoroutineDispatcher } /** Coroutine dispatcher for background operations on for UI. */ @@ -112,9 +111,8 @@ class SysUICoroutinesModule { @UiBackground @SysUISingleton fun uiBgCoroutineContext( - @Tracing tracingCoroutineContext: CoroutineContext, @UiBackground uiBgCoroutineDispatcher: CoroutineDispatcher, ): CoroutineContext { - return uiBgCoroutineDispatcher + tracingCoroutineContext + return uiBgCoroutineDispatcher } } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt index 82f41a7fd154..4d9aaa6dc6b0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.util.settings +import com.android.app.tracing.coroutines.createCoroutineTracingContext import android.annotation.UserIdInt import android.content.ContentResolver import android.database.ContentObserver @@ -93,7 +94,7 @@ interface SettingsProxy { */ @AnyThread fun registerContentObserverAsync(name: String, settingsObserver: ContentObserver) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("SettingsProxy-A")).launch { registerContentObserverSync(getUriFor(name), settingsObserver) } @@ -110,7 +111,7 @@ interface SettingsProxy { settingsObserver: ContentObserver, @WorkerThread registered: Runnable ) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("SettingsProxy-B")).launch { registerContentObserverSync(getUriFor(name), settingsObserver) registered.run() } @@ -143,7 +144,7 @@ interface SettingsProxy { */ @AnyThread fun registerContentObserverAsync(uri: Uri, settingsObserver: ContentObserver) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("SettingsProxy-C")).launch { registerContentObserverSync(uri, settingsObserver) } @@ -160,7 +161,7 @@ interface SettingsProxy { settingsObserver: ContentObserver, @WorkerThread registered: Runnable ) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("SettingsProxy-D")).launch { registerContentObserverSync(uri, settingsObserver) registered.run() } @@ -205,7 +206,7 @@ interface SettingsProxy { notifyForDescendants: Boolean, settingsObserver: ContentObserver ) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("SettingsProxy-E")).launch { registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver) } @@ -223,7 +224,7 @@ interface SettingsProxy { settingsObserver: ContentObserver, @WorkerThread registered: Runnable ) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("SettingsProxy-F")).launch { registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver) registered.run() } @@ -274,7 +275,7 @@ interface SettingsProxy { notifyForDescendants: Boolean, settingsObserver: ContentObserver ) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("SettingsProxy-G")).launch { registerContentObserverSync(uri, notifyForDescendants, settingsObserver) } @@ -292,7 +293,7 @@ interface SettingsProxy { settingsObserver: ContentObserver, @WorkerThread registered: Runnable ) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("SettingsProxy-H")).launch { registerContentObserverSync(uri, notifyForDescendants, settingsObserver) registered.run() } @@ -329,7 +330,7 @@ interface SettingsProxy { */ @AnyThread fun unregisterContentObserverAsync(settingsObserver: ContentObserver) = - CoroutineScope(backgroundDispatcher).launch { unregisterContentObserver(settingsObserver) } + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("SettingsProxy-I")).launch { unregisterContentObserver(settingsObserver) } /** * Look up a name in the database. diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt index 8e3b813a2a82..c820c07b61b1 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.util.settings +import com.android.app.tracing.coroutines.createCoroutineTracingContext import android.annotation.UserIdInt import android.annotation.WorkerThread import android.content.ContentResolver @@ -78,7 +79,7 @@ interface UserSettingsProxy : SettingsProxy { } override fun registerContentObserverAsync(uri: Uri, settingsObserver: ContentObserver) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("UserSettingsProxy-A")).launch { registerContentObserverForUserSync(uri, settingsObserver, userId) } @@ -112,7 +113,7 @@ interface UserSettingsProxy : SettingsProxy { notifyForDescendants: Boolean, settingsObserver: ContentObserver ) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("UserSettingsProxy-B")).launch { registerContentObserverForUserSync(uri, notifyForDescendants, settingsObserver, userId) } @@ -157,7 +158,7 @@ interface UserSettingsProxy : SettingsProxy { settingsObserver: ContentObserver, userHandle: Int ) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("UserSettingsProxy-C")).launch { registerContentObserverForUserSync(getUriFor(name), settingsObserver, userHandle) } @@ -198,7 +199,7 @@ interface UserSettingsProxy : SettingsProxy { settingsObserver: ContentObserver, userHandle: Int ) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("UserSettingsProxy-D")).launch { registerContentObserverForUserSync(uri, settingsObserver, userHandle) } @@ -215,7 +216,7 @@ interface UserSettingsProxy : SettingsProxy { userHandle: Int, @WorkerThread registered: Runnable ) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("UserSettingsProxy-E")).launch { registerContentObserverForUserSync(uri, settingsObserver, userHandle) registered.run() } @@ -274,7 +275,7 @@ interface UserSettingsProxy : SettingsProxy { settingsObserver: ContentObserver, userHandle: Int ) { - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("UserSettingsProxy-F")).launch { registerContentObserverForUserSync( getUriFor(name), notifyForDescendants, @@ -338,7 +339,7 @@ interface UserSettingsProxy : SettingsProxy { settingsObserver: ContentObserver, userHandle: Int ) = - CoroutineScope(backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher + createCoroutineTracingContext("UserSettingsProxy-G")).launch { registerContentObserverForUserSync( uri, notifyForDescendants, diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index d63e728cf443..d63e728cf443 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index e444db4895d4..e444db4895d4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java index 7bb6ef1c8895..7bb6ef1c8895 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt index 5d76e325bd3a..85e8ab43b2ee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt @@ -22,10 +22,12 @@ import android.content.Context import android.graphics.Bitmap import android.net.Uri import android.os.PersistableBundle +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE import com.android.systemui.SysuiTestCase -import com.android.systemui.util.mockito.whenever import java.io.IOException import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -37,6 +39,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.Mock import org.mockito.MockitoAnnotations +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -88,7 +91,8 @@ class ClipboardModelTest : SysuiTestCase() { @Test @Throws(IOException::class) - fun test_imageClipData() { + @DisableFlags(FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE) + fun test_imageClipData_legacy() { val testBitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888) whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver) whenever(mMockContext.resources).thenReturn(mContext.resources) @@ -103,6 +107,21 @@ class ClipboardModelTest : SysuiTestCase() { @Test @Throws(IOException::class) + @EnableFlags(FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE) + fun test_imageClipData() { + val testBitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888) + whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver) + whenever(mMockContext.resources).thenReturn(mContext.resources) + whenever(mMockContentResolver.loadThumbnail(any(), any(), any())).thenReturn(testBitmap) + whenever(mMockContentResolver.getType(any())).thenReturn("text") + val imageClipData = ClipData("Test", arrayOf("image/png"), ClipData.Item(Uri.parse("test"))) + val model = ClipboardModel.fromClipData(mMockContext, mClipboardUtils, imageClipData, "") + assertEquals(ClipboardModel.Type.IMAGE, model.type) + assertEquals(testBitmap, model.loadThumbnail(mMockContext)) + } + + @Test + @Throws(IOException::class) fun test_imageClipData_loadFailure() { whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver) whenever(mMockContext.resources).thenReturn(mContext.resources) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt index 414974cc2941..414974cc2941 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt index 6423d25cb88a..8d060e936cd9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt @@ -60,13 +60,13 @@ class DragAndDropTest : SysuiTestCase() { onSetTiles: (List<TileSpec>) -> Unit, ) { DefaultEditTileGrid( - currentListState = listState, + listState = listState, otherTiles = listOf(), columns = 4, modifier = Modifier.fillMaxSize(), onRemoveTile = {}, onSetTiles = onSetTiles, - onResize = {}, + onResize = { _, _ -> }, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt index 682ed92cc593..ee1c0e99d6ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt @@ -25,7 +25,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performCustomAccessibilityActionWithLabel +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeLeft +import androidx.compose.ui.test.swipeRight import androidx.compose.ui.text.AnnotatedString import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -43,15 +47,19 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalTestApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class ResizingTest : SysuiTestCase() { @get:Rule val composeRule = createComposeRule() @Composable - private fun EditTileGridUnderTest(listState: EditTileListState, onResize: (TileSpec) -> Unit) { + private fun EditTileGridUnderTest( + listState: EditTileListState, + onResize: (TileSpec, Boolean) -> Unit, + ) { DefaultEditTileGrid( - currentListState = listState, + listState = listState, otherTiles = listOf(), columns = 4, modifier = Modifier.fillMaxSize(), @@ -61,22 +69,12 @@ class ResizingTest : SysuiTestCase() { ) } - @OptIn(ExperimentalTestApi::class) @Test - fun resizedIcon_shouldBeLarge() { + fun toggleIconTile_shouldBeLarge() { var tiles by mutableStateOf(TestEditTiles) val listState = EditTileListState(tiles, 4) composeRule.setContent { - EditTileGridUnderTest(listState) { spec -> - tiles = - tiles.map { - if (it.tile.tileSpec == spec) { - toggleWidth(it) - } else { - it - } - } - } + EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } } composeRule.waitForIdle() @@ -87,35 +85,74 @@ class ResizingTest : SysuiTestCase() { assertThat(tiles.find { it.tile.tileSpec.spec == "tileA" }?.width).isEqualTo(2) } - @OptIn(ExperimentalTestApi::class) + @Test + fun toggleLargeTile_shouldBeIcon() { + var tiles by mutableStateOf(TestEditTiles) + val listState = EditTileListState(tiles, 4) + composeRule.setContent { + EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } + } + composeRule.waitForIdle() + + composeRule + .onNodeWithContentDescription("tileD_large") + .performCustomAccessibilityActionWithLabel("Toggle size") + + assertThat(tiles.find { it.tile.tileSpec.spec == "tileD_large" }?.width).isEqualTo(1) + } + @Test fun resizedLarge_shouldBeIcon() { var tiles by mutableStateOf(TestEditTiles) val listState = EditTileListState(tiles, 4) composeRule.setContent { - EditTileGridUnderTest(listState) { spec -> - tiles = - tiles.map { - if (it.tile.tileSpec == spec) { - toggleWidth(it) - } else { - it - } - } + EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } + } + composeRule.waitForIdle() + + composeRule + .onNodeWithContentDescription("tileA") + .performClick() // Select + .performTouchInput { // Resize down + swipeRight() } + composeRule.waitForIdle() + + assertThat(tiles.find { it.tile.tileSpec.spec == "tileA" }?.width).isEqualTo(1) + } + + @Test + fun resizedIcon_shouldBeLarge() { + var tiles by mutableStateOf(TestEditTiles) + val listState = EditTileListState(tiles, 4) + composeRule.setContent { + EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } } composeRule.waitForIdle() composeRule .onNodeWithContentDescription("tileD_large") - .performCustomAccessibilityActionWithLabel("Toggle size") + .performClick() // Select + .performTouchInput { // Resize down + swipeLeft() + } + composeRule.waitForIdle() assertThat(tiles.find { it.tile.tileSpec.spec == "tileD_large" }?.width).isEqualTo(1) } companion object { - private fun toggleWidth(tile: SizedTile<EditTileViewModel>): SizedTile<EditTileViewModel> { - return SizedTileImpl(tile.tile, width = if (tile.isIcon) 2 else 1) + private fun List<SizedTile<EditTileViewModel>>.resize( + spec: TileSpec, + toIcon: Boolean, + ): List<SizedTile<EditTileViewModel>> { + return map { + if (it.tile.tileSpec == spec) { + SizedTileImpl(it.tile, width = if (toIcon) 1 else 2) + } else { + it + } + } } private fun createEditTile(tileSpec: String): SizedTile<EditTileViewModel> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 4bd0c757543b..a6afd0e499f4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -453,6 +453,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mUiEventLogger, () -> mKosmos.getInteractionJankMonitor(), mJavaAdapter, + () -> mKeyguardInteractor, () -> mKeyguardTransitionInteractor, () -> mShadeInteractor, () -> mKosmos.getDeviceUnlockedInteractor(), @@ -611,6 +612,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { new UiEventLoggerFake(), () -> mKosmos.getInteractionJankMonitor(), mJavaAdapter, + () -> mKeyguardInteractor, () -> mKeyguardTransitionInteractor, () -> mShadeInteractor, () -> mKosmos.getDeviceUnlockedInteractor(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 4fd830d0891e..a8bcfbcfc539 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -756,6 +756,27 @@ class MobileIconInteractorTest : SysuiTestCase() { assertThat(latest!!.level).isEqualTo(4) } + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @Test + fun satBasedIcon_reportsLevelZeroWhenOutOfService() = + testScope.runTest { + val latest by collectLastValue(underTest.signalLevelIcon) + + // GIVEN a satellite connection + connectionRepository.isNonTerrestrial.value = true + // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH + connectionRepository.inflateSignalStrength.value = true + + connectionRepository.primaryLevel.value = 4 + assertThat(latest!!.level).isEqualTo(4) + + connectionRepository.isInService.value = false + connectionRepository.primaryLevel.value = 4 + + // THEN level reports 0, by policy + assertThat(latest!!.level).isEqualTo(0) + } + private fun createInteractor( overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl() ) = diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt index 64ae05131b5a..e6c98cd83b5e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt @@ -16,6 +16,8 @@ package com.android.systemui.keyguard.domain.interactor +import android.service.dream.dreamManager +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor @@ -39,10 +41,12 @@ var Kosmos.fromDreamingTransitionInteractor by mainDispatcher = testDispatcher, keyguardInteractor = keyguardInteractor, glanceableHubTransitions = glanceableHubTransitions, + communalInteractor = communalInteractor, communalSceneInteractor = communalSceneInteractor, communalSettingsInteractor = communalSettingsInteractor, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + dreamManager = dreamManager, deviceEntryInteractor = deviceEntryInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorKosmos.kt index 52416bae0d9d..ace11573c7c6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorKosmos.kt @@ -41,6 +41,5 @@ val Kosmos.keyguardDismissInteractor by trustRepository = trustRepository, alternateBouncerInteractor = alternateBouncerInteractor, powerInteractor = powerInteractor, - keyguardTransitionInteractor = keyguardTransitionInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToGlanceableHubTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToGlanceableHubTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..ef10459b45cb --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToGlanceableHubTransitionViewModelKosmos.kt @@ -0,0 +1,27 @@ +/* + * 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.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.dozingToGlanceableHubTransitionViewModel by Fixture { + DozingToGlanceableHubTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt index 2deeb253e925..cfc31c7f301c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt @@ -20,6 +20,7 @@ import com.android.internal.logging.uiEventLogger import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.jank.interactionJankMonitor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor +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.scene.domain.interactor.sceneBackInteractor @@ -36,6 +37,7 @@ var Kosmos.statusBarStateController: SysuiStatusBarStateController by uiEventLogger, { interactionJankMonitor }, mock(), + { keyguardInteractor }, { keyguardTransitionInteractor }, { shadeInteractor }, { deviceUnlockedInteractor }, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardStateCallbackInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardStateCallbackInteractorKosmos.kt new file mode 100644 index 000000000000..58dd522d40ec --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardStateCallbackInteractorKosmos.kt @@ -0,0 +1,44 @@ +/* + * 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.domain.interactor + +import com.android.keyguard.trustManager +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor +import com.android.systemui.keyguard.dismissCallbackRegistry +import com.android.systemui.keyguard.domain.interactor.KeyguardStateCallbackInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.trustInteractor +import com.android.systemui.keyguard.domain.interactor.windowManagerLockscreenVisibilityInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.user.domain.interactor.selectedUserInteractor + +val Kosmos.keyguardStateCallbackInteractor by + Kosmos.Fixture { + KeyguardStateCallbackInteractor( + applicationScope = testScope.backgroundScope, + backgroundDispatcher = testDispatcher, + selectedUserInteractor = selectedUserInteractor, + keyguardTransitionInteractor = keyguardTransitionInteractor, + trustInteractor = trustInteractor, + simBouncerInteractor = simBouncerInteractor, + dismissCallbackRegistry = dismissCallbackRegistry, + wmLockscreenVisibilityInteractor = windowManagerLockscreenVisibilityInteractor, + trustManager = trustManager, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index ffd8aabdd964..a9e117affefb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -25,6 +25,7 @@ import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToOccludedTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.dozingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dreamingToLockscreenTransitionViewModel @@ -66,6 +67,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { aodToGoneTransitionViewModel = aodToGoneTransitionViewModel, aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel, + dozingToGlanceableHubTransitionViewModel = dozingToGlanceableHubTransitionViewModel, dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel, dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel, dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel, diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java index 1f98334bb8ce..c3b7087a44c3 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java @@ -16,15 +16,7 @@ package com.android.server.appfunctions; -import android.annotation.NonNull; -import android.os.UserHandle; -import android.util.SparseArray; - -import com.android.internal.annotations.GuardedBy; - import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -41,50 +33,5 @@ public final class AppFunctionExecutors { /* unit= */ TimeUnit.SECONDS, /* workQueue= */ new LinkedBlockingQueue<>()); - /** A map of per-user executors for queued work. */ - @GuardedBy("sLock") - private static final SparseArray<ExecutorService> mPerUserExecutorsLocked = new SparseArray<>(); - - private static final Object sLock = new Object(); - - /** - * Returns a per-user executor for queued metadata sync request. - * - * <p>The work submitted to these executor (Sync request) needs to be synchronous per user hence - * the use of a single thread. - * - * <p>Note: Use a different executor if not calling {@code submitSyncRequest} on a {@code - * MetadataSyncAdapter}. - */ - // TODO(b/357551503): Restrict the scope of this executor to the MetadataSyncAdapter itself. - public static ExecutorService getPerUserSyncExecutor(@NonNull UserHandle user) { - synchronized (sLock) { - ExecutorService executor = mPerUserExecutorsLocked.get(user.getIdentifier(), null); - if (executor == null) { - executor = Executors.newSingleThreadExecutor(); - mPerUserExecutorsLocked.put(user.getIdentifier(), executor); - } - return executor; - } - } - - /** - * Shuts down and removes the per-user executor for queued work. - * - * <p>This should be called when the user is removed. - */ - public static void shutDownAndRemoveUserExecutor(@NonNull UserHandle user) - throws InterruptedException { - ExecutorService executor; - synchronized (sLock) { - executor = mPerUserExecutorsLocked.get(user.getIdentifier()); - mPerUserExecutorsLocked.remove(user.getIdentifier()); - } - if (executor != null) { - executor.shutdown(); - var unused = executor.awaitTermination(30, TimeUnit.SECONDS); - } - } - private AppFunctionExecutors() {} } diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index b4713d9f11af..1e723b5a1da2 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -95,12 +95,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { public void onUserStopping(@NonNull TargetUser user) { Objects.requireNonNull(user); - try { - AppFunctionExecutors.shutDownAndRemoveUserExecutor(user.getUserHandle()); - MetadataSyncPerUser.removeUserSyncAdapter(user.getUserHandle()); - } catch (InterruptedException e) { - Slog.e(TAG, "Unable to remove data for: " + user.getUserHandle(), e); - } + MetadataSyncPerUser.removeUserSyncAdapter(user.getUserHandle()); } @Override diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java index e29b6e403f2a..d84b20556053 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java +++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java @@ -42,6 +42,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.infra.AndroidFuture; import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults; @@ -53,7 +54,9 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; /** * This class implements helper methods for synchronously interacting with AppSearch while @@ -63,9 +66,15 @@ import java.util.concurrent.Executor; */ public class MetadataSyncAdapter { private static final String TAG = MetadataSyncAdapter.class.getSimpleName(); - private final Executor mSyncExecutor; + + private final ExecutorService mExecutor; + private final AppSearchManager mAppSearchManager; private final PackageManager mPackageManager; + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private Future<?> mCurrentSyncTask; // Hidden constants in {@link SetSchemaRequest} that restricts runtime metadata visibility // by permissions. @@ -73,12 +82,10 @@ public class MetadataSyncAdapter { public static final int EXECUTE_APP_FUNCTIONS_TRUSTED = 10; public MetadataSyncAdapter( - @NonNull Executor syncExecutor, - @NonNull PackageManager packageManager, - @NonNull AppSearchManager appSearchManager) { - mSyncExecutor = Objects.requireNonNull(syncExecutor); + @NonNull PackageManager packageManager, @NonNull AppSearchManager appSearchManager) { mPackageManager = Objects.requireNonNull(packageManager); mAppSearchManager = Objects.requireNonNull(appSearchManager); + mExecutor = Executors.newSingleThreadExecutor(); } /** @@ -97,7 +104,7 @@ public class MetadataSyncAdapter { AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB) .build(); AndroidFuture<Boolean> settableSyncStatus = new AndroidFuture<>(); - mSyncExecutor.execute( + Runnable runnable = () -> { try (FutureAppSearchSession staticMetadataSearchSession = new FutureAppSearchSessionImpl( @@ -117,10 +124,23 @@ public class MetadataSyncAdapter { } catch (Exception ex) { settableSyncStatus.completeExceptionally(ex); } - }); + }; + + synchronized (mLock) { + if (mCurrentSyncTask != null && !mCurrentSyncTask.isDone()) { + var unused = mCurrentSyncTask.cancel(false); + } + mCurrentSyncTask = mExecutor.submit(runnable); + } + return settableSyncStatus; } + /** This method shuts down the {@link MetadataSyncAdapter} scheduler. */ + public void shutDown() { + mExecutor.shutdown(); + } + @WorkerThread @VisibleForTesting void trySyncAppFunctionMetadataBlocking( diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java index f421527e72d0..e933ec1ba4b1 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java +++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java @@ -55,10 +55,7 @@ public final class MetadataSyncPerUser { PackageManager perUserPackageManager = userContext.getPackageManager(); if (perUserAppSearchManager != null) { metadataSyncAdapter = - new MetadataSyncAdapter( - AppFunctionExecutors.getPerUserSyncExecutor(user), - perUserPackageManager, - perUserAppSearchManager); + new MetadataSyncAdapter(perUserPackageManager, perUserAppSearchManager); sPerUserMetadataSyncAdapter.put(user.getIdentifier(), metadataSyncAdapter); return metadataSyncAdapter; } @@ -74,7 +71,12 @@ public final class MetadataSyncPerUser { */ public static void removeUserSyncAdapter(UserHandle user) { synchronized (sLock) { - sPerUserMetadataSyncAdapter.remove(user.getIdentifier()); + MetadataSyncAdapter metadataSyncAdapter = + sPerUserMetadataSyncAdapter.get(user.getIdentifier(), null); + if (metadataSyncAdapter != null) { + metadataSyncAdapter.shutDown(); + sPerUserMetadataSyncAdapter.remove(user.getIdentifier()); + } } } } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 6ae6f3d4713a..6af4be50b00c 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -619,7 +619,7 @@ public class AppOpsService extends IAppOpsService.Stub { this.op = op; this.uid = uid; this.uidState = uidState; - this.packageName = packageName; + this.packageName = packageName.intern(); // We keep an invariant that the persistent device will always have an entry in // mDeviceAttributedOps. mDeviceAttributedOps.put(PERSISTENT_DEVICE_ID_DEFAULT, @@ -1031,7 +1031,7 @@ public class AppOpsService extends IAppOpsService.Stub { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - String pkgName = intent.getData().getEncodedSchemeSpecificPart(); + String pkgName = intent.getData().getEncodedSchemeSpecificPart().intern(); int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); if (action.equals(ACTION_PACKAGE_ADDED) @@ -1235,7 +1235,7 @@ public class AppOpsService extends IAppOpsService.Stub { Ops ops = uidState.pkgOps.get(packageName); if (ops == null) { ops = new Ops(packageName, uidState); - uidState.pkgOps.put(packageName, ops); + uidState.pkgOps.put(packageName.intern(), ops); } SparseIntArray packageModes = @@ -4739,7 +4739,7 @@ public class AppOpsService extends IAppOpsService.Stub { return null; } ops = new Ops(packageName, uidState); - uidState.pkgOps.put(packageName, ops); + uidState.pkgOps.put(packageName.intern(), ops); } if (edit) { @@ -5076,7 +5076,7 @@ public class AppOpsService extends IAppOpsService.Stub { Ops ops = uidState.pkgOps.get(pkgName); if (ops == null) { ops = new Ops(pkgName, uidState); - uidState.pkgOps.put(pkgName, ops); + uidState.pkgOps.put(pkgName.intern(), ops); } ops.put(op.op, op); } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 55d9c6eac87a..0fd22c583192 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -17,6 +17,11 @@ package com.android.server.audio; import static android.media.audio.Flags.scoManagedByAudio; +import static com.android.media.audio.Flags.equalScoLeaVcIndexRange; +import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_BLE_HEADSET; +import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_BLE_SPEAKER; +import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_SCO; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.compat.CompatChanges; @@ -64,6 +69,7 @@ import android.util.Pair; import android.util.PrintWriterPrinter; import com.android.internal.annotations.GuardedBy; +import com.android.server.audio.AudioService.BtCommDeviceActiveType; import com.android.server.utils.EventLogger; import java.io.PrintWriter; @@ -333,8 +339,8 @@ public class AudioDeviceBroker { Log.v(TAG, "setCommunicationDevice, device: " + device + ", uid: " + uid); } - synchronized (mDeviceStateLock) { - if (device == null) { + if (device == null) { + synchronized (mDeviceStateLock) { CommunicationRouteClient client = getCommunicationRouteClientForUid(uid); if (client == null) { return false; @@ -835,15 +841,15 @@ public class AudioDeviceBroker { return isDeviceOnForCommunication(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); } - /*package*/ boolean isBluetoothScoActive() { + private boolean isBluetoothScoActive() { return isDeviceActiveForCommunication(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); } - /*package*/ boolean isBluetoothBleHeadsetActive() { + private boolean isBluetoothBleHeadsetActive() { return isDeviceActiveForCommunication(AudioDeviceInfo.TYPE_BLE_HEADSET); } - /*package*/ boolean isBluetoothBleSpeakerActive() { + private boolean isBluetoothBleSpeakerActive() { return isDeviceActiveForCommunication(AudioDeviceInfo.TYPE_BLE_SPEAKER); } @@ -1437,7 +1443,20 @@ public class AudioDeviceBroker { } mCurCommunicationPortId = portId; - mAudioService.postScoDeviceActive(isBluetoothScoActive()); + @BtCommDeviceActiveType int btCommDeviceActiveType = 0; + if (equalScoLeaVcIndexRange()) { + if (isBluetoothScoActive()) { + btCommDeviceActiveType = BT_COMM_DEVICE_ACTIVE_SCO; + } else if (isBluetoothBleHeadsetActive()) { + btCommDeviceActiveType = BT_COMM_DEVICE_ACTIVE_BLE_HEADSET; + } else if (isBluetoothBleSpeakerActive()) { + btCommDeviceActiveType = BT_COMM_DEVICE_ACTIVE_BLE_SPEAKER; + } + mAudioService.postBtCommDeviceActive(btCommDeviceActiveType); + } else { + mAudioService.postBtCommDeviceActive( + isBluetoothScoActive() ? BT_COMM_DEVICE_ACTIVE_SCO : btCommDeviceActiveType); + } final int nbDispatchers = mCommDevDispatchers.beginBroadcast(); for (int i = 0; i < nbDispatchers; i++) { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index df69afe6ed79..e83b03690a24 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -66,6 +66,7 @@ import static com.android.media.audio.Flags.alarmMinVolumeZero; import static com.android.media.audio.Flags.asDeviceConnectionFailure; import static com.android.media.audio.Flags.audioserverPermissions; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; +import static com.android.media.audio.Flags.equalScoLeaVcIndexRange; import static com.android.media.audio.Flags.replaceStreamBtSco; import static com.android.media.audio.Flags.ringerModeAffectsAlarm; import static com.android.media.audio.Flags.setStreamVolumeOrder; @@ -470,7 +471,7 @@ public class AudioService extends IAudioService.Stub private static final int MSG_CONFIGURATION_CHANGED = 54; private static final int MSG_BROADCAST_MASTER_MUTE = 55; private static final int MSG_UPDATE_CONTEXTUAL_VOLUMES = 56; - private static final int MSG_SCO_DEVICE_ACTIVE_UPDATE = 57; + private static final int MSG_BT_COMM_DEVICE_ACTIVE_UPDATE = 57; /** * Messages handled by the {@link SoundDoseHelper}, do not exceed @@ -766,7 +767,21 @@ public class AudioService extends IAudioService.Stub * @see System#MUTE_STREAMS_AFFECTED */ private int mUserMutableStreams; - private final AtomicBoolean mScoDeviceActive = new AtomicBoolean(false); + /** The active bluetooth device type used for communication is sco. */ + /*package*/ static final int BT_COMM_DEVICE_ACTIVE_SCO = 1; + /** The active bluetooth device type used for communication is ble headset. */ + /*package*/ static final int BT_COMM_DEVICE_ACTIVE_BLE_HEADSET = 1 << 1; + /** The active bluetooth device type used for communication is ble speaker. */ + /*package*/ static final int BT_COMM_DEVICE_ACTIVE_BLE_SPEAKER = 1 << 2; + @IntDef({ + BT_COMM_DEVICE_ACTIVE_SCO, BT_COMM_DEVICE_ACTIVE_BLE_HEADSET, + BT_COMM_DEVICE_ACTIVE_BLE_SPEAKER + }) + @Retention(RetentionPolicy.SOURCE) + public @interface BtCommDeviceActiveType { + } + + private final AtomicInteger mBtCommDeviceActive = new AtomicInteger(0); @NonNull private SoundEffectsHelper mSfxHelper; @@ -2522,12 +2537,18 @@ public class AudioService extends IAudioService.Stub // this should not happen, throwing exception throw new IllegalArgumentException("STREAM_BLUETOOTH_SCO is deprecated"); } - return streamType == AudioSystem.STREAM_VOICE_CALL && mScoDeviceActive.get(); + return streamType == AudioSystem.STREAM_VOICE_CALL + && mBtCommDeviceActive.get() == BT_COMM_DEVICE_ACTIVE_SCO; } else { return streamType == AudioSystem.STREAM_BLUETOOTH_SCO; } } + private boolean isStreamBluetoothComm(int streamType) { + return (streamType == AudioSystem.STREAM_VOICE_CALL && mBtCommDeviceActive.get() != 0) + || streamType == AudioSystem.STREAM_BLUETOOTH_SCO; + } + private void dumpStreamStates(PrintWriter pw) { pw.println("\nStream volumes (device: index)"); int numStreamTypes = AudioSystem.getNumStreamTypes(); @@ -4761,7 +4782,7 @@ public class AudioService extends IAudioService.Stub + asDeviceConnectionFailure()); pw.println("\tandroid.media.audio.autoPublicVolumeApiHardening:" + autoPublicVolumeApiHardening()); - pw.println("\tandroid.media.audio.Flags.automaticBtDeviceType:" + pw.println("\tandroid.media.audio.automaticBtDeviceType:" + automaticBtDeviceType()); pw.println("\tandroid.media.audio.featureSpatialAudioHeadtrackingLowLatency:" + featureSpatialAudioHeadtrackingLowLatency()); @@ -4783,6 +4804,8 @@ public class AudioService extends IAudioService.Stub + absVolumeIndexFix()); pw.println("\tcom.android.media.audio.replaceStreamBtSco:" + replaceStreamBtSco()); + pw.println("\tcom.android.media.audio.equalScoLeaVcIndexRange:" + + equalScoLeaVcIndexRange()); } private void dumpAudioMode(PrintWriter pw) { @@ -4896,7 +4919,7 @@ public class AudioService extends IAudioService.Stub final VolumeStreamState streamState = getVssForStreamOrDefault(streamTypeAlias); if (!replaceStreamBtSco() && (streamType == AudioManager.STREAM_VOICE_CALL) - && isInCommunication() && mDeviceBroker.isBluetoothScoActive()) { + && isInCommunication() && mBtCommDeviceActive.get() == BT_COMM_DEVICE_ACTIVE_SCO) { Log.i(TAG, "setStreamVolume for STREAM_VOICE_CALL, switching to STREAM_BLUETOOTH_SCO"); streamType = AudioManager.STREAM_BLUETOOTH_SCO; } @@ -5947,10 +5970,10 @@ public class AudioService extends IAudioService.Stub final boolean ringerModeMute = ringerMode == AudioManager.RINGER_MODE_VIBRATE || ringerMode == AudioManager.RINGER_MODE_SILENT; final boolean shouldRingSco = ringerMode == AudioManager.RINGER_MODE_VIBRATE - && mDeviceBroker.isBluetoothScoActive(); + && mBtCommDeviceActive.get() == BT_COMM_DEVICE_ACTIVE_SCO; final boolean shouldRingBle = ringerMode == AudioManager.RINGER_MODE_VIBRATE - && (mDeviceBroker.isBluetoothBleHeadsetActive() - || mDeviceBroker.isBluetoothBleSpeakerActive()); + && (mBtCommDeviceActive.get() == BT_COMM_DEVICE_ACTIVE_BLE_HEADSET + || mBtCommDeviceActive.get() == BT_COMM_DEVICE_ACTIVE_BLE_SPEAKER); // Ask audio policy engine to force use Bluetooth SCO/BLE channel if needed final String eventSource = "muteRingerModeStreams() from u/pid:" + Binder.getCallingUid() + "/" + Binder.getCallingPid(); @@ -7419,7 +7442,8 @@ public class AudioService extends IAudioService.Stub case AudioSystem.PLATFORM_VOICE: if (isInCommunication() || mAudioSystem.isStreamActive(AudioManager.STREAM_VOICE_CALL, 0)) { - if (!replaceStreamBtSco() && mDeviceBroker.isBluetoothScoActive()) { + if (!replaceStreamBtSco() + && mBtCommDeviceActive.get() == BT_COMM_DEVICE_ACTIVE_SCO) { if (DEBUG_VOL) { Log.v(TAG, "getActiveStreamType: Forcing STREAM_BLUETOOTH_SCO..."); } @@ -7463,7 +7487,8 @@ public class AudioService extends IAudioService.Stub } default: if (isInCommunication()) { - if (!replaceStreamBtSco() && mDeviceBroker.isBluetoothScoActive()) { + if (!replaceStreamBtSco() + && mBtCommDeviceActive.get() == BT_COMM_DEVICE_ACTIVE_SCO) { if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_BLUETOOTH_SCO"); return AudioSystem.STREAM_BLUETOOTH_SCO; } else { @@ -7788,15 +7813,15 @@ public class AudioService extends IAudioService.Stub 0 /*delay*/); } - /*package*/ void postScoDeviceActive(boolean scoDeviceActive) { + /*package*/ void postBtCommDeviceActive(@BtCommDeviceActiveType int btCommDeviceActive) { sendMsg(mAudioHandler, - MSG_SCO_DEVICE_ACTIVE_UPDATE, - SENDMSG_QUEUE, scoDeviceActive ? 1 : 0 /*arg1*/, 0 /*arg2*/, null /*obj*/, + MSG_BT_COMM_DEVICE_ACTIVE_UPDATE, + SENDMSG_QUEUE, btCommDeviceActive /*arg1*/, 0 /*arg2*/, null /*obj*/, 0 /*delay*/); } - private void onUpdateScoDeviceActive(boolean scoDeviceActive) { - if (mScoDeviceActive.compareAndSet(!scoDeviceActive, scoDeviceActive)) { + private void onUpdateBtCommDeviceActive(@BtCommDeviceActiveType int btCommDeviceActive) { + if (mBtCommDeviceActive.getAndSet(btCommDeviceActive) != btCommDeviceActive) { getVssForStreamOrDefault(AudioSystem.STREAM_VOICE_CALL).updateIndexFactors(); } } @@ -8997,7 +9022,7 @@ public class AudioService extends IAudioService.Stub } public void updateIndexFactors() { - if (!replaceStreamBtSco()) { + if (!replaceStreamBtSco() && !equalScoLeaVcIndexRange()) { return; } @@ -9008,10 +9033,18 @@ public class AudioService extends IAudioService.Stub mIndexMax = MAX_STREAM_VOLUME[AudioSystem.STREAM_BLUETOOTH_SCO] * 10; } - // SCO devices have a different min index - if (isStreamBluetoothSco(mStreamType)) { + if (!equalScoLeaVcIndexRange() && isStreamBluetoothSco(mStreamType)) { + // SCO devices have a different min index mIndexMin = MIN_STREAM_VOLUME[AudioSystem.STREAM_BLUETOOTH_SCO] * 10; mIndexStepFactor = 1.f; + } else if (equalScoLeaVcIndexRange() && isStreamBluetoothComm(mStreamType)) { + // For non SCO devices the stream state does not change the min index + if (mBtCommDeviceActive.get() == BT_COMM_DEVICE_ACTIVE_SCO) { + mIndexMin = MIN_STREAM_VOLUME[AudioSystem.STREAM_BLUETOOTH_SCO] * 10; + } else { + mIndexMin = MIN_STREAM_VOLUME[mStreamType] * 10; + } + mIndexStepFactor = 1.f; } else { mIndexMin = MIN_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL] * 10; mIndexStepFactor = (float) (mIndexMax - mIndexMin) / (float) ( @@ -9207,7 +9240,7 @@ public class AudioService extends IAudioService.Stub private void setStreamVolumeIndex(int index, int device) { // Only set audio policy BT SCO stream volume to 0 when the stream is actually muted. // This allows RX path muting by the audio HAL only when explicitly muted but not when - // index is just set to 0 to repect BT requirements + // index is just set to 0 to respect BT requirements if (isStreamBluetoothSco(mStreamType) && index == 0 && !isFullyMuted()) { index = 1; } @@ -10217,8 +10250,8 @@ public class AudioService extends IAudioService.Stub onUpdateContextualVolumes(); break; - case MSG_SCO_DEVICE_ACTIVE_UPDATE: - onUpdateScoDeviceActive(msg.arg1 != 0); + case MSG_BT_COMM_DEVICE_ACTIVE_UPDATE: + onUpdateBtCommDeviceActive(msg.arg1); break; case MusicFxHelper.MSG_EFFECT_CLIENT_GONE: diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index 5d850896d5de..2d802b21cf03 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -49,7 +49,6 @@ import android.hardware.biometrics.ITestSessionCallback; import android.hardware.biometrics.PromptInfo; import android.hardware.biometrics.SensorLocationInternal; import android.hardware.biometrics.SensorPropertiesInternal; -import android.hardware.biometrics.face.IFace; import android.hardware.face.FaceSensorConfigurations; import android.hardware.face.FaceSensorProperties; import android.hardware.face.FaceSensorPropertiesInternal; @@ -73,6 +72,7 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.server.SystemService; +import com.android.server.biometrics.sensors.face.FaceService; import com.android.server.biometrics.sensors.fingerprint.FingerprintService; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; @@ -211,7 +211,7 @@ public class AuthService extends SystemService { */ @VisibleForTesting public String[] getFaceAidlInstances() { - return ServiceManager.getDeclaredInstances(IFace.DESCRIPTOR); + return FaceService.getDeclaredInstances(); } /** diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java index bd6d59391e4a..8c988729a1ae 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java @@ -19,6 +19,7 @@ package com.android.server.biometrics.sensors.face; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.Manifest.permission.MANAGE_FACE; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; +import static android.hardware.face.FaceSensorConfigurations.getIFace; import android.annotation.NonNull; import android.annotation.Nullable; @@ -60,6 +61,7 @@ import android.util.proto.ProtoOutputStream; import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.internal.widget.LockPatternUtils; import com.android.server.SystemService; @@ -753,7 +755,7 @@ public class FaceService extends SystemService { public FaceService(Context context) { this(context, null /* faceProviderFunction */, () -> IBiometricService.Stub.asInterface( ServiceManager.getService(Context.BIOMETRIC_SERVICE)), null /* faceProvider */, - () -> ServiceManager.getDeclaredInstances(IFace.DESCRIPTOR)); + () -> getDeclaredInstances()); } @VisibleForTesting FaceService(Context context, @@ -778,8 +780,7 @@ public class FaceService extends SystemService { mFaceProvider = faceProvider != null ? faceProvider : (name) -> { final String fqName = IFace.DESCRIPTOR + "/" + name; - final IFace face = IFace.Stub.asInterface( - Binder.allowBlocking(ServiceManager.waitForDeclaredService(fqName))); + final IFace face = getIFace(fqName); if (face == null) { Slog.e(TAG, "Unable to get declared service: " + fqName); return null; @@ -835,6 +836,23 @@ public class FaceService extends SystemService { */ public static native void releaseSurfaceHandle(@NonNull NativeHandle handle); + /** + * Get all face hal instances declared in manifest + * @return instance names + */ + public static String[] getDeclaredInstances() { + String[] a = ServiceManager.getDeclaredInstances(IFace.DESCRIPTOR); + Slog.i(TAG, "Before:getDeclaredInstances: IFace instance found, a.length=" + + a.length); + if (!ArrayUtils.contains(a, "virtual")) { + // Now, the virtual hal is registered with IVirtualHal interface and it is also + // moved from vendor to system_ext partition without a device manifest. So + // if the old vhal is not declared, add here. + a = ArrayUtils.appendElement(String.class, a, "virtual"); + } + Slog.i(TAG, "After:getDeclaredInstances: a.length=" + a.length); + return a; + } void syncEnrollmentsNow() { Utils.checkPermissionOrShell(getContext(), MANAGE_FACE); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java index dca14914a572..3ed01d5a2cc9 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java @@ -23,6 +23,9 @@ import android.hardware.biometrics.ITestSessionCallback; import android.hardware.biometrics.face.AuthenticationFrame; import android.hardware.biometrics.face.BaseFrame; import android.hardware.biometrics.face.EnrollmentFrame; +import android.hardware.biometrics.face.virtualhal.AcquiredInfoAndVendorCode; +import android.hardware.biometrics.face.virtualhal.EnrollmentProgressStep; +import android.hardware.biometrics.face.virtualhal.NextEnrollment; import android.hardware.face.Face; import android.hardware.face.FaceAuthenticationFrame; import android.hardware.face.FaceEnrollFrame; @@ -50,6 +53,7 @@ import java.util.Set; public class BiometricTestSessionImpl extends ITestSession.Stub { private static final String TAG = "face/aidl/BiometricTestSessionImpl"; + private static final int VHAL_ENROLLMENT_ID = 9999; @NonNull private final Context mContext; private final int mSensorId; @@ -144,16 +148,35 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { super.setTestHalEnabled_enforcePermission(); - mProvider.setTestHalEnabled(enabled); mSensor.setTestHalEnabled(enabled); + mProvider.setTestHalEnabled(enabled); } @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) @Override - public void startEnroll(int userId) { + public void startEnroll(int userId) throws RemoteException { super.startEnroll_enforcePermission(); + Slog.i(TAG, "startEnroll(): isVhalForTesting=" + mProvider.isVhalForTesting()); + if (mProvider.isVhalForTesting()) { + final AcquiredInfoAndVendorCode[] acquiredInfoAndVendorCodes = + {new AcquiredInfoAndVendorCode()}; + final EnrollmentProgressStep[] enrollmentProgressSteps = + {new EnrollmentProgressStep(), new EnrollmentProgressStep()}; + enrollmentProgressSteps[0].durationMs = 100; + enrollmentProgressSteps[0].acquiredInfoAndVendorCodes = acquiredInfoAndVendorCodes; + enrollmentProgressSteps[1].durationMs = 200; + enrollmentProgressSteps[1].acquiredInfoAndVendorCodes = acquiredInfoAndVendorCodes; + + final NextEnrollment nextEnrollment = new NextEnrollment(); + nextEnrollment.id = VHAL_ENROLLMENT_ID; + nextEnrollment.progressSteps = enrollmentProgressSteps; + nextEnrollment.result = true; + mProvider.getVhal().setNextEnrollment(nextEnrollment); + mProvider.getVhal().setOperationAuthenticateDuration(6000); + } + mProvider.scheduleEnroll(mSensorId, new Binder(), new byte[69], userId, mReceiver, mContext.getOpPackageName(), new int[0] /* disabledFeatures */, null /* previewSurface */, false /* debugConsent */, @@ -166,6 +189,10 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { super.finishEnroll_enforcePermission(); + if (mProvider.isVhalForTesting()) { + return; + } + int nextRandomId = mRandom.nextInt(); while (mEnrollmentIds.contains(nextRandomId)) { nextRandomId = mRandom.nextInt(); @@ -178,11 +205,16 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) @Override - public void acceptAuthentication(int userId) { + public void acceptAuthentication(int userId) throws RemoteException { // Fake authentication with any of the existing faces super.acceptAuthentication_enforcePermission(); + if (mProvider.isVhalForTesting()) { + mProvider.getVhal().setEnrollmentHit(VHAL_ENROLLMENT_ID); + return; + } + List<Face> faces = FaceUtils.getInstance(mSensorId) .getBiometricsForUser(mContext, userId); if (faces.isEmpty()) { @@ -196,10 +228,15 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) @Override - public void rejectAuthentication(int userId) { + public void rejectAuthentication(int userId) throws RemoteException { super.rejectAuthentication_enforcePermission(); + if (mProvider.isVhalForTesting()) { + mProvider.getVhal().setEnrollmentHit(VHAL_ENROLLMENT_ID + 1); + return; + } + mSensor.getSessionForUser(userId).getHalSessionCallback().onAuthenticationFailed(); } @@ -236,11 +273,17 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) @Override - public void cleanupInternalState(int userId) { + public void cleanupInternalState(int userId) throws RemoteException { super.cleanupInternalState_enforcePermission(); Slog.d(TAG, "cleanupInternalState: " + userId); + + if (mProvider.isVhalForTesting()) { + Slog.i(TAG, "cleanup virtualhal configurations"); + mProvider.getVhal().resetConfigurations(); //setEnrollments(new int[]{}); + } + mProvider.scheduleInternalCleanup(mSensorId, userId, new ClientMonitorCallback() { @Override public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index bb213bfa79e6..5127e68a9df3 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -16,6 +16,9 @@ package com.android.server.biometrics.sensors.face.aidl; +import static android.hardware.face.FaceSensorConfigurations.getIFace; +import static android.hardware.face.FaceSensorConfigurations.remapFqName; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -32,6 +35,7 @@ import android.hardware.biometrics.ITestSession; import android.hardware.biometrics.ITestSessionCallback; import android.hardware.biometrics.face.IFace; import android.hardware.biometrics.face.SensorProps; +import android.hardware.biometrics.face.virtualhal.IVirtualHal; import android.hardware.face.Face; import android.hardware.face.FaceAuthenticateOptions; import android.hardware.face.FaceEnrollOptions; @@ -54,6 +58,7 @@ import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver; import com.android.server.biometrics.AuthenticationStatsCollector; import com.android.server.biometrics.BiometricDanglingReceiver; import com.android.server.biometrics.BiometricHandlerProvider; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; @@ -130,6 +135,11 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { private AuthenticationStatsCollector mAuthenticationStatsCollector; @Nullable private IFace mDaemon; + @Nullable + private IVirtualHal mVhal; + @Nullable + private String mHalInstanceNameCurrent; + private final class BiometricTaskStackListener extends TaskStackListener { @Override @@ -286,14 +296,37 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { if (mTestHalEnabled) { return true; } - return ServiceManager.checkService(IFace.DESCRIPTOR + "/" + mHalInstanceName) != null; + return ServiceManager.checkService( + remapFqName(IFace.DESCRIPTOR + "/" + mHalInstanceName)) != null; } @Nullable @VisibleForTesting synchronized IFace getHalInstance() { if (mTestHalEnabled) { - return new TestHal(); + if (Flags.useVhalForTesting()) { + if (!mHalInstanceNameCurrent.contains("virtual")) { + Slog.i(getTag(), "Switching face hal from " + mHalInstanceName + + " to virtual hal"); + mHalInstanceNameCurrent = "virtual"; + mDaemon = null; + } + } else { + // Enabling the test HAL for a single sensor in a multi-sensor HAL currently enables + // the test HAL for all sensors under that HAL. This can be updated in the future if + // necessary. + return new TestHal(); + } + } else { + if (mHalInstanceNameCurrent == null) { + mHalInstanceNameCurrent = mHalInstanceName; + } else if (mHalInstanceNameCurrent.contains("virtual") + && mHalInstanceNameCurrent != mHalInstanceName) { + Slog.i(getTag(), "Switching face from virtual hal " + "to " + + mHalInstanceName); + mHalInstanceNameCurrent = mHalInstanceName; + mDaemon = null; + } } if (mDaemon != null) { @@ -302,10 +335,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { Slog.d(getTag(), "Daemon was null, reconnecting"); - mDaemon = IFace.Stub.asInterface( - Binder.allowBlocking( - ServiceManager.waitForDeclaredService( - IFace.DESCRIPTOR + "/" + mHalInstanceName))); + mDaemon = getIFace(IFace.DESCRIPTOR + "/" + mHalInstanceNameCurrent); if (mDaemon == null) { Slog.e(getTag(), "Unable to get daemon"); return null; @@ -833,7 +863,13 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { } void setTestHalEnabled(boolean enabled) { + final boolean changed = enabled != mTestHalEnabled; mTestHalEnabled = enabled; + Slog.i(getTag(), "setTestHalEnabled(): isVhalForTestingFlags=" + Flags.useVhalForTesting() + + " mTestHalEnabled=" + mTestHalEnabled + " changed=" + changed); + if (changed && isVhalForTesting()) { + getHalInstance(); + } } @Override @@ -851,9 +887,40 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { } /** + * Return true if vhal_for_testing feature is enabled and test is active + */ + public boolean isVhalForTesting() { + return (Flags.useVhalForTesting() && mTestHalEnabled); + } + + + /** * Sends a face re enroll notification. */ public void sendFaceReEnrollNotification() { mAuthenticationStatsCollector.sendFaceReEnrollNotification(); } + + /** + * Sends a fingerprint enroll notification. + */ + public void sendFingerprintReEnrollNotification() { + mAuthenticationStatsCollector.sendFingerprintReEnrollNotification(); + } + + /** + * Return virtual hal AIDL interface if it is used for testing + * + */ + public IVirtualHal getVhal() throws RemoteException { + if (mVhal == null && isVhalForTesting()) { + mVhal = IVirtualHal.Stub.asInterface( + Binder.allowBlocking( + ServiceManager.waitForService( + IVirtualHal.DESCRIPTOR + "/" + + mHalInstanceNameCurrent))); + Slog.d(getTag(), "getVhal " + mHalInstanceNameCurrent); + } + return mVhal; + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java index 6f9534993a3f..9fddcfc199b9 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java @@ -17,6 +17,7 @@ package com.android.server.biometrics.sensors.face.aidl; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE; +import static android.hardware.face.FaceSensorConfigurations.remapFqName; import android.annotation.NonNull; import android.annotation.Nullable; @@ -337,7 +338,8 @@ public class Sensor { if (mTestHalEnabled) { return true; } - return ServiceManager.checkService(IFace.DESCRIPTOR + "/" + halInstanceName) != null; + return ServiceManager.checkService( + remapFqName(IFace.DESCRIPTOR + "/" + halInstanceName)) != null; } /** diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index e7fd8f7db182..ae33b83b49dc 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -571,6 +571,10 @@ public final class DisplayManagerService extends SystemService { private final DisplayNotificationManager mDisplayNotificationManager; private final ExternalDisplayStatsService mExternalDisplayStatsService; + // Manages the relative placement of extended displays + @Nullable + private final DisplayTopologyCoordinator mDisplayTopologyCoordinator; + /** * Applications use {@link android.view.Display#getRefreshRate} and * {@link android.view.Display.Mode#getRefreshRate} to know what is the display refresh rate. @@ -644,6 +648,11 @@ public final class DisplayManagerService extends SystemService { mDisplayNotificationManager = new DisplayNotificationManager(mFlags, mContext, mExternalDisplayStatsService); mExternalDisplayPolicy = new ExternalDisplayPolicy(new ExternalDisplayPolicyInjector()); + if (mFlags.isDisplayTopologyEnabled()) { + mDisplayTopologyCoordinator = new DisplayTopologyCoordinator(); + } else { + mDisplayTopologyCoordinator = null; + } } public void setupSchedulerPolicies() { @@ -3474,9 +3483,13 @@ public final class DisplayManagerService extends SystemService { mSmallAreaDetectionController.dump(pw); } + if (mDisplayTopologyCoordinator != null) { + pw.println(); + mDisplayTopologyCoordinator.dump(pw); + } + pw.println(); mFlags.dump(pw); - } private static float[] getFloatArray(TypedArray array) { diff --git a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java new file mode 100644 index 000000000000..631f14755b12 --- /dev/null +++ b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java @@ -0,0 +1,104 @@ +/* + * 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.server.display; + +import android.annotation.Nullable; +import android.util.IndentingPrintWriter; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * This class manages the relative placement (topology) of extended displays. It is responsible for + * updating and persisting the topology. + */ +class DisplayTopologyCoordinator { + + /** + * The topology tree + */ + @Nullable + private TopologyTreeNode mRoot; + + /** + * The logical display ID of the primary display that will show certain UI elements. + * This is not necessarily the same as the default display. + */ + private int mPrimaryDisplayId; + + /** + * Print the object's state and debug information into the given stream. + * @param pw The stream to dump information to. + */ + public void dump(PrintWriter pw) { + pw.println("DisplayTopologyCoordinator:"); + pw.println("--------------------"); + IndentingPrintWriter ipw = new IndentingPrintWriter(pw); + ipw.increaseIndent(); + + ipw.println("mPrimaryDisplayId: " + mPrimaryDisplayId); + + ipw.println("Topology tree:"); + if (mRoot != null) { + ipw.increaseIndent(); + mRoot.dump(ipw); + ipw.decreaseIndent(); + } + } + + private static class TopologyTreeNode { + + /** + * The logical display ID + */ + private int mDisplayId; + + private final List<TopologyTreeNode> mChildren = new ArrayList<>(); + + /** + * The position of this display relative to its parent. + */ + private Position mPosition; + + /** + * The distance from the top edge of the parent display to the top edge of this display (in + * case of POSITION_LEFT or POSITION_RIGHT) or from the left edge of the parent display + * to the left edge of this display (in case of POSITION_TOP or POSITION_BOTTOM). The unit + * used is density-independent pixels (dp). + */ + private double mOffset; + + /** + * Print the object's state and debug information into the given stream. + * @param ipw The stream to dump information to. + */ + void dump(IndentingPrintWriter ipw) { + ipw.println("Display {id=" + mDisplayId + ", position=" + mPosition + + ", offset=" + mOffset + "}"); + ipw.increaseIndent(); + for (TopologyTreeNode child : mChildren) { + child.dump(ipw); + } + ipw.decreaseIndent(); + } + + private enum Position { + POSITION_LEFT, POSITION_TOP, POSITION_RIGHT, POSITION_BOTTOM + } + } +} diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index f600e7fc2946..df66893a2f35 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -69,6 +69,10 @@ public class DisplayManagerFlags { Flags.FLAG_ENABLE_MODE_LIMIT_FOR_EXTERNAL_DISPLAY, Flags::enableModeLimitForExternalDisplay); + private final FlagState mDisplayTopology = new FlagState( + Flags.FLAG_DISPLAY_TOPOLOGY, + Flags::displayTopology); + private final FlagState mConnectedDisplayErrorHandlingFlagState = new FlagState( Flags.FLAG_ENABLE_CONNECTED_DISPLAY_ERROR_HANDLING, Flags::enableConnectedDisplayErrorHandling); @@ -266,6 +270,10 @@ public class DisplayManagerFlags { return mExternalDisplayLimitModeState.isEnabled(); } + public boolean isDisplayTopologyEnabled() { + return mDisplayTopology.isEnabled(); + } + /** * @return Whether displays refresh rate synchronization is enabled. */ @@ -441,6 +449,7 @@ public class DisplayManagerFlags { pw.println(" " + mConnectedDisplayManagementFlagState); pw.println(" " + mDisplayOffloadFlagState); pw.println(" " + mExternalDisplayLimitModeState); + pw.println(" " + mDisplayTopology); pw.println(" " + mHdrClamperFlagState); pw.println(" " + mNbmControllerFlagState); pw.println(" " + mPowerThrottlingClamperFlagState); diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 9968ba57bba4..e3ebe5bcd9ed 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -92,6 +92,14 @@ flag { } flag { + name: "display_topology" + namespace: "display_manager" + description: "Display topology for moving cursors and windows between extended displays" + bug: "278199220" + is_fixed_read_only: true +} + +flag { name: "enable_displays_refresh_rates_synchronization" namespace: "display_manager" description: "Enables synchronization of refresh rates across displays" diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 101596d9d7c1..aae7b59b1a1a 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -261,6 +261,7 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { .setDisplayName(HdmiUtils.getDefaultDeviceName(source)) .setDeviceType(deviceTypes.get(0)) .setVendorId(Constants.VENDOR_ID_UNKNOWN) + .setPortId(mService.getHdmiCecNetwork().physicalAddressToPortId(physicalAddress)) .build(); mService.getHdmiCecNetwork().addCecDevice(newDevice); } @@ -1433,6 +1434,7 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { assertRunOnServiceThread(); mService.unregisterTvInputCallback(mTvInputCallback); + mTvInputs.clear(); // Remove any repeated working actions. // HotplugDetectionAction will be reinstated during the wake up process. // HdmiControlService.onWakeUp() -> initializeLocalDevices() -> diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java index f3514653518b..a1e5ebc002a5 100644 --- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java +++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java @@ -24,7 +24,6 @@ import android.content.res.Configuration; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Rect; -import android.hardware.input.InputManager; import android.util.Slog; import android.util.TypedValue; import android.view.Gravity; @@ -42,6 +41,7 @@ import com.android.server.input.TouchpadHardwareProperties; import com.android.server.input.TouchpadHardwareState; import java.util.Objects; +import java.util.function.Consumer; public class TouchpadDebugView extends LinearLayout { private static final float MAX_SCREEN_WIDTH_PROPORTION = 0.4f; @@ -54,7 +54,6 @@ public class TouchpadDebugView extends LinearLayout { private static final int ROUNDED_CORNER_RADIUS_DP = 24; private static final int BUTTON_PRESSED_BACKGROUND_COLOR = Color.rgb(118, 151, 99); private static final int BUTTON_RELEASED_BACKGROUND_COLOR = Color.rgb(84, 85, 169); - /** * Input device ID for the touchpad that this debug view is displaying. */ @@ -76,24 +75,24 @@ public class TouchpadDebugView extends LinearLayout { private int mWindowLocationBeforeDragX; private int mWindowLocationBeforeDragY; private int mLatestGestureType = 0; + private TouchpadSelectionView mTouchpadSelectionView; + private TouchpadVisualizationView mTouchpadVisualizationView; private TextView mGestureInfoView; - private TextView mNameView; - @NonNull private TouchpadHardwareState mLastTouchpadState = new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0, new TouchpadFingerState[0]); - private TouchpadVisualizationView mTouchpadVisualizationView; private final TouchpadHardwareProperties mTouchpadHardwareProperties; public TouchpadDebugView(Context context, int touchpadId, - TouchpadHardwareProperties touchpadHardwareProperties) { + TouchpadHardwareProperties touchpadHardwareProperties, + Consumer<Integer> touchpadSwitchHandler) { super(context); mTouchpadId = touchpadId; mWindowManager = Objects.requireNonNull(getContext().getSystemService(WindowManager.class)); mTouchpadHardwareProperties = touchpadHardwareProperties; - init(context, touchpadId); + init(context, touchpadId, touchpadSwitchHandler); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mWindowLayoutParams = new WindowManager.LayoutParams(); @@ -115,7 +114,8 @@ public class TouchpadDebugView extends LinearLayout { mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; } - private void init(Context context, int touchpadId) { + private void init(Context context, int touchpadId, + Consumer<Integer> touchpadSwitchHandler) { updateScreenDimensions(); setOrientation(VERTICAL); setLayoutParams(new LayoutParams( @@ -123,18 +123,14 @@ public class TouchpadDebugView extends LinearLayout { LayoutParams.WRAP_CONTENT)); setBackgroundColor(Color.TRANSPARENT); - mNameView = new TextView(context); - mNameView.setBackgroundColor(BUTTON_RELEASED_BACKGROUND_COLOR); - mNameView.setTextSize(TEXT_SIZE_SP); - mNameView.setText(Objects.requireNonNull(Objects.requireNonNull( - mContext.getSystemService(InputManager.class)) - .getInputDevice(touchpadId)).getName()); - mNameView.setGravity(Gravity.CENTER); - mNameView.setTextColor(Color.WHITE); + mTouchpadSelectionView = new TouchpadSelectionView(context, + touchpadId, touchpadSwitchHandler); + mTouchpadSelectionView.setBackgroundColor(BUTTON_RELEASED_BACKGROUND_COLOR); + mTouchpadSelectionView.setGravity(Gravity.CENTER); int paddingInDP = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, TEXT_PADDING_DP, getResources().getDisplayMetrics()); - mNameView.setPadding(paddingInDP, paddingInDP, paddingInDP, paddingInDP); - mNameView.setLayoutParams( + mTouchpadSelectionView.setPadding(paddingInDP, paddingInDP, paddingInDP, paddingInDP); + mTouchpadSelectionView.setLayoutParams( new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); mTouchpadVisualizationView = new TouchpadVisualizationView(context, @@ -147,10 +143,11 @@ public class TouchpadDebugView extends LinearLayout { mGestureInfoView.setPadding(paddingInDP, paddingInDP, paddingInDP, paddingInDP); mGestureInfoView.setLayoutParams( new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + //TODO(b/369061237): Handle longer text updateTheme(getResources().getConfiguration().uiMode); - addView(mNameView); + addView(mTouchpadSelectionView); addView(mTouchpadVisualizationView); addView(mGestureInfoView); @@ -359,12 +356,12 @@ public class TouchpadDebugView extends LinearLayout { private void onTouchpadButtonPress() { Slog.d(TAG, "You clicked me!"); - mNameView.setBackgroundColor(BUTTON_PRESSED_BACKGROUND_COLOR); + mTouchpadSelectionView.setBackgroundColor(BUTTON_PRESSED_BACKGROUND_COLOR); } private void onTouchpadButtonRelease() { Slog.d(TAG, "You released the click"); - mNameView.setBackgroundColor(BUTTON_RELEASED_BACKGROUND_COLOR); + mTouchpadSelectionView.setBackgroundColor(BUTTON_RELEASED_BACKGROUND_COLOR); } /** diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java index cb43977d9911..19c802b3c096 100644 --- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java +++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java @@ -45,8 +45,8 @@ public class TouchpadDebugViewController implements InputManager.InputDeviceList private boolean mTouchpadVisualizerEnabled = false; public TouchpadDebugViewController(Context context, Looper looper, - InputManagerService inputManagerService) { - //TODO(b/363979581): Handle multi-display scenarios + InputManagerService inputManagerService) { + //TODO(b/369059937): Handle multi-display scenarios mContext = context; mHandler = new Handler(looper); mInputManagerService = inputManagerService; @@ -77,6 +77,14 @@ public class TouchpadDebugViewController implements InputManager.InputDeviceList } } + /** + * Switch to showing the touchpad with the given device ID + */ + public void switchVisualisationToTouchpadId(int newDeviceId) { + if (mTouchpadDebugView != null) hideDebugView(mTouchpadDebugView.getTouchpadId()); + showDebugView(newDeviceId); + } + @Override public void onInputDeviceChanged(int deviceId) { } @@ -117,7 +125,7 @@ public class TouchpadDebugViewController implements InputManager.InputDeviceList touchpadId); mTouchpadDebugView = new TouchpadDebugView(mContext, touchpadId, - touchpadHardwareProperties); + touchpadHardwareProperties, this::switchVisualisationToTouchpadId); final WindowManager.LayoutParams mWindowLayoutParams = mTouchpadDebugView.getWindowLayoutParams(); @@ -150,7 +158,7 @@ public class TouchpadDebugViewController implements InputManager.InputDeviceList * @param deviceId the deviceId of the touchpad that is sending the hardware state */ public void updateTouchpadHardwareState(TouchpadHardwareState touchpadHardwareState, - int deviceId) { + int deviceId) { if (mTouchpadDebugView != null) { mTouchpadDebugView.updateHardwareState(touchpadHardwareState, deviceId); } diff --git a/services/core/java/com/android/server/input/debug/TouchpadSelectionView.java b/services/core/java/com/android/server/input/debug/TouchpadSelectionView.java new file mode 100644 index 000000000000..05217b6ab1e0 --- /dev/null +++ b/services/core/java/com/android/server/input/debug/TouchpadSelectionView.java @@ -0,0 +1,111 @@ +/* + * Copyright 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.server.input.debug; + +import android.content.Context; +import android.graphics.Color; +import android.hardware.input.InputManager; +import android.view.Gravity; +import android.view.InputDevice; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.PopupMenu; +import android.widget.TextView; + +import java.util.Objects; +import java.util.function.Consumer; + +public class TouchpadSelectionView extends LinearLayout { + private static final float TEXT_SIZE_SP = 16.0f; + + int mCurrentTouchpadId; + + public TouchpadSelectionView(Context context, int touchpadId, + Consumer<Integer> touchpadSwitchHandler) { + super(context); + mCurrentTouchpadId = touchpadId; + init(context, touchpadSwitchHandler); + } + + private void init(Context context, Consumer<Integer> touchpadSwitchHandler) { + setOrientation(HORIZONTAL); + setLayoutParams(new LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT)); + setBackgroundColor(Color.TRANSPARENT); + + TextView nameView = new TextView(context); + nameView.setTextSize(TEXT_SIZE_SP); + nameView.setText(getTouchpadName(mCurrentTouchpadId)); + nameView.setGravity(Gravity.LEFT); + nameView.setTextColor(Color.WHITE); + + LayoutParams textParams = new LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + textParams.rightMargin = 16; + nameView.setLayoutParams(textParams); + + ImageButton arrowButton = new ImageButton(context); + arrowButton.setImageDrawable(context.getDrawable(android.R.drawable.arrow_down_float)); + arrowButton.setForegroundGravity(Gravity.RIGHT); + arrowButton.setBackgroundColor(Color.TRANSPARENT); + arrowButton.setLayoutParams(new LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + arrowButton.setOnClickListener(v -> showPopupMenu(v, context, touchpadSwitchHandler)); + + addView(nameView); + addView(arrowButton); + } + + private void showPopupMenu(View anchorView, Context context, + Consumer<Integer> touchpadSwitchHandler) { + int i = 0; + PopupMenu popupMenu = new PopupMenu(context, anchorView); + + final InputManager inputManager = Objects.requireNonNull( + mContext.getSystemService(InputManager.class)); + for (int deviceId : inputManager.getInputDeviceIds()) { + InputDevice inputDevice = inputManager.getInputDevice(deviceId); + if (Objects.requireNonNull(inputDevice).supportsSource( + InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE)) { + popupMenu.getMenu().add(0, deviceId, i, getTouchpadName(deviceId)); + i++; + } + } + + popupMenu.setOnMenuItemClickListener(item -> { + if (item.getItemId() == mCurrentTouchpadId) { + return false; + } + + touchpadSwitchHandler.accept(item.getItemId()); + return true; + }); + + popupMenu.show(); + } + + private String getTouchpadName(int touchpadId) { + return Objects.requireNonNull(Objects.requireNonNull( + mContext.getSystemService(InputManager.class)) + .getInputDevice(touchpadId)).getName(); + } +} diff --git a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java index 96426bbfe4f3..eeec5ccdecf6 100644 --- a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java +++ b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java @@ -30,6 +30,7 @@ import com.android.server.input.TouchpadHardwareState; import java.util.ArrayDeque; import java.util.HashMap; +import java.util.Locale; import java.util.Map; public class TouchpadVisualizationView extends View { @@ -50,6 +51,7 @@ public class TouchpadVisualizationView extends View { private final Paint mOvalFillPaint; private final Paint mTracePaint; private final Paint mCenterPointPaint; + private final Paint mPressureTextPaint; private final RectF mTempOvalRect = new RectF(); public TouchpadVisualizationView(Context context, @@ -71,6 +73,8 @@ public class TouchpadVisualizationView extends View { mCenterPointPaint.setAntiAlias(true); mCenterPointPaint.setARGB(255, 255, 0, 0); mCenterPointPaint.setStrokeWidth(2); + mPressureTextPaint = new Paint(); + mPressureTextPaint.setAntiAlias(true); } private void removeOldPoints() { @@ -134,6 +138,13 @@ public class TouchpadVisualizationView extends View { mOvalFillPaint.setAlpha((int) pressureToOpacity); drawOval(canvas, newX, newY, newTouchMajor, newTouchMinor, newAngle); + + String formattedPressure = String.format(Locale.getDefault(), "Ps: %.2f", + touchpadFingerState.getPressure()); + float textWidth = mPressureTextPaint.measureText(formattedPressure); + + canvas.drawText(formattedPressure, newX - textWidth / 2, + newY - newTouchMajor / 2, mPressureTextPaint); } mTempFingerStatesByTrackingId.clear(); @@ -199,6 +210,7 @@ public class TouchpadVisualizationView extends View { */ public void setLightModeTheme() { this.setBackgroundColor(Color.rgb(20, 20, 20)); + mPressureTextPaint.setARGB(255, 255, 255, 255); mOvalFillPaint.setARGB(255, 255, 255, 255); mOvalStrokePaint.setARGB(255, 255, 255, 255); } @@ -208,6 +220,7 @@ public class TouchpadVisualizationView extends View { */ public void setNightModeTheme() { this.setBackgroundColor(Color.rgb(240, 240, 240)); + mPressureTextPaint.setARGB(255, 0, 0, 0); mOvalFillPaint.setARGB(255, 0, 0, 0); mOvalStrokePaint.setARGB(255, 0, 0, 0); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index ba7d4d218ca5..b15fcc917588 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -33,6 +33,7 @@ import static android.app.Notification.EXTRA_LARGE_ICON_BIG; import static android.app.Notification.EXTRA_SUB_TEXT; import static android.app.Notification.EXTRA_TEXT; import static android.app.Notification.EXTRA_TEXT_LINES; +import static android.app.Notification.EXTRA_TITLE; import static android.app.Notification.EXTRA_TITLE_BIG; import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; import static android.app.Notification.FLAG_AUTO_CANCEL; @@ -45,6 +46,7 @@ import static android.app.Notification.FLAG_NO_CLEAR; import static android.app.Notification.FLAG_NO_DISMISS; import static android.app.Notification.FLAG_ONGOING_EVENT; import static android.app.Notification.FLAG_ONLY_ALERT_ONCE; +import static android.app.Notification.FLAG_PROMOTED_ONGOING; import static android.app.Notification.FLAG_USER_INITIATED_JOB; import static android.app.NotificationChannel.CONVERSATION_CHANNEL_ID_FORMAT; import static android.app.NotificationChannel.NEWS_ID; @@ -3516,7 +3518,7 @@ public class NotificationManagerService extends SystemService { private String getHistoryTitle(Notification n) { CharSequence title = null; if (n.extras != null) { - title = n.extras.getCharSequence(Notification.EXTRA_TITLE); + title = n.extras.getCharSequence(EXTRA_TITLE); if (title == null) { title = n.extras.getCharSequence(EXTRA_TITLE_BIG); } @@ -4114,6 +4116,75 @@ public class NotificationManagerService extends SystemService { } @Override + @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING) + public boolean canBePromoted(String pkg, int uid) { + checkCallerIsSystemOrSystemUiOrShell(); + if (!android.app.Flags.uiRichOngoing()) { + return false; + } + return mPreferencesHelper.canBePromoted(pkg, uid); + } + + @Override + @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING) + public void setCanBePromoted(String pkg, int uid, boolean promote) { + checkCallerIsSystemOrSystemUiOrShell(); + if (!android.app.Flags.uiRichOngoing()) { + return; + } + boolean changed = mPreferencesHelper.setCanBePromoted(pkg, uid, promote); + if (changed) { + // check for pending/posted notifs from this app and update the flag + synchronized (mNotificationLock) { + // for enqueued we just need to update the flag + List<NotificationRecord> enqueued = findAppNotificationByListLocked( + mEnqueuedNotifications, pkg, UserHandle.getUserId(uid)); + for (NotificationRecord r : enqueued) { + if (promote + && r.getNotification().hasPromotableCharacteristics() + && r.getImportance() > IMPORTANCE_MIN) { + r.getNotification().flags |= FLAG_PROMOTED_ONGOING; + } else if (!promote) { + r.getNotification().flags &= ~FLAG_PROMOTED_ONGOING; + } + } + // if the notification is posted we need to update the flag and tell listeners + List<NotificationRecord> posted = findAppNotificationByListLocked( + mNotificationList, pkg, UserHandle.getUserId(uid)); + for (NotificationRecord r : posted) { + if (promote + && !hasFlag(r.getNotification().flags, FLAG_PROMOTED_ONGOING) + && r.getNotification().hasPromotableCharacteristics() + && r.getImportance() > IMPORTANCE_MIN) { + r.getNotification().flags |= FLAG_PROMOTED_ONGOING; + // we could set a wake lock here but this value should only change + // in response to user action, so the device should be awake long enough + // to post + PostNotificationTracker tracker = + mPostNotificationTrackerFactory.newTracker(null); + // Set false for isAppForeground because that field is only used + // for bubbles and messagingstyle can not be promoted + mHandler.post(new EnqueueNotificationRunnable( + r.getUser().getIdentifier(), + r, /* isAppForeground */ false, /* isAppProvided= */ false, + tracker)); + } else if (!promote + && hasFlag(r.getNotification().flags, FLAG_PROMOTED_ONGOING)){ + r.getNotification().flags &= ~FLAG_PROMOTED_ONGOING; + PostNotificationTracker tracker = + mPostNotificationTrackerFactory.newTracker(null); + mHandler.post(new EnqueueNotificationRunnable( + r.getUser().getIdentifier(), + r, /* isAppForeground */ false, /* isAppProvided= */ false, + tracker)); + } + } + } + handleSavePolicyFile(); + } + } + + @Override public boolean hasSentValidMsg(String pkg, int uid) { checkCallerIsSystem(); return mPreferencesHelper.hasSentValidMsg(pkg, uid); @@ -7698,6 +7769,16 @@ public class NotificationManagerService extends SystemService { return false; } + if (android.app.Flags.uiRichOngoing()) { + // This would normally be done in fixNotification(), but we need the channel info so + // it's done a little late + if (mPreferencesHelper.canBePromoted(pkg, notificationUid) + && notification.hasPromotableCharacteristics() + && channel.getImportance() > IMPORTANCE_MIN) { + notification.flags |= FLAG_PROMOTED_ONGOING; + } + } + final NotificationRecord r = new NotificationRecord(getContext(), n, channel); r.setIsAppImportanceLocked(mPermissionHelper.isPermissionUserSet(pkg, userId)); r.setPostSilently(postSilently); @@ -7938,6 +8019,9 @@ public class NotificationManagerService extends SystemService { } } + // Apps cannot set this flag + notification.flags &= ~FLAG_PROMOTED_ONGOING; + // Ensure CallStyle has all the correct actions if (notification.isStyle(Notification.CallStyle.class)) { Notification.Builder builder = @@ -8061,12 +8145,7 @@ public class NotificationManagerService extends SystemService { private void checkRemoteViews(String pkg, String tag, int id, Notification notification) { if (android.app.Flags.removeRemoteViews()) { - if (notification.contentView != null || notification.bigContentView != null - || notification.headsUpContentView != null - || (notification.publicVersion != null - && (notification.publicVersion.contentView != null - || notification.publicVersion.bigContentView != null - || notification.publicVersion.headsUpContentView != null))) { + if (notification.containsCustomViews()) { Slog.i(TAG, "Removed customViews for " + pkg); mUsageStats.registerImageRemoved(pkg); } @@ -9236,8 +9315,8 @@ public class NotificationManagerService extends SystemService { } } - final String oldTitle = String.valueOf(oldN.extras.get(Notification.EXTRA_TITLE)); - final String newTitle = String.valueOf(newN.extras.get(Notification.EXTRA_TITLE)); + final String oldTitle = String.valueOf(oldN.extras.get(EXTRA_TITLE)); + final String newTitle = String.valueOf(newN.extras.get(EXTRA_TITLE)); if (!Objects.equals(oldTitle, newTitle)) { if (DEBUG_INTERRUPTIVENESS) { Slog.v(TAG, "INTERRUPTIVENESS: " @@ -10654,6 +10733,22 @@ public class NotificationManagerService extends SystemService { } @GuardedBy("mNotificationLock") + @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING) + private @NonNull List<NotificationRecord> findAppNotificationByListLocked( + ArrayList<NotificationRecord> list, String pkg, int userId) { + List<NotificationRecord> records = new ArrayList<>(); + final int len = list.size(); + for (int i = 0; i < len; i++) { + NotificationRecord r = list.get(i); + if (notificationMatchesUserId(r, userId, false) + && r.getSbn().getPackageName().equals(pkg)) { + records.add(r); + } + } + return records; + } + + @GuardedBy("mNotificationLock") private @NonNull List<NotificationRecord> findGroupNotificationByListLocked( ArrayList<NotificationRecord> list, String pkg, String groupKey, int userId) { List<NotificationRecord> records = new ArrayList<>(); diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index a4fdb758a740..fcc8d2f74ce9 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -41,6 +41,7 @@ import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_P import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -162,6 +163,7 @@ public class PreferencesHelper implements RankingConfig { private static final String ATT_SENT_VALID_MESSAGE = "sent_valid_msg"; private static final String ATT_USER_DEMOTED_INVALID_MSG_APP = "user_demote_msg_app"; private static final String ATT_SENT_VALID_BUBBLE = "sent_valid_bubble"; + private static final String ATT_PROMOTE_NOTIFS = "promote"; private static final String ATT_CREATION_TIME = "creation_time"; @@ -351,6 +353,10 @@ public class PreferencesHelper implements RankingConfig { r.userDemotedMsgApp = parser.getAttributeBoolean( null, ATT_USER_DEMOTED_INVALID_MSG_APP, false); r.hasSentValidBubble = parser.getAttributeBoolean(null, ATT_SENT_VALID_BUBBLE, false); + if (android.app.Flags.uiRichOngoing()) { + r.canHavePromotedNotifs = + parser.getAttributeBoolean(null, ATT_PROMOTE_NOTIFS, false); + } final int innerDepth = parser.getDepth(); int type; @@ -739,6 +745,11 @@ public class PreferencesHelper implements RankingConfig { out.attributeBoolean(null, ATT_USER_DEMOTED_INVALID_MSG_APP, r.userDemotedMsgApp); out.attributeBoolean(null, ATT_SENT_VALID_BUBBLE, r.hasSentValidBubble); + if (android.app.Flags.uiRichOngoing()) { + if (r.canHavePromotedNotifs) { + out.attributeBoolean(null, ATT_PROMOTE_NOTIFS, r.canHavePromotedNotifs); + } + } if (Flags.persistIncompleteRestoreData() && r.uid == UNKNOWN_UID) { out.attributeLong(null, ATT_CREATION_TIME, r.creationTime); @@ -839,6 +850,28 @@ public class PreferencesHelper implements RankingConfig { } } + @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING) + public boolean canBePromoted(String packageName, int uid) { + synchronized (mLock) { + return getOrCreatePackagePreferencesLocked(packageName, uid).canHavePromotedNotifs; + } + } + + @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING) + public boolean setCanBePromoted(String packageName, int uid, boolean promote) { + boolean changed = false; + synchronized (mLock) { + PackagePreferences pkgPrefs = getOrCreatePackagePreferencesLocked(packageName, uid); + if (pkgPrefs.canHavePromotedNotifs != promote) { + pkgPrefs.canHavePromotedNotifs = promote; + changed = true; + } + } + // no need to send a ranking update because we need to update the flag value on all pending + // and posted notifs and NMS will take care of that + return changed; + } + public boolean isInInvalidMsgState(String packageName, int uid) { synchronized (mLock) { PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid); @@ -2180,6 +2213,10 @@ public class PreferencesHelper implements RankingConfig { pw.print(" fixedImportance="); pw.print(r.fixedImportance); } + if (android.app.Flags.uiRichOngoing() && r.canHavePromotedNotifs) { + pw.print(" promoted="); + pw.print(r.canHavePromotedNotifs); + } pw.println(); for (NotificationChannel channel : r.channels.values()) { pw.print(prefix); @@ -3028,6 +3065,9 @@ public class PreferencesHelper implements RankingConfig { boolean migrateToPm = false; long creationTime; + @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING) + boolean canHavePromotedNotifs = false; + @UserIdInt int userId; Delegate delegate = null; diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 0eb4cbda72bf..626c3ddd49d9 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -1722,7 +1722,7 @@ public class ZenModeHelper { // booleans to determine whether to reset the rules to the default rules boolean allRulesDisabled = true; boolean hasDefaultRules = config.automaticRules.containsAll( - ZenModeConfig.DEFAULT_RULE_IDS); + ZenModeConfig.getDefaultRuleIds()); long time = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis(); if (config.automaticRules != null && config.automaticRules.size() > 0) { @@ -1799,6 +1799,14 @@ public class ZenModeHelper { config.deletedRules.clear(); } + if (Flags.modesUi() && config.automaticRules != null) { + ZenRule obsoleteEventsRule = config.automaticRules.get( + ZenModeConfig.EVENTS_OBSOLETE_RULE_ID); + if (obsoleteEventsRule != null && !obsoleteEventsRule.enabled) { + config.automaticRules.remove(ZenModeConfig.EVENTS_OBSOLETE_RULE_ID); + } + } + if (DEBUG) Log.d(TAG, reason); synchronized (mConfigLock) { setConfigLocked(config, null, @@ -2257,7 +2265,7 @@ public class ZenModeHelper { private static void updateRuleStringsForCurrentLocale(Context context, ZenModeConfig defaultConfig) { for (ZenRule rule : defaultConfig.automaticRules.values()) { - if (ZenModeConfig.EVENTS_DEFAULT_RULE_ID.equals(rule.id)) { + if (ZenModeConfig.EVENTS_OBSOLETE_RULE_ID.equals(rule.id)) { rule.name = context.getResources() .getString(R.string.zen_mode_default_events_name); } else if (ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID.equals(rule.id)) { @@ -2279,7 +2287,7 @@ public class ZenModeHelper { } ZenPolicy defaultPolicy = defaultConfig.getZenPolicy(); for (ZenRule rule : defaultConfig.automaticRules.values()) { - if (ZenModeConfig.DEFAULT_RULE_IDS.contains(rule.id) && rule.zenPolicy == null) { + if (ZenModeConfig.getDefaultRuleIds().contains(rule.id) && rule.zenPolicy == null) { rule.zenPolicy = defaultPolicy.copy(); } } @@ -2483,7 +2491,7 @@ public class ZenModeHelper { List<StatsEvent> events) { // Make the ID safe. String id = rule.id == null ? "" : rule.id; - if (!ZenModeConfig.DEFAULT_RULE_IDS.contains(id)) { + if (!ZenModeConfig.getDefaultRuleIds().contains(id)) { id = ""; } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 3e70d92dd49d..8bab9de903ba 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1101,8 +1101,6 @@ public class UserManagerService extends IUserManager.Stub { if (android.multiuser.Flags.cachesNotInvalidatedAtStartReadOnly()) { UserManager.invalidateIsUserUnlockedCache(); UserManager.invalidateQuietModeEnabledCache(); - UserManager.invalidateStaticUserProperties(); - UserManager.invalidateUserPropertiesCache(); UserManager.invalidateUserSerialNumberCache(); } } diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java index e3d71e4998be..f78c4488cbfb 100644 --- a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java +++ b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java @@ -81,7 +81,7 @@ import java.util.function.Consumer; public final class RollbackPackageHealthObserver implements PackageHealthObserver { private static final String TAG = "RollbackPackageHealthObserver"; private static final String NAME = "rollback-observer"; - private static final String ACTION_NAME = RollbackPackageHealthObserver.class.getName(); + private static final String CLASS_NAME = RollbackPackageHealthObserver.class.getName(); private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT | ApplicationInfo.FLAG_SYSTEM; @@ -610,14 +610,16 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve } }; + String intentActionName = CLASS_NAME + rollback.getRollbackId(); // Register the BroadcastReceiver mContext.registerReceiver(rollbackReceiver, - new IntentFilter(ACTION_NAME), + new IntentFilter(intentActionName), Context.RECEIVER_NOT_EXPORTED); - Intent intentReceiver = new Intent(ACTION_NAME); + Intent intentReceiver = new Intent(intentActionName); intentReceiver.putExtra("rollbackId", rollback.getRollbackId()); intentReceiver.setPackage(mContext.getPackageName()); + intentReceiver.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); PendingIntent rollbackPendingIntent = PendingIntent.getBroadcast(mContext, rollback.getRollbackId(), diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index ccc9b17ff840..12d733fc8c1a 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2641,9 +2641,15 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return true; } // Only do transfer after transaction has done when starting window exist. - if (mStartingData != null && mStartingData.mWaitForSyncTransactionCommit) { - mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_COPY_TO_CLIENT; - return true; + if (mStartingData != null) { + final boolean isWaitingForSyncTransactionCommit = + Flags.removeStartingWindowWaitForMultiTransitions() + ? getSyncTransactionCommitCallbackDepth() > 0 + : mStartingData.mWaitForSyncTransactionCommit; + if (isWaitingForSyncTransactionCommit) { + mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_COPY_TO_CLIENT; + return true; + } } requestCopySplashScreen(); return isTransferringSplashScreen(); @@ -2847,7 +2853,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final boolean animate; final boolean hasImeSurface; if (mStartingData != null) { - if (mStartingData.mWaitForSyncTransactionCommit + final boolean isWaitingForSyncTransactionCommit = + Flags.removeStartingWindowWaitForMultiTransitions() + ? getSyncTransactionCommitCallbackDepth() > 0 + : mStartingData.mWaitForSyncTransactionCommit; + if (isWaitingForSyncTransactionCommit || mSyncState != SYNC_STATE_NONE) { mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_REMOVE_DIRECTLY; mStartingData.mPrepareRemoveAnimation = prepareAnimation; diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index f0a4763796e3..57b879277326 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -75,7 +75,8 @@ class InsetsSourceProvider { private final Rect mTmpRect = new Rect(); private final InsetsSourceControl mFakeControl; - private final Consumer<Transaction> mSetLeashPositionConsumer; + private final Point mPosition = new Point(); + private final Consumer<Transaction> mSetControlPositionConsumer; private @Nullable InsetsControlTarget mPendingControlTarget; private @Nullable InsetsControlTarget mFakeControlTarget; @@ -126,13 +127,14 @@ class InsetsSourceProvider { source.getId(), source.getType(), null /* leash */, false /* initialVisible */, new Point(), Insets.NONE); mControllable = (InsetsPolicy.CONTROLLABLE_TYPES & source.getType()) != 0; - mSetLeashPositionConsumer = t -> { - if (mControl != null) { - final SurfaceControl leash = mControl.getLeash(); - if (leash != null) { - final Point position = mControl.getSurfacePosition(); - t.setPosition(leash, position.x, position.y); - } + mSetControlPositionConsumer = t -> { + if (mControl == null || mControlTarget == null) { + return; + } + boolean changed = mControl.setSurfacePosition(mPosition.x, mPosition.y); + final SurfaceControl leash = mControl.getLeash(); + if (changed && leash != null) { + t.setPosition(leash, mPosition.x, mPosition.y); } if (mHasPendingPosition) { mHasPendingPosition = false; @@ -140,9 +142,22 @@ class InsetsSourceProvider { mStateController.notifyControlTargetChanged(mPendingControlTarget, this); } } + changed |= updateInsetsHint(); + if (changed) { + mStateController.notifyControlChanged(mControlTarget, this); + } }; } + private boolean updateInsetsHint() { + final Insets insetsHint = getInsetsHint(); + if (!mControl.getInsetsHint().equals(insetsHint)) { + mControl.setInsetsHint(insetsHint); + return true; + } + return false; + } + InsetsSource getSource() { return mSource; } @@ -363,26 +378,32 @@ class InsetsSourceProvider { } final boolean serverVisibleChanged = mServerVisible != isServerVisible; setServerVisible(isServerVisible); - updateInsetsControlPosition(windowState, serverVisibleChanged); - } - - void updateInsetsControlPosition(WindowState windowState) { - updateInsetsControlPosition(windowState, false); + final boolean positionChanged = updateInsetsControlPosition(windowState); + if (mControl != null && !positionChanged + // The insets hint would be updated if the position is changed. Here updates it for + // the possible change of the bounds or the server visibility. + && (updateInsetsHint() + || serverVisibleChanged + && android.view.inputmethod.Flags.refactorInsetsController())) { + // Only call notifyControlChanged here when the position is not changed. Otherwise, it + // is called or is scheduled to be called during updateInsetsControlPosition. + mStateController.notifyControlChanged(mControlTarget, this); + } } - private void updateInsetsControlPosition(WindowState windowState, - boolean serverVisibleChanged) { + /** + * @return {#code true} if the surface position of the control is changed. + */ + boolean updateInsetsControlPosition(WindowState windowState) { if (mControl == null) { - return; + return false; } - boolean changed = false; final Point position = getWindowFrameSurfacePosition(); - if (mControl.setSurfacePosition(position.x, position.y) && mControlTarget != null) { - changed = true; + if (!mPosition.equals(position)) { + mPosition.set(position.x, position.y); if (windowState != null && windowState.getWindowFrames().didFrameSizeChange() && windowState.mWinAnimator.getShown() && mWindowContainer.okToDisplay()) { - mHasPendingPosition = true; - windowState.applyWithNextDraw(mSetLeashPositionConsumer); + windowState.applyWithNextDraw(mSetControlPositionConsumer); } else { Transaction t = mWindowContainer.getSyncTransaction(); if (windowState != null) { @@ -399,20 +420,11 @@ class InsetsSourceProvider { } } } - mSetLeashPositionConsumer.accept(t); + mSetControlPositionConsumer.accept(t); } + return true; } - final Insets insetsHint = getInsetsHint(); - if (!mControl.getInsetsHint().equals(insetsHint)) { - mControl.setInsetsHint(insetsHint); - changed = true; - } - if (android.view.inputmethod.Flags.refactorInsetsController() && serverVisibleChanged) { - changed = true; - } - if (changed) { - mStateController.notifyControlChanged(mControlTarget, this); - } + return false; } private Point getWindowFrameSurfacePosition() { diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 5550f3efaa3a..d295378a9b65 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -699,8 +699,10 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { final WindowState win = mService.windowForClientLocked(this, window, false /* throwOnError */); if (win != null) { - ImeTracker.forLogging().onProgress(imeStatsToken, - ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); + if (android.view.inputmethod.Flags.refactorInsetsController()) { + ImeTracker.forLogging().onProgress(imeStatsToken, + ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); + } win.setRequestedVisibleTypes(requestedVisibleTypes); win.getDisplayContent().getInsetsPolicy().onRequestedVisibleTypesChanged(win, imeStatsToken); diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java index 24fb20731c43..896612d3d27a 100644 --- a/services/core/java/com/android/server/wm/StartingData.java +++ b/services/core/java/com/android/server/wm/StartingData.java @@ -68,7 +68,9 @@ public abstract class StartingData { * window. * Note this isn't equal to transition playing, the period should be * Sync finishNow -> Start transaction apply. + * @deprecated TODO(b/362347290): cleanup after fix ramp up */ + @Deprecated boolean mWaitForSyncTransactionCommit; /** diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index a2fda0afb9c6..86bb75ab3f8c 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -35,6 +35,8 @@ import static android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS; import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME; import static android.content.pm.ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY; import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS; +import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP; +import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP; import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY; import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PORTRAIT_ONLY; import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PRESERVE_ORIENTATION; @@ -49,6 +51,7 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.SurfaceControl.METADATA_TASK_ID; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED; @@ -133,6 +136,7 @@ import android.app.IActivityController; import android.app.PictureInPictureParams; import android.app.TaskInfo; import android.app.WindowConfiguration; +import android.app.compat.CompatChanges; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -503,6 +507,12 @@ class Task extends TaskFragment { int mOffsetXForInsets; int mOffsetYForInsets; + /** + * Whether the compatibility overrides that change the resizability of the app should be allowed + * for the specific app. + */ + boolean mAllowForceResizeOverride = true; + private final AnimatingActivityRegistry mAnimatingActivityRegistry = new AnimatingActivityRegistry(); @@ -666,6 +676,7 @@ class Task extends TaskFragment { intent = _intent; mMinWidth = minWidth; mMinHeight = minHeight; + updateAllowForceResizeOverride(); } mAtmService.getTaskChangeNotificationController().notifyTaskCreated(_taskId, realActivity); mHandler = new ActivityTaskHandler(mTaskSupervisor.mLooper); @@ -1028,6 +1039,7 @@ class Task extends TaskFragment { mTaskSupervisor.mRecentTasks.remove(this); mTaskSupervisor.mRecentTasks.add(this); } + updateAllowForceResizeOverride(); } /** Sets the original minimal width and height. */ @@ -1823,6 +1835,17 @@ class Task extends TaskFragment { -1 /* don't check PID */, -1 /* don't check UID */, this); } + private void updateAllowForceResizeOverride() { + try { + mAllowForceResizeOverride = mAtmService.mContext.getPackageManager().getProperty( + PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES, + getBasePackageName()).getBoolean(); + } catch (PackageManager.NameNotFoundException e) { + // Package not found or property not defined, reset to default value. + mAllowForceResizeOverride = true; + } + } + /** * Check that a given bounds matches the application requested orientation. * @@ -2812,7 +2835,18 @@ class Task extends TaskFragment { boolean isResizeable(boolean checkPictureInPictureSupport) { final boolean forceResizable = mAtmService.mForceResizableActivities && getActivityType() == ACTIVITY_TYPE_STANDARD; - return forceResizable || ActivityInfo.isResizeableMode(mResizeMode) + if (forceResizable) return true; + + final UserHandle userHandle = UserHandle.getUserHandleForUid(mUserId); + final boolean forceResizableOverride = mAllowForceResizeOverride + && CompatChanges.isChangeEnabled( + FORCE_RESIZE_APP, getBasePackageName(), userHandle); + final boolean forceNonResizableOverride = mAllowForceResizeOverride + && CompatChanges.isChangeEnabled( + FORCE_NON_RESIZE_APP, getBasePackageName(), userHandle); + + if (forceNonResizableOverride) return false; + return forceResizableOverride || ActivityInfo.isResizeableMode(mResizeMode) || (mSupportsPictureInPicture && checkPictureInPictureSupport); } diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 92953e5a5041..83e714d82dd2 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -429,7 +429,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } final IBinder activityToken; - if (activity.getPid() == mOrganizerPid) { + if (activity.getPid() == mOrganizerPid && activity.getUid() == mOrganizerUid) { // We only pass the actual token if the activity belongs to the organizer process. activityToken = activity.token; } else { @@ -458,7 +458,8 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr change.setTaskFragmentToken(lastParentTfToken); } // Only pass the activity token to the client if it belongs to the same process. - if (nextFillTaskActivity != null && nextFillTaskActivity.getPid() == mOrganizerPid) { + if (nextFillTaskActivity != null && nextFillTaskActivity.getPid() == mOrganizerPid + && nextFillTaskActivity.getUid() == mOrganizerUid) { change.setOtherActivityToken(nextFillTaskActivity.token); } return change; @@ -553,6 +554,10 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr "Replacing existing organizer currently unsupported"); } + if (pid <= 0) { + throw new IllegalStateException("Cannot register from invalid pid: " + pid); + } + if (restoreFromCachedStateIfPossible(organizer, pid, uid, outSavedState)) { return; } diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 0a9cb1c38dab..1c03ba571923 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -4351,4 +4351,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< t.merge(mSyncTransaction); } + int getSyncTransactionCommitCallbackDepth() { + return mSyncTransactionCommitCallbackDepth; + } } diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt index c05c3819ca28..bc64e158e830 100644 --- a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt @@ -36,7 +36,6 @@ import androidx.test.platform.app.InstrumentationRegistry import com.android.internal.infra.AndroidFuture import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults import com.google.common.truth.Truth.assertThat -import com.google.common.util.concurrent.MoreExecutors import java.util.concurrent.atomic.AtomicBoolean import org.junit.Test import org.junit.runner.RunWith @@ -46,7 +45,6 @@ import org.junit.runners.JUnit4 class MetadataSyncAdapterTest { private val context = InstrumentationRegistry.getInstrumentation().targetContext private val appSearchManager = context.getSystemService(AppSearchManager::class.java) - private val testExecutor = MoreExecutors.directExecutor() private val packageManager = context.packageManager @Test @@ -138,8 +136,7 @@ class MetadataSyncAdapterTest { PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build() runtimeSearchSession.put(putDocumentsRequest).get() staticSearchSession.put(putDocumentsRequest).get() - val metadataSyncAdapter = - MetadataSyncAdapter(testExecutor, packageManager, appSearchManager) + val metadataSyncAdapter = MetadataSyncAdapter(packageManager, appSearchManager) val submitSyncRequest = metadataSyncAdapter.trySyncAppFunctionMetadataBlocking( @@ -180,8 +177,7 @@ class MetadataSyncAdapterTest { val putDocumentsRequest: PutDocumentsRequest = PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build() staticSearchSession.put(putDocumentsRequest).get() - val metadataSyncAdapter = - MetadataSyncAdapter(testExecutor, packageManager, appSearchManager) + val metadataSyncAdapter = MetadataSyncAdapter(packageManager, appSearchManager) val submitSyncRequest = metadataSyncAdapter.trySyncAppFunctionMetadataBlocking( @@ -236,8 +232,7 @@ class MetadataSyncAdapterTest { val putDocumentsRequest: PutDocumentsRequest = PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build() runtimeSearchSession.put(putDocumentsRequest).get() - val metadataSyncAdapter = - MetadataSyncAdapter(testExecutor, packageManager, appSearchManager) + val metadataSyncAdapter = MetadataSyncAdapter(packageManager, appSearchManager) val submitSyncRequest = metadataSyncAdapter.trySyncAppFunctionMetadataBlocking( diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java index 3dd2f24aa4e4..1cad255b85d7 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java @@ -509,8 +509,12 @@ public class ApplicationStartInfoTest { mAppStartInfoTracker.handleProcessBroadcastStart(3, app, buildIntent(COMPONENT), false /* isAlarm */); + // Add a brief delay between timestamps to make sure the clock, which is in milliseconds has + // actually incremented. + sleep(1); mAppStartInfoTracker.handleProcessBroadcastStart(2, app, buildIntent(COMPONENT), false /* isAlarm */); + sleep(1); mAppStartInfoTracker.handleProcessBroadcastStart(1, app, buildIntent(COMPONENT), false /* isAlarm */); @@ -557,9 +561,10 @@ public class ApplicationStartInfoTest { // Now load from disk. mAppStartInfoTracker.loadExistingProcessStartInfo(); - // Confirm clock has been set and that its current time is greater than the previous one. + // Confirm clock has been set and that its current time is greater than or equal to the + // previous one, thereby ensuring it was loaded from disk. assertNotNull(mAppStartInfoTracker.mMonotonicClock); - assertTrue(mAppStartInfoTracker.mMonotonicClock.monotonicTime() > originalMonotonicTime); + assertTrue(mAppStartInfoTracker.mMonotonicClock.monotonicTime() >= originalMonotonicTime); } private static <T> void setFieldValue(Class clazz, Object obj, String fieldName, T val) { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index b8f9767b5512..130690d80b70 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -38,6 +38,7 @@ import static android.app.Notification.FLAG_NO_CLEAR; import static android.app.Notification.FLAG_NO_DISMISS; import static android.app.Notification.FLAG_ONGOING_EVENT; import static android.app.Notification.FLAG_ONLY_ALERT_ONCE; +import static android.app.Notification.FLAG_PROMOTED_ONGOING; import static android.app.Notification.FLAG_USER_INITIATED_JOB; import static android.app.Notification.GROUP_ALERT_CHILDREN; import static android.app.Notification.VISIBILITY_PRIVATE; @@ -53,6 +54,7 @@ import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_MAX; +import static android.app.NotificationManager.IMPORTANCE_MIN; import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS; @@ -468,6 +470,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationChannel mSilentChannel = new NotificationChannel("low", "low", IMPORTANCE_LOW); + NotificationChannel mMinChannel = new NotificationChannel("min", "min", IMPORTANCE_MIN); + private static final int NOTIFICATION_LOCATION_UNKNOWN = 0; private static final String VALID_CONVO_SHORTCUT_ID = "shortcut"; @@ -558,8 +562,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return FlagsParameterization.allCombinationsOf( - FLAG_ALL_NOTIFS_NEED_TTL); + return FlagsParameterization.allCombinationsOf(); } public NotificationManagerServiceTest(FlagsParameterization flags) { @@ -856,15 +859,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mInternalService = mService.getInternalService(); mBinderService.createNotificationChannels(mPkg, new ParceledListSlice( - Arrays.asList(mTestNotificationChannel, mSilentChannel))); + Arrays.asList(mTestNotificationChannel, mSilentChannel, mMinChannel))); mBinderService.createNotificationChannels(PKG_P, new ParceledListSlice( - Arrays.asList(mTestNotificationChannel, mSilentChannel))); + Arrays.asList(mTestNotificationChannel, mSilentChannel, mMinChannel))); mBinderService.createNotificationChannels(PKG_O, new ParceledListSlice( - Arrays.asList(mTestNotificationChannel, mSilentChannel))); + Arrays.asList(mTestNotificationChannel, mSilentChannel, mMinChannel))); assertNotNull(mBinderService.getNotificationChannel( mPkg, mContext.getUserId(), mPkg, TEST_CHANNEL_ID)); assertNotNull(mBinderService.getNotificationChannel( mPkg, mContext.getUserId(), mPkg, mSilentChannel.getId())); + assertNotNull(mBinderService.getNotificationChannel( + mPkg, mContext.getUserId(), mPkg, mMinChannel.getId())); clearInvocations(mRankingHandler); when(mPermissionHelper.hasPermission(mUid)).thenReturn(true); @@ -943,6 +948,16 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } } + private ShortcutInfo createMockConvoShortcut() { + ShortcutInfo info = mock(ShortcutInfo.class); + when(info.getPackage()).thenReturn(mPkg); + when(info.getId()).thenReturn(VALID_CONVO_SHORTCUT_ID); + when(info.getUserId()).thenReturn(USER_SYSTEM); + when(info.isLongLived()).thenReturn(true); + when(info.isEnabled()).thenReturn(true); + return info; + } + private void simulatePackageSuspendBroadcast(boolean suspend, String pkg, int uid) { // mimics receive broadcast that package is (un)suspended @@ -16540,13 +16555,298 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID); } - private ShortcutInfo createMockConvoShortcut() { - ShortcutInfo info = mock(ShortcutInfo.class); - when(info.getPackage()).thenReturn(mPkg); - when(info.getId()).thenReturn(VALID_CONVO_SHORTCUT_ID); - when(info.getUserId()).thenReturn(USER_SYSTEM); - when(info.isLongLived()).thenReturn(true); - when(info.isEnabled()).thenReturn(true); - return info; + @Test + @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING) + public void testSetCanBePromoted_granted() throws Exception { + mContext.getTestablePermissions().setPermission( + android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); + // qualifying posted notification + Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .setColorized(true) + .setFlag(FLAG_CAN_COLORIZE, true) // add manually since we're skipping post + .build(); + + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 9, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + + // qualifying enqueued notification + Notification n1 = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .setColorized(true) + .setFlag(FLAG_CAN_COLORIZE, true) // add manually since we're skipping post + .build(); + StatusBarNotification sbn1 = new StatusBarNotification(mPkg, mPkg, 7, null, mUid, 0, + n1, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r1 = new NotificationRecord(mContext, sbn1, mTestNotificationChannel); + + // another package but otherwise would qualify + Notification n2 = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .setColorized(true) + .setFlag(FLAG_CAN_COLORIZE, true) // add manually since we're skipping post + .build(); + StatusBarNotification sbn2 = new StatusBarNotification(PKG_O, PKG_O, 7, null, UID_O, 0, + n2, UserHandle.getUserHandleForUid(UID_O), null, 0); + NotificationRecord r2 = new NotificationRecord(mContext, sbn2, mTestNotificationChannel); + + // not-qualifying posted notification + Notification n3 = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .build(); + + StatusBarNotification sbn3 = new StatusBarNotification(mPkg, mPkg, 8, null, mUid, 0, + n3, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r3 = new NotificationRecord(mContext, sbn3, mTestNotificationChannel); + + mService.addNotification(r3); + mService.addNotification(r2); + mService.addNotification(r); + mService.addEnqueuedNotification(r1); + + mBinderService.setCanBePromoted(mPkg, mUid, true); + + waitForIdle(); + + ArgumentCaptor<NotificationRecord> captor = + ArgumentCaptor.forClass(NotificationRecord.class); + verify(mListeners, times(1)).prepareNotifyPostedLocked( + captor.capture(), any(), anyBoolean()); + + // the posted one + assertThat(mService.hasFlag(captor.getValue().getNotification().flags, + FLAG_PROMOTED_ONGOING)).isTrue(); + // the enqueued one + assertThat(mService.hasFlag(r1.getNotification().flags, FLAG_PROMOTED_ONGOING)).isTrue(); + // the other app + assertThat(mService.hasFlag(r2.getNotification().flags, FLAG_PROMOTED_ONGOING)).isFalse(); + // same app, not qualifying + assertThat(mService.hasFlag(r3.getNotification().flags, FLAG_PROMOTED_ONGOING)).isFalse(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING) + public void testSetCanBePromoted_granted_onlyNotifiesOnce() throws Exception { + mContext.getTestablePermissions().setPermission( + android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); + // qualifying posted notification + Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .setColorized(true) + .setFlag(FLAG_CAN_COLORIZE, true) // add manually since we're skipping post + .build(); + + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 9, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + + mService.addNotification(r); + + mBinderService.setCanBePromoted(mPkg, mUid, true); + waitForIdle(); + mBinderService.setCanBePromoted(mPkg, mUid, true); + waitForIdle(); + + ArgumentCaptor<NotificationRecord> captor = + ArgumentCaptor.forClass(NotificationRecord.class); + verify(mListeners, times(1)).prepareNotifyPostedLocked( + captor.capture(), any(), anyBoolean()); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING) + public void testSetCanBePromoted_revoked() throws Exception { + mContext.getTestablePermissions().setPermission( + android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); + // start from true state + mBinderService.setCanBePromoted(mPkg, mUid, true); + + // qualifying posted notification + Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .setColorized(true) + .setFlag(FLAG_PROMOTED_ONGOING, true) // add manually since we're skipping post + .setFlag(FLAG_CAN_COLORIZE, true) // add manually since we're skipping post + .build(); + + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 9, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + + // qualifying enqueued notification + Notification n1 = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .setColorized(true) + .setFlag(FLAG_PROMOTED_ONGOING, true) // add manually since we're skipping post + .setFlag(FLAG_CAN_COLORIZE, true) // add manually since we're skipping post + .build(); + StatusBarNotification sbn1 = new StatusBarNotification(mPkg, mPkg, 7, null, mUid, 0, + n1, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r1 = new NotificationRecord(mContext, sbn1, mTestNotificationChannel); + + // doesn't qualify, same package + Notification n2 = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .build(); + StatusBarNotification sbn2 = new StatusBarNotification(mPkg, mPkg, 8, null, mUid, 0, + n2, UserHandle.getUserHandleForUid(UID_O), null, 0); + NotificationRecord r2 = new NotificationRecord(mContext, sbn2, mTestNotificationChannel); + + mService.addNotification(r2); + mService.addNotification(r); + mService.addEnqueuedNotification(r1); + + mBinderService.setCanBePromoted(mPkg, mUid, false); + + waitForIdle(); + + ArgumentCaptor<NotificationRecord> captor = + ArgumentCaptor.forClass(NotificationRecord.class); + verify(mListeners, times(1)).prepareNotifyPostedLocked( + captor.capture(), any(), anyBoolean()); + + // the posted one + assertThat(mService.hasFlag(captor.getValue().getNotification().flags, + FLAG_PROMOTED_ONGOING)).isFalse(); + // the enqueued one + assertThat(mService.hasFlag(r1.getNotification().flags, FLAG_PROMOTED_ONGOING)).isFalse(); + // the not qualifying one + assertThat(mService.hasFlag(r2.getNotification().flags, FLAG_PROMOTED_ONGOING)).isFalse(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING) + public void testSetCanBePromoted_revoked_onlyNotifiesOnce() throws Exception { + mContext.getTestablePermissions().setPermission( + android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); + // start from true state + mBinderService.setCanBePromoted(mPkg, mUid, true); + + // qualifying posted notification + Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .setColorized(true) + .setFlag(FLAG_PROMOTED_ONGOING, true) // add manually since we're skipping post + .setFlag(FLAG_CAN_COLORIZE, true) // add manually since we're skipping post + .build(); + + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 9, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + + mService.addNotification(r); + + mBinderService.setCanBePromoted(mPkg, mUid, false); + waitForIdle(); + mBinderService.setCanBePromoted(mPkg, mUid, false); + waitForIdle(); + + ArgumentCaptor<NotificationRecord> captor = + ArgumentCaptor.forClass(NotificationRecord.class); + verify(mListeners, times(1)).prepareNotifyPostedLocked( + captor.capture(), any(), anyBoolean()); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING) + public void testPostPromotableNotification() throws Exception { + mBinderService.setCanBePromoted(mPkg, mUid, true); + assertThat(mBinderService.canBePromoted(mPkg, mUid)).isTrue(); + mContext.getTestablePermissions().setPermission( + android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); + + Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .setColorized(true) + .build(); + //assertThat(n.hasPromotableCharacteristics()).isTrue(); + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 9, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + + mBinderService.enqueueNotificationWithTag(mPkg, mPkg, sbn.getTag(), + sbn.getId(), sbn.getNotification(), sbn.getUserId()); + waitForIdle(); + + ArgumentCaptor<NotificationRecord> captor = + ArgumentCaptor.forClass(NotificationRecord.class); + verify(mListeners, times(1)).prepareNotifyPostedLocked( + captor.capture(), any(), anyBoolean()); + + assertThat(mService.hasFlag(captor.getValue().getNotification().flags, + FLAG_PROMOTED_ONGOING)).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING) + public void testPostPromotableNotification_noPermission() throws Exception { + mContext.getTestablePermissions().setPermission( + android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); + Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .setColorized(true) + .build(); + + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 9, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + + mBinderService.enqueueNotificationWithTag(mPkg, mPkg, sbn.getTag(), + sbn.getId(), sbn.getNotification(), sbn.getUserId()); + waitForIdle(); + + ArgumentCaptor<NotificationRecord> captor = + ArgumentCaptor.forClass(NotificationRecord.class); + verify(mListeners, times(1)).prepareNotifyPostedLocked( + captor.capture(), any(), anyBoolean()); + + assertThat(mService.hasFlag(captor.getValue().getNotification().flags, + FLAG_PROMOTED_ONGOING)).isFalse(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING) + public void testPostPromotableNotification_unimportantNotification() throws Exception { + mBinderService.setCanBePromoted(mPkg, mUid, true); + mContext.getTestablePermissions().setPermission( + android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); + Notification n = new Notification.Builder(mContext, mMinChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) + .setColor(Color.WHITE) + .setColorized(true) + .build(); + + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 9, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + + mBinderService.enqueueNotificationWithTag(mPkg, mPkg, sbn.getTag(), + sbn.getId(), sbn.getNotification(), sbn.getUserId()); + waitForIdle(); + + ArgumentCaptor<NotificationRecord> captor = + ArgumentCaptor.forClass(NotificationRecord.class); + verify(mListeners, times(1)).prepareNotifyPostedLocked( + captor.capture(), any(), anyBoolean()); + + assertThat(mService.hasFlag(captor.getValue().getNotification().flags, + FLAG_PROMOTED_ONGOING)).isFalse(); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 1905ae4aec4b..7d63062784f9 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -518,6 +518,17 @@ public class PreferencesHelperTest extends UiServiceTestCase { doneLatch.await(); } + private static NotificationChannel cloneChannel(NotificationChannel original) { + Parcel parcel = Parcel.obtain(); + try { + original.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return NotificationChannel.CREATOR.createFromParcel(parcel); + } finally { + parcel.recycle(); + } + } + @Test public void testWriteXml_onlyBackupsTargetUser() throws Exception { // Setup package notifications. @@ -631,6 +642,9 @@ public class PreferencesHelperTest extends UiServiceTestCase { } mHelper.setShowBadge(PKG_N_MR1, UID_N_MR1, true); + if (android.app.Flags.uiRichOngoing()) { + mHelper.setCanBePromoted(PKG_N_MR1, UID_N_MR1, true); + } ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, false, UserHandle.USER_ALL, channel1.getId(), channel2.getId(), @@ -641,6 +655,9 @@ public class PreferencesHelperTest extends UiServiceTestCase { loadStreamXml(baos, false, UserHandle.USER_ALL); assertTrue(mXmlHelper.canShowBadge(PKG_N_MR1, UID_N_MR1)); + if (android.app.Flags.uiRichOngoing()) { + assertThat(mXmlHelper.canBePromoted(PKG_N_MR1, UID_N_MR1)).isTrue(); + } assertEquals(channel1, mXmlHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId(), false)); compareChannels(channel2, @@ -6293,14 +6310,21 @@ public class PreferencesHelperTest extends UiServiceTestCase { }, 20, 50); } - private static NotificationChannel cloneChannel(NotificationChannel original) { - Parcel parcel = Parcel.obtain(); - try { - original.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - return NotificationChannel.CREATOR.createFromParcel(parcel); - } finally { - parcel.recycle(); - } + @Test + @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING) + public void testNoAppHasPermissionToPromoteByDefault() { + mHelper.setShowBadge(PKG_P, UID_P, true); + assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isFalse(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING) + public void testSetCanBePromoted() { + mHelper.setCanBePromoted(PKG_P, UID_P, true); + assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue(); + + mHelper.setCanBePromoted(PKG_P, UID_P, false); + assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isFalse(); + verify(mHandler, never()).requestSort(); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 12069284e462..d4cba8d726fb 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -18,7 +18,7 @@ package com.android.server.notification; import static android.app.AutomaticZenRule.TYPE_BEDTIME; import static android.app.AutomaticZenRule.TYPE_IMMERSIVE; -import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR; +import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; import static android.app.AutomaticZenRule.TYPE_UNKNOWN; import static android.app.Flags.FLAG_MODES_API; import static android.app.Flags.FLAG_MODES_UI; @@ -221,7 +221,7 @@ import platform.test.runner.parameterized.Parameters; @TestableLooper.RunWithLooper public class ZenModeHelperTest extends UiServiceTestCase { - private static final String EVENTS_DEFAULT_RULE_ID = ZenModeConfig.EVENTS_DEFAULT_RULE_ID; + private static final String EVENTS_DEFAULT_RULE_ID = ZenModeConfig.EVENTS_OBSOLETE_RULE_ID; private static final String SCHEDULE_DEFAULT_RULE_ID = ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID; private static final String CUSTOM_PKG_NAME = "not.android"; @@ -1216,7 +1216,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // list for tracking which ids we've seen in the pulled atom output List<String> ids = new ArrayList<>(); - ids.addAll(ZenModeConfig.DEFAULT_RULE_IDS); + ids.addAll(ZenModeConfig.getDefaultRuleIds()); ids.add(""); // empty string for root config for (StatsEvent ev : events) { @@ -1793,14 +1793,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { // check default rules ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; assertTrue(rules.size() != 0); - for (String defaultId : ZenModeConfig.DEFAULT_RULE_IDS) { + for (String defaultId : ZenModeConfig.getDefaultRuleIds()) { assertTrue(rules.containsKey(defaultId)); } assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy()); } - @Test public void testReadXmlAllDisabledRulesResetDefaultRules() throws Exception { setupZenConfig(); @@ -1830,7 +1829,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // check default rules ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; assertTrue(rules.size() != 0); - for (String defaultId : ZenModeConfig.DEFAULT_RULE_IDS) { + for (String defaultId : ZenModeConfig.getDefaultRuleIds()) { assertTrue(rules.containsKey(defaultId)); } assertFalse(rules.containsKey("customRule")); @@ -1839,6 +1838,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @DisableFlags(FLAG_MODES_UI) // modes_ui has only 1 default rule public void testReadXmlOnlyOneDefaultRuleExists() throws Exception { setupZenConfig(); Policy originalPolicy = mZenModeHelper.getNotificationPolicy(); @@ -1882,11 +1882,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { // check default rules ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; - assertTrue(rules.size() != 0); - for (String defaultId : ZenModeConfig.DEFAULT_RULE_IDS) { - assertTrue(rules.containsKey(defaultId)); + assertThat(rules).isNotEmpty(); + for (String defaultId : ZenModeConfig.getDefaultRuleIds()) { + assertThat(rules).containsKey(defaultId); } - assertFalse(rules.containsKey("customRule")); + assertThat(rules).doesNotContainKey("customRule"); assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy()); } @@ -1932,13 +1932,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { defaultEventRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; defaultEventRule.conditionId = ZenModeConfig.toScheduleConditionId( defaultEventRuleInfo); - defaultEventRule.id = ZenModeConfig.EVENTS_DEFAULT_RULE_ID; + defaultEventRule.id = ZenModeConfig.EVENTS_OBSOLETE_RULE_ID; defaultScheduleRule.zenPolicy = new ZenPolicy.Builder() .allowAlarms(false) .allowMedia(false) .allowRepeatCallers(false) .build(); - automaticRules.put(ZenModeConfig.EVENTS_DEFAULT_RULE_ID, defaultEventRule); + automaticRules.put(ZenModeConfig.EVENTS_OBSOLETE_RULE_ID, defaultEventRule); mZenModeHelper.mConfig.automaticRules = automaticRules; @@ -1951,18 +1951,19 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); // check default rules + int expectedNumAutoRules = 1 + ZenModeConfig.getDefaultRuleIds().size(); // custom + default ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; - assertEquals(3, rules.size()); - for (String defaultId : ZenModeConfig.DEFAULT_RULE_IDS) { - assertTrue(rules.containsKey(defaultId)); + assertThat(rules).hasSize(expectedNumAutoRules); + for (String defaultId : ZenModeConfig.getDefaultRuleIds()) { + assertThat(rules).containsKey(defaultId); } - assertTrue(rules.containsKey("customRule")); + assertThat(rules).containsKey("customRule"); assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy()); List<StatsEvent> events = new LinkedList<>(); mZenModeHelper.pullRules(events); - assertEquals(4, events.size()); + assertThat(events).hasSize(expectedNumAutoRules + 1); // auto + manual } @Test @@ -2151,8 +2152,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { defaultEventRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; defaultEventRule.conditionId = ZenModeConfig.toScheduleConditionId( defaultEventRuleInfo); - defaultEventRule.id = ZenModeConfig.EVENTS_DEFAULT_RULE_ID; - automaticRules.put(ZenModeConfig.EVENTS_DEFAULT_RULE_ID, defaultEventRule); + defaultEventRule.id = ZenModeConfig.EVENTS_OBSOLETE_RULE_ID; + automaticRules.put(ZenModeConfig.EVENTS_OBSOLETE_RULE_ID, defaultEventRule); mZenModeHelper.mConfig.automaticRules = automaticRules; @@ -2167,7 +2168,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // check default rules ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; assertThat(rules.size()).isGreaterThan(0); - for (String defaultId : ZenModeConfig.DEFAULT_RULE_IDS) { + for (String defaultId : ZenModeConfig.getDefaultRuleIds()) { assertThat(rules).containsKey(defaultId); ZenRule rule = rules.get(defaultId); assertThat(rule.zenPolicy).isNotNull(); @@ -2371,7 +2372,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Find default rules; check they have non-null policies; check that they match the default // and not whatever has been set up in setupZenConfig. ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; - for (String defaultId : ZenModeConfig.DEFAULT_RULE_IDS) { + for (String defaultId : ZenModeConfig.getDefaultRuleIds()) { assertThat(rules).containsKey(defaultId); ZenRule rule = rules.get(defaultId); assertThat(rule.zenPolicy).isNotNull(); @@ -6884,7 +6885,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.onUserSwitched(101); ZenRule eventsRule = mZenModeHelper.mConfig.automaticRules.get( - ZenModeConfig.EVENTS_DEFAULT_RULE_ID); + ZenModeConfig.EVENTS_OBSOLETE_RULE_ID); assertThat(eventsRule).isNotNull(); assertThat(eventsRule.zenPolicy).isNull(); @@ -6900,7 +6901,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.onUserSwitched(201); ZenRule eventsRule = mZenModeHelper.mConfig.automaticRules.get( - ZenModeConfig.EVENTS_DEFAULT_RULE_ID); + ZenModeConfig.EVENTS_OBSOLETE_RULE_ID); assertThat(eventsRule).isNotNull(); assertThat(eventsRule.zenPolicy).isEqualTo(mZenModeHelper.getDefaultZenPolicy()); @@ -6915,11 +6916,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.onUserSwitched(301); ZenRule eventsRule = mZenModeHelper.mConfig.automaticRules.get( - ZenModeConfig.EVENTS_DEFAULT_RULE_ID); + ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); assertThat(eventsRule).isNotNull(); assertThat(eventsRule.zenPolicy).isEqualTo(mZenModeHelper.getDefaultZenPolicy()); - assertThat(eventsRule.type).isEqualTo(TYPE_SCHEDULE_CALENDAR); + assertThat(eventsRule.type).isEqualTo(TYPE_SCHEDULE_TIME); assertThat(eventsRule.triggerDescription).isNotEmpty(); } @@ -7008,6 +7009,46 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(zenRule.zenPolicy).isNotSameInstanceAs(mZenModeHelper.getDefaultZenPolicy()); } + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void readXml_withDisabledEventsRule_deletesIt() throws Exception { + ZenRule rule = new ZenRule(); + rule.id = ZenModeConfig.EVENTS_OBSOLETE_RULE_ID; + rule.name = "Events"; + rule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + rule.conditionId = Uri.parse("events"); + + rule.enabled = false; + mZenModeHelper.mConfig.automaticRules.put(ZenModeConfig.EVENTS_OBSOLETE_RULE_ID, rule); + ByteArrayOutputStream xmlBytes = writeXmlAndPurge(ZenModeConfig.XML_VERSION_MODES_UI); + TypedXmlPullParser parser = getParserForByteStream(xmlBytes); + + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + + assertThat(mZenModeHelper.mConfig.automaticRules).doesNotContainKey( + ZenModeConfig.EVENTS_OBSOLETE_RULE_ID); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void readXml_withEnabledEventsRule_keepsIt() throws Exception { + ZenRule rule = new ZenRule(); + rule.id = ZenModeConfig.EVENTS_OBSOLETE_RULE_ID; + rule.name = "Events"; + rule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + rule.conditionId = Uri.parse("events"); + + rule.enabled = true; + mZenModeHelper.mConfig.automaticRules.put(ZenModeConfig.EVENTS_OBSOLETE_RULE_ID, rule); + ByteArrayOutputStream xmlBytes = writeXmlAndPurge(ZenModeConfig.XML_VERSION_MODES_UI); + TypedXmlPullParser parser = getParserForByteStream(xmlBytes); + + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + + assertThat(mZenModeHelper.mConfig.automaticRules).containsKey( + ZenModeConfig.EVENTS_OBSOLETE_RULE_ID); + } + private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode, @Nullable ZenPolicy zenPolicy) { ZenRule rule = new ZenRule(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 7ff2e50926a5..4b03483d43b9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -29,6 +29,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME; import static android.content.pm.ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY; +import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; +import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; @@ -75,6 +77,7 @@ import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.TaskInfo; import android.app.WindowConfiguration; +import android.compat.testing.PlatformCompatChangeRule; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -97,9 +100,13 @@ import androidx.test.filters.MediumTest; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import libcore.junit.util.compat.CoreCompatChangeRule; + import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestRule; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.xmlpull.v1.XmlPullParser; @@ -122,6 +129,9 @@ import java.io.Reader; @RunWith(WindowTestRunner.class) public class TaskTests extends WindowTestsBase { + @Rule + public TestRule compatChangeRule = new PlatformCompatChangeRule(); + private static final String TASK_TAG = "task"; private Rect mParentBounds; @@ -404,6 +414,85 @@ public class TaskTests extends WindowTestsBase { } @Test + @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_RESIZE_APP}) + public void testIsResizeable_nonResizeable_forceResize_overridesEnabled_Resizeable() { + final Task task = new TaskBuilder(mSupervisor) + .setCreateActivity(true) + .setComponent( + ComponentName.createRelative(mContext, SizeCompatTests.class.getName())) + .build(); + task.setResizeMode(RESIZE_MODE_UNRESIZEABLE); + // Override should take effect and task should be resizeable. + assertTrue(task.getTaskInfo().isResizeable); + } + + @Test + @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_RESIZE_APP}) + public void testIsResizeable_nonResizeable_forceResize_overridesDisabled_nonResizeable() { + final Task task = new TaskBuilder(mSupervisor) + .setCreateActivity(true) + .setComponent( + ComponentName.createRelative(mContext, SizeCompatTests.class.getName())) + .build(); + task.setResizeMode(RESIZE_MODE_UNRESIZEABLE); + + // Disallow resize overrides. + task.mAllowForceResizeOverride = false; + + // Override should not take effect and task should be un-resizeable. + assertFalse(task.getTaskInfo().isResizeable); + } + + @Test + @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP}) + public void testIsResizeable_resizeable_forceNonResize_overridesEnabled_nonResizeable() { + final Task task = new TaskBuilder(mSupervisor) + .setCreateActivity(true) + .setComponent( + ComponentName.createRelative(mContext, SizeCompatTests.class.getName())) + .build(); + task.setResizeMode(RESIZE_MODE_RESIZEABLE); + + // Override should take effect and task should be un-resizeable. + assertFalse(task.getTaskInfo().isResizeable); + } + + @Test + @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP}) + public void testIsResizeable_resizeable_forceNonResize_overridesDisabled_Resizeable() { + final Task task = new TaskBuilder(mSupervisor) + .setCreateActivity(true) + .setComponent( + ComponentName.createRelative(mContext, SizeCompatTests.class.getName())) + .build(); + task.setResizeMode(RESIZE_MODE_RESIZEABLE); + + // Disallow resize overrides. + task.mAllowForceResizeOverride = false; + + // Override should not take effect and task should be resizeable. + assertTrue(task.getTaskInfo().isResizeable); + } + + @Test + @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP}) + public void testIsResizeable_systemWideForceResize_compatForceNonResize__Resizeable() { + final Task task = new TaskBuilder(mSupervisor) + .setCreateActivity(true) + .setComponent( + ComponentName.createRelative(mContext, SizeCompatTests.class.getName())) + .build(); + task.setResizeMode(RESIZE_MODE_RESIZEABLE); + + // Set system-wide force resizeable override. + task.mAtmService.mForceResizableActivities = true; + + // System wide override should tak priority over app compat override so the task should + // remain resizeable. + assertTrue(task.getTaskInfo().isResizeable); + } + + @Test public void testResolveNonResizableTaskWindowingMode() { // Test with no support non-resizable in multi window regardless the screen size. mAtm.mSupportsNonResizableMultiWindow = -1; diff --git a/telephony/common/android/telephony/LocationAccessPolicy.java b/telephony/common/android/telephony/LocationAccessPolicy.java index a6781478a765..0f4809c2918d 100644 --- a/telephony/common/android/telephony/LocationAccessPolicy.java +++ b/telephony/common/android/telephony/LocationAccessPolicy.java @@ -33,6 +33,7 @@ import android.util.Log; import android.widget.Toast; import com.android.internal.telephony.TelephonyPermissions; +import com.android.internal.telephony.flags.Flags; import com.android.internal.telephony.util.TelephonyUtils; /** @@ -283,6 +284,8 @@ public final class LocationAccessPolicy { int minSdkVersion = Manifest.permission.ACCESS_FINE_LOCATION.equals(permissionToCheck) ? query.minSdkVersionForFine : query.minSdkVersionForCoarse; + UserHandle callingUserHandle = UserHandle.getUserHandleForUid(query.callingUid); + // If the app fails for some reason, see if it should be allowed to proceed. if (minSdkVersion > MAX_SDK_FOR_ANY_ENFORCEMENT) { String errorMsg = "Allowing " + query.callingPackage + " " + locationTypeForLog @@ -291,7 +294,8 @@ public final class LocationAccessPolicy { + query.method; logError(context, query, errorMsg); return null; - } else if (!isAppAtLeastSdkVersion(context, query.callingPackage, minSdkVersion)) { + } else if (!isAppAtLeastSdkVersion(context, callingUserHandle, query.callingPackage, + minSdkVersion)) { String errorMsg = "Allowing " + query.callingPackage + " " + locationTypeForLog + " because it doesn't target API " + minSdkVersion + " yet." + " Please fix this app. Called from " + query.method; @@ -420,11 +424,19 @@ public final class LocationAccessPolicy { } } - private static boolean isAppAtLeastSdkVersion(Context context, String pkgName, int sdkVersion) { + private static boolean isAppAtLeastSdkVersion(Context context, + @NonNull UserHandle callingUserHandle, String pkgName, int sdkVersion) { try { - if (context.getPackageManager().getApplicationInfo(pkgName, 0).targetSdkVersion - >= sdkVersion) { - return true; + if (Flags.hsumPackageManager()) { + if (context.getPackageManager().getApplicationInfoAsUser( + pkgName, 0, callingUserHandle).targetSdkVersion >= sdkVersion) { + return true; + } + } else { + if (context.getPackageManager().getApplicationInfo(pkgName, 0).targetSdkVersion + >= sdkVersion) { + return true; + } } } catch (PackageManager.NameNotFoundException e) { // In case of exception, assume known app (more strict checking) diff --git a/tests/FlickerTests/ActivityEmbedding/OWNERS b/tests/FlickerTests/ActivityEmbedding/OWNERS new file mode 100644 index 000000000000..981b3168cdbb --- /dev/null +++ b/tests/FlickerTests/ActivityEmbedding/OWNERS @@ -0,0 +1 @@ +# Bug component: 1168918 diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java index 945907009a9c..b5258dfc9c3c 100644 --- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java +++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java @@ -57,6 +57,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.function.Consumer; + /** * Build/Install/Run: * atest TouchpadDebugViewTest @@ -99,10 +101,12 @@ public class TouchpadDebugViewTest { when(mInputManager.getInputDevice(TOUCHPAD_DEVICE_ID)).thenReturn(inputDevice); + Consumer<Integer> touchpadSwitchHandler = id -> {}; + mTouchpadDebugView = new TouchpadDebugView(mTestableContext, TOUCHPAD_DEVICE_ID, new TouchpadHardwareProperties.Builder(0f, 0f, 500f, 500f, 45f, 47f, -4f, 5f, (short) 10, true, - true).build()); + true).build(), touchpadSwitchHandler); mTouchpadDebugView.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), @@ -321,26 +325,30 @@ public class TouchpadDebugViewTest { new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0, new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID); - assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.rgb(118, 151, 99)); + assertEquals(((ColorDrawable) child.getBackground()).getColor(), + Color.parseColor("#769763")); mTouchpadDebugView.updateHardwareState( new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0, new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID); - assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.rgb(84, 85, 169)); + assertEquals(((ColorDrawable) child.getBackground()).getColor(), + Color.parseColor("#5455A9")); mTouchpadDebugView.updateHardwareState( new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0, new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID); - assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.rgb(118, 151, 99)); + assertEquals(((ColorDrawable) child.getBackground()).getColor(), + Color.parseColor("#769763")); // Color should not change because hardware state of a different touchpad mTouchpadDebugView.updateHardwareState( new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0, new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID + 1); - assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.rgb(118, 151, 99)); + assertEquals(((ColorDrawable) child.getBackground()).getColor(), + Color.parseColor("#769763")); } @Test diff --git a/tools/systemfeatures/Android.bp b/tools/systemfeatures/Android.bp index a9e63289ee93..590f7190881a 100644 --- a/tools/systemfeatures/Android.bp +++ b/tools/systemfeatures/Android.bp @@ -30,8 +30,8 @@ genrule { name: "systemfeatures-gen-tests-srcs", cmd: "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwNoFeatures --readonly=false > $(location RwNoFeatures.java) && " + "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoNoFeatures --readonly=true --feature-apis=WATCH > $(location RoNoFeatures.java) && " + - "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwFeatures --readonly=false --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RwFeatures.java) && " + - "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoFeatures --readonly=true --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: --feature-apis=WATCH,PC > $(location RoFeatures.java)", + "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwFeatures --readonly=false --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:UNAVAILABLE --feature=AUTO: > $(location RwFeatures.java) && " + + "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoFeatures --readonly=true --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:UNAVAILABLE --feature=AUTO: --feature-apis=WATCH,PC > $(location RoFeatures.java)", out: [ "RwNoFeatures.java", "RoNoFeatures.java", diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt index 5df453deaf2a..cba521e639cb 100644 --- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt +++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt @@ -20,7 +20,10 @@ import com.google.common.base.CaseFormat import com.squareup.javapoet.ClassName import com.squareup.javapoet.JavaFile import com.squareup.javapoet.MethodSpec +import com.squareup.javapoet.ParameterizedTypeName import com.squareup.javapoet.TypeSpec +import java.util.HashMap +import java.util.Map import javax.lang.model.element.Modifier /* @@ -31,7 +34,7 @@ import javax.lang.model.element.Modifier * * <pre> * <cmd> com.foo.RoSystemFeatures --readonly=true \ - * --feature=WATCH:0 --feature=AUTOMOTIVE: --feature=VULKAN:9348 + * --feature=WATCH:0 --feature=AUTOMOTIVE: --feature=VULKAN:9348 --feature=PC:UNAVAILABLE * --feature-apis=WATCH,PC,LEANBACK * </pre> * @@ -43,12 +46,13 @@ import javax.lang.model.element.Modifier * @AssumeTrueForR8 * public static boolean hasFeatureWatch(Context context); * @AssumeFalseForR8 - * public static boolean hasFeatureAutomotive(Context context); + * public static boolean hasFeaturePc(Context context); * @AssumeTrueForR8 * public static boolean hasFeatureVulkan(Context context); - * public static boolean hasFeaturePc(Context context); + * public static boolean hasFeatureAutomotive(Context context); * public static boolean hasFeatureLeanback(Context context); * public static Boolean maybeHasFeature(String feature, int version); + * public static ArrayMap<String, FeatureInfo> getCompileTimeAvailableFeatures(); * } * </pre> */ @@ -58,6 +62,7 @@ object SystemFeaturesGenerator { private const val READONLY_ARG = "--readonly=" private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager") private val CONTEXT_CLASS = ClassName.get("android.content", "Context") + private val FEATUREINFO_CLASS = ClassName.get("android.content.pm", "FeatureInfo") private val ASSUME_TRUE_CLASS = ClassName.get("com.android.aconfig.annotations", "AssumeTrueForR8") private val ASSUME_FALSE_CLASS = @@ -67,7 +72,10 @@ object SystemFeaturesGenerator { println("Usage: SystemFeaturesGenerator <outputClassName> [options]") println(" Options:") println(" --readonly=true|false Whether to encode features as build-time constants") - println(" --feature=\$NAME:\$VER A feature+version pair (blank version == disabled)") + println(" --feature=\$NAME:\$VER A feature+version pair, where \$VER can be:") + println(" * blank/empty == undefined (variable API)") + println(" * valid int == enabled (constant API)") + println(" * UNAVAILABLE == disabled (constant API)") println(" This will always generate associated query APIs,") println(" adding to or replacing those from `--feature-apis=`.") println(" --feature-apis=\$NAME_1,\$NAME_2") @@ -89,7 +97,7 @@ object SystemFeaturesGenerator { var readonly = false var outputClassName: ClassName? = null - val featureArgs = mutableListOf<FeatureArg>() + val featureArgs = mutableListOf<FeatureInfo>() // We could just as easily hardcode this list, as the static API surface should change // somewhat infrequently, but this decouples the codegen from the framework completely. val featureApiArgs = mutableSetOf<String>() @@ -122,7 +130,7 @@ object SystemFeaturesGenerator { featureArgs.associateByTo( features, { it.name }, - { FeatureInfo(it.name, it.version, readonly) }, + { FeatureInfo(it.name, it.version, it.readonly && readonly) }, ) outputClassName @@ -139,6 +147,7 @@ object SystemFeaturesGenerator { addFeatureMethodsToClass(classBuilder, features.values) addMaybeFeatureMethodToClass(classBuilder, features.values) + addGetFeaturesMethodToClass(classBuilder, features.values) // TODO(b/203143243): Add validation of build vs runtime values to ensure consistency. JavaFile.builder(outputClassName.packageName(), classBuilder.build()) @@ -154,13 +163,17 @@ object SystemFeaturesGenerator { * Parses a feature argument of the form "--feature=$NAME:$VER", where "$VER" is optional. * * "--feature=WATCH:0" -> Feature enabled w/ version 0 (default version when enabled) * * "--feature=WATCH:7" -> Feature enabled w/ version 7 - * * "--feature=WATCH:" -> Feature disabled + * * "--feature=WATCH:" -> Feature status undefined, runtime API generated + * * "--feature=WATCH:UNAVAILABLE" -> Feature disabled */ - private fun parseFeatureArg(arg: String): FeatureArg { + private fun parseFeatureArg(arg: String): FeatureInfo { val featureArgs = arg.substring(FEATURE_ARG.length).split(":") val name = parseFeatureName(featureArgs[0]) - val version = featureArgs.getOrNull(1)?.toIntOrNull() - return FeatureArg(name, version) + return when (featureArgs.getOrNull(1)) { + null, "" -> FeatureInfo(name, null, readonly = false) + "UNAVAILABLE" -> FeatureInfo(name, null, readonly = true) + else -> FeatureInfo(name, featureArgs[1].toIntOrNull(), readonly = true) + } } private fun parseFeatureName(name: String): String = @@ -218,7 +231,7 @@ object SystemFeaturesGenerator { /* * Adds a generic query method to the class with the form: {@code public static boolean * maybeHasFeature(String featureName, int version)}, returning null if the feature version is - * undefined or not readonly. + * undefined or not (compile-time) readonly. * * This method is useful for internal usage within the framework, e.g., from the implementation * of {@link android.content.pm.PackageManager#hasSystemFeature(Context)}, when we may only @@ -267,7 +280,41 @@ object SystemFeaturesGenerator { builder.addMethod(methodBuilder.build()) } - private data class FeatureArg(val name: String, val version: Int?) + /* + * Adds a method to get all compile-time enabled features. + * + * This method is useful for internal usage within the framework to augment + * any system features that are parsed from the various partitions. + */ + private fun addGetFeaturesMethodToClass( + builder: TypeSpec.Builder, + features: Collection<FeatureInfo>, + ) { + val methodBuilder = + MethodSpec.methodBuilder("getCompileTimeAvailableFeatures") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addAnnotation(ClassName.get("android.annotation", "NonNull")) + .addJavadoc("Gets features marked as available at compile-time, keyed by name." + + "\n\n@hide") + .returns(ParameterizedTypeName.get( + ClassName.get(Map::class.java), + ClassName.get(String::class.java), + FEATUREINFO_CLASS)) + + val availableFeatures = features.filter { it.readonly && it.version != null } + methodBuilder.addStatement("Map<String, FeatureInfo> features = new \$T<>(\$L)", + HashMap::class.java, availableFeatures.size) + if (!availableFeatures.isEmpty()) { + methodBuilder.addStatement("FeatureInfo fi = new FeatureInfo()") + } + for (feature in availableFeatures) { + methodBuilder.addStatement("fi.name = \$T.\$N", PACKAGEMANAGER_CLASS, feature.name) + methodBuilder.addStatement("fi.version = \$L", feature.version) + methodBuilder.addStatement("features.put(fi.name, new FeatureInfo(fi))") + } + methodBuilder.addStatement("return features") + builder.addMethod(methodBuilder.build()) + } private data class FeatureInfo(val name: String, val version: Int?, val readonly: Boolean) } diff --git a/tools/systemfeatures/tests/golden/RoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoFeatures.java.gen index 724639b52d23..edbfc4237547 100644 --- a/tools/systemfeatures/tests/golden/RoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RoFeatures.java.gen @@ -3,16 +3,20 @@ // --readonly=true \ // --feature=WATCH:1 \ // --feature=WIFI:0 \ -// --feature=VULKAN:-1 \ +// --feature=VULKAN:UNAVAILABLE \ // --feature=AUTO: \ // --feature-apis=WATCH,PC package com.android.systemfeatures; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.content.pm.FeatureInfo; import android.content.pm.PackageManager; import com.android.aconfig.annotations.AssumeFalseForR8; import com.android.aconfig.annotations.AssumeTrueForR8; +import java.util.HashMap; +import java.util.Map; /** * @hide @@ -62,9 +66,8 @@ public final class RoFeatures { * * @hide */ - @AssumeFalseForR8 public static boolean hasFeatureAuto(Context context) { - return false; + return hasFeatureFallback(context, PackageManager.FEATURE_AUTO); } private static boolean hasFeatureFallback(Context context, String featureName) { @@ -79,10 +82,27 @@ public final class RoFeatures { switch (featureName) { case PackageManager.FEATURE_WATCH: return 1 >= version; case PackageManager.FEATURE_WIFI: return 0 >= version; - case PackageManager.FEATURE_VULKAN: return -1 >= version; - case PackageManager.FEATURE_AUTO: return false; + case PackageManager.FEATURE_VULKAN: return false; default: break; } return null; } + + /** + * Gets features marked as available at compile-time, keyed by name. + * + * @hide + */ + @NonNull + public static Map<String, FeatureInfo> getCompileTimeAvailableFeatures() { + Map<String, FeatureInfo> features = new HashMap<>(2); + FeatureInfo fi = new FeatureInfo(); + fi.name = PackageManager.FEATURE_WATCH; + fi.version = 1; + features.put(fi.name, new FeatureInfo(fi)); + fi.name = PackageManager.FEATURE_WIFI; + fi.version = 0; + features.put(fi.name, new FeatureInfo(fi)); + return features; + } } diff --git a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen index 59c5b4e8fecb..bf7a00679fa6 100644 --- a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen @@ -4,9 +4,13 @@ // --feature-apis=WATCH package com.android.systemfeatures; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.content.pm.FeatureInfo; import android.content.pm.PackageManager; +import java.util.HashMap; +import java.util.Map; /** * @hide @@ -32,4 +36,15 @@ public final class RoNoFeatures { public static Boolean maybeHasFeature(String featureName, int version) { return null; } + + /** + * Gets features marked as available at compile-time, keyed by name. + * + * @hide + */ + @NonNull + public static Map<String, FeatureInfo> getCompileTimeAvailableFeatures() { + Map<String, FeatureInfo> features = new HashMap<>(0); + return features; + } } diff --git a/tools/systemfeatures/tests/golden/RwFeatures.java.gen b/tools/systemfeatures/tests/golden/RwFeatures.java.gen index 6f897591e48f..b20b228f9814 100644 --- a/tools/systemfeatures/tests/golden/RwFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RwFeatures.java.gen @@ -3,13 +3,17 @@ // --readonly=false \ // --feature=WATCH:1 \ // --feature=WIFI:0 \ -// --feature=VULKAN:-1 \ +// --feature=VULKAN:UNAVAILABLE \ // --feature=AUTO: package com.android.systemfeatures; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.content.pm.FeatureInfo; import android.content.pm.PackageManager; +import java.util.HashMap; +import java.util.Map; /** * @hide @@ -62,4 +66,15 @@ public final class RwFeatures { public static Boolean maybeHasFeature(String featureName, int version) { return null; } + + /** + * Gets features marked as available at compile-time, keyed by name. + * + * @hide + */ + @NonNull + public static Map<String, FeatureInfo> getCompileTimeAvailableFeatures() { + Map<String, FeatureInfo> features = new HashMap<>(0); + return features; + } } diff --git a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen index 2111d564f28d..d91f5b62d8d4 100644 --- a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen @@ -3,8 +3,12 @@ // --readonly=false package com.android.systemfeatures; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.content.pm.FeatureInfo; +import java.util.HashMap; +import java.util.Map; /** * @hide @@ -21,4 +25,15 @@ public final class RwNoFeatures { public static Boolean maybeHasFeature(String featureName, int version) { return null; } + + /** + * Gets features marked as available at compile-time, keyed by name. + * + * @hide + */ + @NonNull + public static Map<String, FeatureInfo> getCompileTimeAvailableFeatures() { + Map<String, FeatureInfo> features = new HashMap<>(0); + return features; + } } diff --git a/tools/systemfeatures/tests/src/FeatureInfo.java b/tools/systemfeatures/tests/src/FeatureInfo.java new file mode 100644 index 000000000000..9d57edc64ca5 --- /dev/null +++ b/tools/systemfeatures/tests/src/FeatureInfo.java @@ -0,0 +1,30 @@ +/* + * 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.content.pm; + +/** Stub for testing */ +public final class FeatureInfo { + public String name; + public int version; + + public FeatureInfo() {} + + public FeatureInfo(FeatureInfo orig) { + name = orig.name; + version = orig.version; + } +} diff --git a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java index 6dfd244a807b..39f8fc44fe23 100644 --- a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java +++ b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.pm.FeatureInfo; import android.content.pm.PackageManager; import org.junit.Before; @@ -36,6 +37,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.Map; + @RunWith(JUnit4.class) public class SystemFeaturesGeneratorTest { @@ -57,6 +60,7 @@ public class SystemFeaturesGeneratorTest { assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); assertThat(RwNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + assertThat(RwNoFeatures.getCompileTimeAvailableFeatures()).isEmpty(); } @Test @@ -68,6 +72,7 @@ public class SystemFeaturesGeneratorTest { assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); assertThat(RoNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + assertThat(RoNoFeatures.getCompileTimeAvailableFeatures()).isEmpty(); // Also ensure we fall back to the PackageManager for feature APIs without an accompanying // versioned feature definition. @@ -101,6 +106,7 @@ public class SystemFeaturesGeneratorTest { assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); assertThat(RwFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + assertThat(RwFeatures.getCompileTimeAvailableFeatures()).isEmpty(); } @Test @@ -110,10 +116,13 @@ public class SystemFeaturesGeneratorTest { assertThat(RoFeatures.hasFeatureWatch(mContext)).isTrue(); assertThat(RoFeatures.hasFeatureWifi(mContext)).isTrue(); assertThat(RoFeatures.hasFeatureVulkan(mContext)).isFalse(); - assertThat(RoFeatures.hasFeatureAuto(mContext)).isFalse(); verify(mPackageManager, never()).hasSystemFeature(anyString(), anyInt()); - // For defined feature types, conditional queries should reflect the build-time versions. + // For defined feature types, conditional queries should reflect either: + // * Enabled if the feature version is specified + // * Disabled if UNAVAILABLE is specified + // * Unknown if no version value is provided + // VERSION=1 assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, -1)).isTrue(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isTrue(); @@ -124,15 +133,19 @@ public class SystemFeaturesGeneratorTest { assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isTrue(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 100)).isFalse(); - // VERSION=-1 - assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, -1)).isTrue(); + // VERSION=UNAVAILABLE + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, -1)).isFalse(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isFalse(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 100)).isFalse(); - // DISABLED - assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, -1)).isFalse(); - assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isFalse(); - assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 100)).isFalse(); + // VERSION= + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false); + assertThat(RoFeatures.hasFeatureAuto(mContext)).isFalse(); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(true); + assertThat(RoFeatures.hasFeatureAuto(mContext)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, -1)).isNull(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 100)).isNull(); // For feature APIs without an associated feature definition, conditional queries should // report null, and explicit queries should report runtime-defined versions. @@ -148,5 +161,12 @@ public class SystemFeaturesGeneratorTest { assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", -1)).isNull(); assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 100)).isNull(); + assertThat(RoFeatures.maybeHasFeature("", 0)).isNull(); + + Map<String, FeatureInfo> compiledFeatures = RoFeatures.getCompileTimeAvailableFeatures(); + assertThat(compiledFeatures.keySet()) + .containsExactly(PackageManager.FEATURE_WATCH, PackageManager.FEATURE_WIFI); + assertThat(compiledFeatures.get(PackageManager.FEATURE_WATCH).version).isEqualTo(1); + assertThat(compiledFeatures.get(PackageManager.FEATURE_WIFI).version).isEqualTo(0); } } |