diff options
174 files changed, 3938 insertions, 844 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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..13e3348b6a04 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; @@ -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/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/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/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/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/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..dc0d8a39dbc1 100644 --- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt +++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt @@ -31,7 +31,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,10 +43,10 @@ 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); * } @@ -67,7 +67,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 +92,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 +125,7 @@ object SystemFeaturesGenerator { featureArgs.associateByTo( features, { it.name }, - { FeatureInfo(it.name, it.version, readonly) }, + { FeatureInfo(it.name, it.version, it.readonly && readonly) }, ) outputClassName @@ -154,13 +157,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 = @@ -267,7 +274,5 @@ object SystemFeaturesGenerator { builder.addMethod(methodBuilder.build()) } - private data class FeatureArg(val name: String, val version: Int?) - 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..dfc2937dc41d 100644 --- a/tools/systemfeatures/tests/golden/RoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RoFeatures.java.gen @@ -3,7 +3,7 @@ // --readonly=true \ // --feature=WATCH:1 \ // --feature=WIFI:0 \ -// --feature=VULKAN:-1 \ +// --feature=VULKAN:UNAVAILABLE \ // --feature=AUTO: \ // --feature-apis=WATCH,PC package com.android.systemfeatures; @@ -62,9 +62,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,8 +78,7 @@ 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; diff --git a/tools/systemfeatures/tests/golden/RwFeatures.java.gen b/tools/systemfeatures/tests/golden/RwFeatures.java.gen index 6f897591e48f..89097fbfd180 100644 --- a/tools/systemfeatures/tests/golden/RwFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RwFeatures.java.gen @@ -3,7 +3,7 @@ // --readonly=false \ // --feature=WATCH:1 \ // --feature=WIFI:0 \ -// --feature=VULKAN:-1 \ +// --feature=VULKAN:UNAVAILABLE \ // --feature=AUTO: package com.android.systemfeatures; diff --git a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java index 6dfd244a807b..c3a23cbd8e48 100644 --- a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java +++ b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java @@ -110,10 +110,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 +127,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 +155,6 @@ 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(); } } |